From f6019ecba9153f09acf68851f07572bf38a69d7e Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 10 Mar 2026 11:45:46 +0100 Subject: [PATCH] feat(compliance-scope): Pflicht/Optional-Klassifikation, offene Fragen sichtbar + Navigation Block-Sidebar zeigt gruen/orange Status pro Block, klickbare Zusammenfassung offener Pflichtfragen unter dem Fortschrittsbalken, und visuelles Highlighting (linker Rand) fuer unbeantwortete Pflichtfragen. Sidebar-Haken wird gesetzt wenn alle Pflichtfragen beantwortet und Auswertung vorhanden. Co-Authored-By: Claude Opus 4.6 --- .../app/sdk/compliance-scope/page.tsx | 7 ++ .../sdk/compliance-scope/ScopeWizardTab.tsx | 82 ++++++++++++++++--- .../lib/sdk/compliance-scope-profiling.ts | 26 ++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/admin-compliance/app/sdk/compliance-scope/page.tsx b/admin-compliance/app/sdk/compliance-scope/page.tsx index 322df33..e07c211 100644 --- a/admin-compliance/app/sdk/compliance-scope/page.tsx +++ b/admin-compliance/app/sdk/compliance-scope/page.tsx @@ -214,6 +214,13 @@ export default function ComplianceScopePage() { return completionStats.isComplete }, [completionStats.isComplete]) + // Mark sidebar step as complete when all required questions answered AND decision exists + useEffect(() => { + if (completionStats.isComplete && scopeState.decision) { + dispatch({ type: 'COMPLETE_STEP', payload: 'compliance-scope' }) + } + }, [completionStats.isComplete, scopeState.decision, dispatch]) + if (isLoading) { return (
diff --git a/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx b/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx index d1b6eaf..c684b76 100644 --- a/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx +++ b/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useState, useCallback, useEffect, useMemo } from 'react' import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types' -import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers } from '@/lib/sdk/compliance-scope-profiling' +import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling' import type { ScopeQuestionBlockId } from '@/lib/sdk/compliance-scope-types' import { useSDK } from '@/lib/sdk' @@ -346,6 +346,10 @@ export function ScopeWizardTab({ {SCOPE_QUESTION_BLOCKS.map((block, idx) => { const progress = getBlockProgress(answers, block.id) const isActive = idx === currentBlockIndex + const unanswered = getUnansweredRequiredQuestions(answers, block.id) + const hasRequired = block.questions.some(q => q.required) + const allRequiredDone = hasRequired && unanswered.length === 0 + return (
@@ -397,6 +417,40 @@ export function ScopeWizardTab({ style={{ width: `${totalProgress}%` }} />
+ + {/* Clickable unanswered required questions summary */} + {(() => { + const allUnanswered = getUnansweredRequiredQuestions(answers) + if (allUnanswered.length === 0) return null + + // Group by block + const byBlock = new Map() + for (const item of allUnanswered) { + if (!byBlock.has(item.blockId)) { + const blockIndex = SCOPE_QUESTION_BLOCKS.findIndex(b => b.id === item.blockId) + byBlock.set(item.blockId, { blockTitle: item.blockTitle, blockIndex, count: 0 }) + } + byBlock.get(item.blockId)!.count++ + } + + return ( +
+ ⚠ Offene Pflichtfragen: + {Array.from(byBlock.entries()).map(([blockId, info], i) => ( + + {i > 0 && ·} + + + ))} +
+ ) + })()} {/* Current Block */} @@ -455,11 +509,19 @@ export function ScopeWizardTab({ {/* Questions */}
- {currentBlock.questions.map((question) => ( -
- {renderQuestion(question)} -
- ))} + {currentBlock.questions.map((question) => { + const isAnswered = answers.some(a => a.questionId === question.id) + const borderClass = question.required + ? isAnswered + ? 'border-l-4 border-l-green-400 pl-4' + : 'border-l-4 border-l-orange-400 pl-4' + : '' + return ( +
+ {renderQuestion(question)} +
+ ) + })}
diff --git a/admin-compliance/lib/sdk/compliance-scope-profiling.ts b/admin-compliance/lib/sdk/compliance-scope-profiling.ts index fa9bc49..053e123 100644 --- a/admin-compliance/lib/sdk/compliance-scope-profiling.ts +++ b/admin-compliance/lib/sdk/compliance-scope-profiling.ts @@ -982,3 +982,29 @@ export function getAllQuestions(): ScopeProfilingQuestion[] { ...HIDDEN_SCORING_QUESTIONS, ] } + +/** + * Get unanswered required questions, optionally filtered by block. + * Returns block metadata along with each question for navigation. + */ +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 +}