From 3efa391de5cf5b93e18f3dc50f6c73dd91f123f1 Mon Sep 17 00:00:00 2001 From: Benjamin Boenisch Date: Wed, 25 Feb 2026 22:26:40 +0100 Subject: [PATCH] feat(sdk): add global seq numbering and visibleWhen for SDK flow navigation Fix interleaved step ordering by introducing global sequence numbers (100-4700) instead of package-relative order. Add conditional visibility (visibleWhen) for optional steps like Import and DSFA. Fix TOM/workflow prerequisite bugs. Co-Authored-By: Claude Opus 4.6 --- .../SDKPipelineSidebar/SDKPipelineSidebar.tsx | 47 +-- .../components/sdk/Sidebar/SDKSidebar.tsx | 135 ++------- admin-compliance/lib/sdk/context.tsx | 19 +- admin-compliance/lib/sdk/types.ts | 278 +++++++++++++++--- 4 files changed, 272 insertions(+), 207 deletions(-) diff --git a/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx b/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx index ff97f9d..e6330b2 100644 --- a/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx +++ b/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx @@ -264,13 +264,11 @@ function SidebarContent({ onNavigate }: SidebarContentProps) { return packageCompletion[prevPkg.id] < 100 } - // Get visible steps based on customer type - const getVisibleSteps = (packageId: SDKPackageId): SDKStep[] => { + // Filter steps based on visibleWhen conditions + const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => { const steps = getStepsForPackage(packageId) return steps.filter(step => { - if (step.id === 'import' && state.customerType === 'new') { - return false - } + if (step.visibleWhen) return step.visibleWhen(state) return true }) } @@ -282,7 +280,7 @@ function SidebarContent({ onNavigate }: SidebarContentProps) { - {/* Zusatzmodule */} -
-
- Zusatzmodule -
-
- {[ - { href: '/sdk/rag', label: 'Legal RAG', icon: 'πŸ”' }, - { href: '/sdk/quality', label: 'AI Quality', icon: 'βœ…' }, - { href: '/sdk/security-backlog', label: 'Security Backlog', icon: '⚠️' }, - { href: '/sdk/compliance-hub', label: 'Compliance Hub', icon: 'πŸ“Š' }, - { href: '/sdk/dsms', label: 'DSMS', icon: 'πŸ›‘οΈ' }, - { href: '/sdk/academy', label: 'Academy', icon: 'πŸŽ“' }, - { href: '/sdk/whistleblower', label: 'Whistleblower', icon: 'πŸ“’' }, - { href: '/sdk/incidents', label: 'Incidents', icon: '🚨' }, - { href: '/sdk/reporting', label: 'Reporting', icon: 'πŸ“ˆ' }, - { href: '/sdk/industry-templates', label: 'Branchenvorlagen', icon: '🏒' }, - { href: '/sdk/document-crawler', label: 'Doc Crawler', icon: 'πŸ“„' }, - { href: '/sdk/advisory-board', label: 'Beirat', icon: 'πŸ’¬' }, - ].map(mod => ( - - {mod.icon} - {mod.label} - - ))} -
-
- {/* Quick Info */} {currentStep && (
diff --git a/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx b/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx index b7fb544..3260c2e 100644 --- a/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx +++ b/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx @@ -347,14 +347,11 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP return steps.some(s => s.url === pathname) } - // Filter steps based on customer type - const getVisibleSteps = (packageId: SDKPackageId): SDKStep[] => { + // Filter steps based on visibleWhen conditions + const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => { const steps = getStepsForPackage(packageId) return steps.filter(step => { - // Hide import step for new customers - if (step.id === 'import' && state.customerType === 'new') { - return false - } + if (step.visibleWhen) return step.visibleWhen(state) return true }) } @@ -397,7 +394,7 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP {/* Navigation - 5 Packages */}
diff --git a/admin-compliance/lib/sdk/context.tsx b/admin-compliance/lib/sdk/context.tsx index 1c9bc6b..f3502b0 100644 --- a/admin-compliance/lib/sdk/context.tsx +++ b/admin-compliance/lib/sdk/context.tsx @@ -99,6 +99,9 @@ const initialState: SDKState = { dsrConfig: null, escalationWorkflows: [], + // IACE (Industrial AI Compliance Engine) + iaceProjects: [], + // Security sbom: null, securityIssues: [], @@ -705,26 +708,26 @@ export function SDKProvider({ ) const goToNextStep = useCallback(() => { - const nextStep = getNextStep(state.currentStep) + const nextStep = getNextStep(state.currentStep, state) if (nextStep) { goToStep(nextStep.id) } - }, [state.currentStep, goToStep]) + }, [state, goToStep]) const goToPreviousStep = useCallback(() => { - const prevStep = getPreviousStep(state.currentStep) + const prevStep = getPreviousStep(state.currentStep, state) if (prevStep) { goToStep(prevStep.id) } - }, [state.currentStep, goToStep]) + }, [state, goToStep]) const canGoNext = useMemo(() => { - return getNextStep(state.currentStep) !== undefined - }, [state.currentStep]) + return getNextStep(state.currentStep, state) !== undefined + }, [state]) const canGoPrevious = useMemo(() => { - return getPreviousStep(state.currentStep) !== undefined - }, [state.currentStep]) + return getPreviousStep(state.currentStep, state) !== undefined + }, [state]) // Progress const completionPercentage = useMemo(() => getCompletionPercentage(state), [state]) diff --git a/admin-compliance/lib/sdk/types.ts b/admin-compliance/lib/sdk/types.ts index 388d44f..97951d2 100644 --- a/admin-compliance/lib/sdk/types.ts +++ b/admin-compliance/lib/sdk/types.ts @@ -62,6 +62,86 @@ export type LegalForm = | 'stiftung' // Foundation | 'other' // Other +// ============================================================================= +// MACHINE BUILDER PROFILE (IACE - Industrial AI Compliance Engine) +// ============================================================================= + +export type MachineProductType = 'test_stand' | 'robot_cell' | 'special_machine' | 'production_line' | 'other' + +export type AIIntegrationType = 'vision' | 'predictive_maintenance' | 'quality_control' | 'robot_control' | 'process_optimization' | 'other' + +export type HumanOversightLevel = 'full' | 'partial' | 'minimal' | 'none' + +export type CriticalSector = 'energy' | 'water' | 'transport' | 'health' | 'pharma' | 'automotive' | 'defense' + +export interface MachineBuilderProfile { + // Produkt + productTypes: MachineProductType[] + productDescription: string + productPride: string + containsSoftware: boolean + containsFirmware: boolean + containsAI: boolean + aiIntegrationType: AIIntegrationType[] + + // Sicherheit + hasSafetyFunction: boolean + safetyFunctionDescription: string + autonomousBehavior: boolean + humanOversightLevel: HumanOversightLevel + + // Konnektivitaet + isNetworked: boolean + hasRemoteAccess: boolean + hasOTAUpdates: boolean + updateMechanism: string + + // Markt & Kunden + exportMarkets: string[] + criticalSectorClients: boolean + criticalSectors: CriticalSector[] + oemClients: boolean + + // CE + ceMarkingRequired: boolean + existingCEProcess: boolean + hasRiskAssessment: boolean +} + +export const MACHINE_PRODUCT_TYPE_LABELS: Record = { + test_stand: 'Pruefstand', + robot_cell: 'Roboterzelle', + special_machine: 'Sondermaschine', + production_line: 'Produktionslinie', + other: 'Sonstige', +} + +export const AI_INTEGRATION_TYPE_LABELS: Record = { + vision: 'Bildverarbeitung / Machine Vision', + predictive_maintenance: 'Predictive Maintenance', + quality_control: 'Qualitaetskontrolle', + robot_control: 'Robotersteuerung', + process_optimization: 'Prozessoptimierung', + other: 'Sonstige', +} + +export const HUMAN_OVERSIGHT_LABELS: Record = { + full: 'Vollstaendig (Mensch entscheidet immer)', + partial: 'Teilweise (Mensch ueberwacht)', + minimal: 'Minimal (Mensch greift nur bei Stoerung ein)', + none: 'Keine (vollautonomer Betrieb)', +} + +export const CRITICAL_SECTOR_LABELS: Record = { + energy: 'Energie', + water: 'Wasser', + transport: 'Transport / Verkehr', + health: 'Gesundheit', + pharma: 'Pharma', + automotive: 'Automotive', + defense: 'Verteidigung', +} + export interface CompanyProfile { // Basic Info companyName: string @@ -102,6 +182,9 @@ export interface CompanyProfile { legalContactName: string | null legalContactEmail: string | null + // Machine Builder (IACE) + machineBuilder?: MachineBuilderProfile + // Completion Status isComplete: boolean completedAt: Date | null @@ -284,6 +367,7 @@ export type CommandType = 'ACTION' | 'NAVIGATION' | 'SEARCH' | 'GENERATE' | 'HEL export interface SDKStep { id: string + seq: number // Globale Sequenznummer (100, 200, 300, ...) phase: SDKPhase package: SDKPackageId order: number @@ -294,6 +378,7 @@ export interface SDKStep { checkpointId: string prerequisiteSteps: string[] isOptional: boolean + visibleWhen?: (state: SDKState) => boolean // Konditionale Sichtbarkeit } export const SDK_STEPS: SDKStep[] = [ @@ -302,6 +387,7 @@ export const SDK_STEPS: SDKStep[] = [ // ============================================================================= { id: 'company-profile', + seq: 100, phase: 1, package: 'vorbereitung', order: 1, @@ -315,6 +401,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'compliance-scope', + seq: 200, phase: 1, package: 'vorbereitung', order: 2, @@ -328,6 +415,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'use-case-assessment', + seq: 300, phase: 1, package: 'vorbereitung', order: 3, @@ -341,6 +429,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'import', + seq: 400, phase: 1, package: 'vorbereitung', order: 4, @@ -350,10 +439,12 @@ export const SDK_STEPS: SDKStep[] = [ url: '/sdk/import', checkpointId: 'CP-IMP', prerequisiteSteps: ['use-case-assessment'], - isOptional: true, // Nur für Bestandskunden + isOptional: true, + visibleWhen: (state) => state.customerType === 'existing', }, { id: 'screening', + seq: 500, phase: 1, package: 'vorbereitung', order: 5, @@ -367,6 +458,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'modules', + seq: 600, phase: 1, package: 'vorbereitung', order: 6, @@ -380,6 +472,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'source-policy', + seq: 700, phase: 1, package: 'vorbereitung', order: 7, @@ -397,6 +490,7 @@ export const SDK_STEPS: SDKStep[] = [ // ============================================================================= { id: 'requirements', + seq: 1000, phase: 1, package: 'analyse', order: 1, @@ -410,6 +504,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'controls', + seq: 1100, phase: 1, package: 'analyse', order: 2, @@ -423,6 +518,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'evidence', + seq: 1200, phase: 1, package: 'analyse', order: 3, @@ -436,6 +532,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'risks', + seq: 1300, phase: 1, package: 'analyse', order: 4, @@ -449,6 +546,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'ai-act', + seq: 1400, phase: 1, package: 'analyse', order: 5, @@ -462,6 +560,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'audit-checklist', + seq: 1500, phase: 1, package: 'analyse', order: 6, @@ -475,6 +574,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'audit-report', + seq: 1600, phase: 1, package: 'analyse', order: 7, @@ -492,6 +592,7 @@ export const SDK_STEPS: SDKStep[] = [ // ============================================================================= { id: 'obligations', + seq: 2000, phase: 2, package: 'dokumentation', order: 1, @@ -505,6 +606,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'dsfa', + seq: 2100, phase: 2, package: 'dokumentation', order: 2, @@ -514,10 +616,17 @@ export const SDK_STEPS: SDKStep[] = [ url: '/sdk/dsfa', checkpointId: 'CP-DSFA', prerequisiteSteps: ['obligations'], - isOptional: true, // Only if dsfa_recommended + isOptional: true, + visibleWhen: (state) => { + const level = state.complianceScope?.decision?.determinedLevel + if (level && ['L2', 'L3', 'L4'].includes(level)) return true + const triggers = state.complianceScope?.decision?.triggeredHardTriggers || [] + return triggers.some(t => t.rule.dsfaRequired) + }, }, { id: 'tom', + seq: 2200, phase: 2, package: 'dokumentation', order: 3, @@ -526,11 +635,12 @@ export const SDK_STEPS: SDKStep[] = [ description: 'Technische & Org. Maßnahmen', url: '/sdk/tom', checkpointId: 'CP-TOM', - prerequisiteSteps: ['dsfa'], + prerequisiteSteps: ['obligations'], isOptional: false, }, { id: 'loeschfristen', + seq: 2300, phase: 2, package: 'dokumentation', order: 4, @@ -544,6 +654,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'vvt', + seq: 2400, phase: 2, package: 'dokumentation', order: 5, @@ -561,6 +672,7 @@ export const SDK_STEPS: SDKStep[] = [ // ============================================================================= { id: 'einwilligungen', + seq: 3000, phase: 2, package: 'rechtliche-texte', order: 1, @@ -574,6 +686,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'consent', + seq: 3100, phase: 2, package: 'rechtliche-texte', order: 2, @@ -587,6 +700,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'cookie-banner', + seq: 3200, phase: 2, package: 'rechtliche-texte', order: 3, @@ -600,6 +714,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'document-generator', + seq: 3300, phase: 2, package: 'rechtliche-texte', order: 4, @@ -610,9 +725,11 @@ export const SDK_STEPS: SDKStep[] = [ checkpointId: 'CP-DOCGEN', prerequisiteSteps: ['cookie-banner'], isOptional: true, + visibleWhen: () => true, }, { id: 'workflow', + seq: 3400, phase: 2, package: 'rechtliche-texte', order: 5, @@ -621,7 +738,7 @@ export const SDK_STEPS: SDKStep[] = [ description: 'Versionierung & Freigabe-Workflow', url: '/sdk/workflow', checkpointId: 'CP-WRKF', - prerequisiteSteps: ['document-generator'], + prerequisiteSteps: ['cookie-banner'], isOptional: false, }, @@ -630,6 +747,7 @@ export const SDK_STEPS: SDKStep[] = [ // ============================================================================= { id: 'dsr', + seq: 4000, phase: 2, package: 'betrieb', order: 1, @@ -643,6 +761,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'escalations', + seq: 4100, phase: 2, package: 'betrieb', order: 2, @@ -656,6 +775,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'vendor-compliance', + seq: 4200, phase: 2, package: 'betrieb', order: 3, @@ -669,6 +789,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'consent-management', + seq: 4300, phase: 2, package: 'betrieb', order: 4, @@ -682,6 +803,7 @@ export const SDK_STEPS: SDKStep[] = [ }, { id: 'notfallplan', + seq: 4400, phase: 2, package: 'betrieb', order: 5, @@ -693,6 +815,48 @@ export const SDK_STEPS: SDKStep[] = [ prerequisiteSteps: ['consent-management'], isOptional: false, }, + { + id: 'incidents', + seq: 4500, + phase: 2, + package: 'betrieb', + order: 6, + name: 'Incident Management', + nameShort: 'Incidents', + description: 'Datenpannen erfassen, bewerten und melden (Art. 33/34 DSGVO)', + url: '/sdk/incidents', + checkpointId: 'CP-INC', + prerequisiteSteps: ['notfallplan'], + isOptional: false, + }, + { + id: 'whistleblower', + seq: 4600, + phase: 2, + package: 'betrieb', + order: 7, + name: 'Hinweisgebersystem', + nameShort: 'Whistleblower', + description: 'Anonymes Meldesystem gemaess HinSchG', + url: '/sdk/whistleblower', + checkpointId: 'CP-WB', + prerequisiteSteps: ['incidents'], + isOptional: false, + }, + { + id: 'academy', + seq: 4700, + phase: 2, + package: 'betrieb', + order: 8, + name: 'Compliance Academy', + nameShort: 'Academy', + description: 'Mitarbeiter-Schulungen & Zertifikate', + url: '/sdk/academy', + checkpointId: 'CP-ACAD', + prerequisiteSteps: ['whistleblower'], + isOptional: false, + }, ] // ============================================================================= @@ -1323,6 +1487,9 @@ export interface SDKState { dsrConfig: DSRConfig | null escalationWorkflows: EscalationWorkflow[] + // IACE (Industrial AI Compliance Engine) + iaceProjects: IACEProjectSummary[] + // Security sbom: SBOM | null securityIssues: SecurityIssue[] @@ -1334,6 +1501,28 @@ export interface SDKState { preferences: UserPreferences } +// ============================================================================= +// IACE PROJECT TYPES +// ============================================================================= + +export type IACEProjectStatus = 'draft' | 'onboarding' | 'classification' | 'hazard_analysis' | 'mitigation' | 'verification' | 'tech_file' | 'completed' | 'archived' + +export interface IACEProjectSummary { + id: string + machineName: string + machineType: MachineProductType + status: IACEProjectStatus + completenessScore: number + riskSummary: { + critical: number + high: number + medium: number + low: number + } + createdAt: string + updatedAt: string +} + // ============================================================================= // SDK ACTIONS // ============================================================================= @@ -1406,48 +1595,59 @@ export function getStepByUrl(url: string): SDKStep | undefined { } export function getStepsForPhase(phase: SDKPhase): SDKStep[] { - return SDK_STEPS.filter(s => s.phase === phase).sort((a, b) => a.order - b.order) + return SDK_STEPS.filter(s => s.phase === phase).sort((a, b) => a.seq - b.seq) } -export function getNextStep(currentStepId: string): SDKStep | undefined { - const currentStep = getStepById(currentStepId) - if (!currentStep) return undefined +// Alle Steps global nach seq sortiert +function getAllStepsSorted(): SDKStep[] { + return [...SDK_STEPS].sort((a, b) => a.seq - b.seq) +} - const stepsInPhase = getStepsForPhase(currentStep.phase) - const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) - - if (currentIndex < stepsInPhase.length - 1) { - return stepsInPhase[currentIndex + 1] - } - - // Move to next phase - if (currentStep.phase === 1) { - return getStepsForPhase(2)[0] - } +// Sichtbare Steps (state-abhaengig) +export function getVisibleSteps(state: SDKState): SDKStep[] { + return getAllStepsSorted().filter(step => { + if (step.visibleWhen) return step.visibleWhen(state) + return true + }) +} +// Naechster sichtbarer Step +export function getNextVisibleStep(currentStepId: string, state: SDKState): SDKStep | undefined { + const visible = getVisibleSteps(state) + const idx = visible.findIndex(s => s.id === currentStepId) + if (idx >= 0 && idx < visible.length - 1) return visible[idx + 1] return undefined } -export function getPreviousStep(currentStepId: string): SDKStep | undefined { - const currentStep = getStepById(currentStepId) - if (!currentStep) return undefined - - const stepsInPhase = getStepsForPhase(currentStep.phase) - const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) - - if (currentIndex > 0) { - return stepsInPhase[currentIndex - 1] - } - - // Move to previous phase - if (currentStep.phase === 2) { - const phase1Steps = getStepsForPhase(1) - return phase1Steps[phase1Steps.length - 1] - } - +// Vorheriger sichtbarer Step +export function getPreviousVisibleStep(currentStepId: string, state: SDKState): SDKStep | undefined { + const visible = getVisibleSteps(state) + const idx = visible.findIndex(s => s.id === currentStepId) + if (idx > 0) return visible[idx - 1] return undefined } +export function getNextStep(currentStepId: string, state?: SDKState): SDKStep | undefined { + if (!state) { + // Fallback: seq-sortiert ohne Sichtbarkeitspruefung + const sorted = getAllStepsSorted() + const idx = sorted.findIndex(s => s.id === currentStepId) + if (idx >= 0 && idx < sorted.length - 1) return sorted[idx + 1] + return undefined + } + return getNextVisibleStep(currentStepId, state) +} + +export function getPreviousStep(currentStepId: string, state?: SDKState): SDKStep | undefined { + if (!state) { + const sorted = getAllStepsSorted() + const idx = sorted.findIndex(s => s.id === currentStepId) + if (idx > 0) return sorted[idx - 1] + return undefined + } + return getPreviousVisibleStep(currentStepId, state) +} + export function calculateRiskScore(likelihood: RiskLikelihood, impact: RiskImpact): number { return likelihood * impact } @@ -1490,7 +1690,7 @@ export function getPackageById(packageId: SDKPackageId): SDKPackage | undefined } export function getStepsForPackage(packageId: SDKPackageId): SDKStep[] { - return SDK_STEPS.filter(s => s.package === packageId).sort((a, b) => a.order - b.order) + return SDK_STEPS.filter(s => s.package === packageId).sort((a, b) => a.seq - b.seq) } export function getPackageCompletionPercentage(state: SDKState, packageId: SDKPackageId): number { @@ -1545,9 +1745,9 @@ export function isPackageUnlocked(state: SDKState, packageId: SDKPackageId): boo return getPackageCompletionPercentage(state, prevPackage.id) === 100 } +/** @deprecated Use getVisibleSteps(state) instead */ export function getVisibleStepsForCustomerType(customerType: CustomerType): SDKStep[] { - return SDK_STEPS.filter(step => { - // Import step is only for existing customers + return getAllStepsSorted().filter(step => { if (step.id === 'import') { return customerType === 'existing' }