refactor(admin): split compliance-scope-profiling.ts (1171 LOC) into focused modules
Split the monolithic file into three content modules plus a barrel re-export: - compliance-scope-profiling-blocks.ts (489 LOC): blocks 1-7, hidden questions, autofill IDs - compliance-scope-profiling-vvt-blocks.ts (274 LOC): blocks 8-9, SCOPE_QUESTION_BLOCKS aggregate - compliance-scope-profiling-helpers.ts (359 LOC): all prefill/export/progress functions - compliance-scope-profiling.ts (41 LOC): barrel re-export preserving existing import paths All files under the 500 LOC hard cap. No consumer changes needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
358
admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts
Normal file
358
admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import type {
|
||||
ScopeQuestionBlockId,
|
||||
ScopeProfilingQuestion,
|
||||
ScopeProfilingAnswer,
|
||||
} from './compliance-scope-types'
|
||||
import type { CompanyProfile } from './types'
|
||||
import {
|
||||
HIDDEN_SCORING_QUESTIONS,
|
||||
} from './compliance-scope-profiling-blocks'
|
||||
import {
|
||||
SCOPE_QUESTION_BLOCKS,
|
||||
} from './compliance-scope-profiling-vvt-blocks'
|
||||
|
||||
/**
|
||||
* Prefill scope answers from CompanyProfile.
|
||||
*/
|
||||
export function prefillFromCompanyProfile(
|
||||
profile: CompanyProfile
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
|
||||
// dpoName -> org_has_dsb (auto-filled, not shown in UI)
|
||||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||||
answers.push({
|
||||
questionId: 'org_has_dsb',
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
|
||||
// offerings -> prod_type mapping (auto-filled, not shown in UI)
|
||||
if (profile.offerings && profile.offerings.length > 0) {
|
||||
const prodTypes: string[] = []
|
||||
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
|
||||
|
||||
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
|
||||
prodTypes.push('webapp')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
|
||||
) {
|
||||
prodTypes.push('mobile')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
|
||||
prodTypes.push('saas')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('onpremise') || o.includes('on-premise')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('onpremise')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('api'))) {
|
||||
prodTypes.push('api')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
|
||||
prodTypes.push('iot')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('beratung') || o.includes('consulting')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('beratung')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('handel')
|
||||
}
|
||||
|
||||
if (prodTypes.length > 0) {
|
||||
answers.push({
|
||||
questionId: 'prod_type',
|
||||
value: prodTypes,
|
||||
})
|
||||
}
|
||||
|
||||
// webshop auto-fill
|
||||
if (offeringsLower.some((o) => o.includes('webshop') || o.includes('shop'))) {
|
||||
answers.push({
|
||||
questionId: 'prod_webshop',
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auto-filled scoring values for questions removed from UI.
|
||||
*/
|
||||
export function getAutoFilledScoringAnswers(
|
||||
profile: CompanyProfile
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
|
||||
if (profile.employeeCount != null) {
|
||||
answers.push({ questionId: 'org_employee_count', value: profile.employeeCount })
|
||||
}
|
||||
if (profile.annualRevenue) {
|
||||
answers.push({ questionId: 'org_annual_revenue', value: profile.annualRevenue })
|
||||
}
|
||||
if (profile.industry && profile.industry.length > 0) {
|
||||
answers.push({ questionId: 'org_industry', value: profile.industry.join(', ') })
|
||||
}
|
||||
if (profile.businessModel) {
|
||||
answers.push({ questionId: 'org_business_model', value: profile.businessModel })
|
||||
}
|
||||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||||
answers.push({ questionId: 'org_has_dsb', value: true })
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile info summary for display in "Aus Profil" info boxes.
|
||||
*/
|
||||
export function getProfileInfoForBlock(
|
||||
profile: CompanyProfile,
|
||||
blockId: ScopeQuestionBlockId
|
||||
): { label: string; value: string }[] {
|
||||
const items: { label: string; value: string }[] = []
|
||||
|
||||
if (blockId === 'organisation') {
|
||||
if (profile.industry && profile.industry.length > 0) items.push({ label: 'Branche', value: profile.industry.join(', ') })
|
||||
if (profile.employeeCount) items.push({ label: 'Mitarbeiter', value: profile.employeeCount })
|
||||
if (profile.annualRevenue) items.push({ label: 'Umsatz', value: profile.annualRevenue })
|
||||
if (profile.businessModel) items.push({ label: 'Geschäftsmodell', value: profile.businessModel })
|
||||
if (profile.dpoName) items.push({ label: 'DSB', value: profile.dpoName })
|
||||
}
|
||||
|
||||
if (blockId === 'product') {
|
||||
if (profile.offerings && profile.offerings.length > 0) {
|
||||
items.push({ label: 'Angebote', value: profile.offerings.join(', ') })
|
||||
}
|
||||
const hasWebshop = profile.offerings?.some(o => o.toLowerCase().includes('webshop') || o.toLowerCase().includes('shop'))
|
||||
if (hasWebshop) items.push({ label: 'Webshop', value: 'Ja' })
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill scope answers from VVT profiling answers
|
||||
*/
|
||||
export function prefillFromVVTAnswers(
|
||||
vvtAnswers: Record<string, unknown>
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
const reverseMap: Record<string, string> = {}
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
for (const q of block.questions) {
|
||||
if (q.mapsToVVTQuestion) {
|
||||
reverseMap[q.mapsToVVTQuestion] = q.id
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
|
||||
const scopeQuestionId = reverseMap[vvtQuestionId]
|
||||
if (scopeQuestionId) {
|
||||
answers.push({ questionId: scopeQuestionId, value: vvtValue })
|
||||
}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill scope answers from Loeschfristen profiling answers
|
||||
*/
|
||||
export function prefillFromLoeschfristenAnswers(
|
||||
lfAnswers: Array<{ questionId: string; value: unknown }>
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
const reverseMap: Record<string, string> = {}
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
for (const q of block.questions) {
|
||||
if (q.mapsToLFQuestion) {
|
||||
reverseMap[q.mapsToLFQuestion] = q.id
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const lfAnswer of lfAnswers) {
|
||||
const scopeQuestionId = reverseMap[lfAnswer.questionId]
|
||||
if (scopeQuestionId) {
|
||||
answers.push({ questionId: scopeQuestionId, value: lfAnswer.value })
|
||||
}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers in VVT format
|
||||
*/
|
||||
export function exportToVVTAnswers(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Record<string, unknown> {
|
||||
const vvtAnswers: Record<string, unknown> = {}
|
||||
for (const answer of scopeAnswers) {
|
||||
let question: ScopeProfilingQuestion | undefined
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
question = block.questions.find((q) => q.id === answer.questionId)
|
||||
if (question) break
|
||||
}
|
||||
if (question?.mapsToVVTQuestion) {
|
||||
vvtAnswers[question.mapsToVVTQuestion] = answer.value
|
||||
}
|
||||
}
|
||||
return vvtAnswers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers in Loeschfristen format
|
||||
*/
|
||||
export function exportToLoeschfristenAnswers(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Array<{ questionId: string; value: unknown }> {
|
||||
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
|
||||
for (const answer of scopeAnswers) {
|
||||
let question: ScopeProfilingQuestion | undefined
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
question = block.questions.find((q) => q.id === answer.questionId)
|
||||
if (question) break
|
||||
}
|
||||
if (question?.mapsToLFQuestion) {
|
||||
lfAnswers.push({ questionId: question.mapsToLFQuestion, value: answer.value })
|
||||
}
|
||||
}
|
||||
return lfAnswers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers for TOM generator
|
||||
*/
|
||||
export function exportToTOMProfile(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Record<string, unknown> {
|
||||
const tomProfile: Record<string, unknown> = {}
|
||||
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
|
||||
|
||||
tomProfile.industry = getVal('org_industry')
|
||||
tomProfile.employeeCount = getVal('org_employee_count')
|
||||
tomProfile.hasDataMinors = getVal('data_minors')
|
||||
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
|
||||
? (getVal('data_art9') as string[]).length > 0
|
||||
: false
|
||||
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
|
||||
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
|
||||
? !(getVal('proc_ai_usage') as string[]).includes('keine')
|
||||
: false
|
||||
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
|
||||
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
|
||||
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
|
||||
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
|
||||
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
|
||||
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
|
||||
tomProfile.hasTraining = getVal('proc_training')
|
||||
|
||||
return tomProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is complete (all required questions answered)
|
||||
*/
|
||||
export function isBlockComplete(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId: ScopeQuestionBlockId
|
||||
): boolean {
|
||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||||
if (!block) return false
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress for a specific block (0-100)
|
||||
*/
|
||||
export function getBlockProgress(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId: ScopeQuestionBlockId
|
||||
): number {
|
||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||||
if (!block) return 0
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
if (requiredQuestions.length === 0) return 100
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
const answeredCount = requiredQuestions.filter((q) =>
|
||||
answeredQuestionIds.has(q.id)
|
||||
).length
|
||||
return Math.round((answeredCount / requiredQuestions.length) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total progress across all blocks (0-100)
|
||||
*/
|
||||
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
|
||||
let totalRequired = 0
|
||||
let totalAnswered = 0
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
totalRequired += requiredQuestions.length
|
||||
totalAnswered += requiredQuestions.filter((q) =>
|
||||
answeredQuestionIds.has(q.id)
|
||||
).length
|
||||
}
|
||||
if (totalRequired === 0) return 100
|
||||
return Math.round((totalAnswered / totalRequired) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get answer value for a specific question
|
||||
*/
|
||||
export function getAnswerValue(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
questionId: string
|
||||
): unknown {
|
||||
const answer = answers.find((a) => a.questionId === questionId)
|
||||
return answer?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all questions as a flat array (including hidden auto-filled questions)
|
||||
*/
|
||||
export function getAllQuestions(): ScopeProfilingQuestion[] {
|
||||
return [
|
||||
...SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions),
|
||||
...HIDDEN_SCORING_QUESTIONS,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unanswered required questions, optionally filtered by block.
|
||||
*/
|
||||
export function getUnansweredRequiredQuestions(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId?: ScopeQuestionBlockId
|
||||
): { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] {
|
||||
const answeredIds = new Set(answers.map((a) => a.questionId))
|
||||
const blocks = blockId
|
||||
? SCOPE_QUESTION_BLOCKS.filter((b) => b.id === blockId)
|
||||
: SCOPE_QUESTION_BLOCKS
|
||||
|
||||
const result: { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] = []
|
||||
for (const block of blocks) {
|
||||
for (const q of block.questions) {
|
||||
if (q.required && !answeredIds.has(q.id)) {
|
||||
result.push({ blockId: block.id, blockTitle: block.title, question: q })
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user