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>
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
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
|
|
}
|