This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx
BreakPilot Dev dff2ef796b feat(admin-v2): Major SDK/Compliance overhaul and new modules
SDK modules added/enhanced:
- compliance-hub, compliance-scope, consent-management, notfallplan
- audit-report, workflow, source-policy, dsms
- advisory-board documentation section
- TOM dashboard components, TOM generator SDM mapping
- DSFA: mitigation library, risk catalog, threshold analysis, source attribution
- VVT: baseline catalog, profiling engine, types
- Loeschfristen: baseline catalog, compliance engine, export, profiling, types
- Compliance scope: engine, profiling, golden tests, types

Existing SDK pages updated:
- dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality
- SDKSidebar, StepHeader — new navigation items and layout
- SDK layout, context, types — expanded type system

Other admin-v2 changes:
- AI agents page, RAG pipeline DSFA integration
- GridOverlay component updates
- Companion feature (development + education)
- Compliance advisor SOUL definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 00:01:04 +01:00

204 lines
6.9 KiB
TypeScript

'use client'
import { Check } from 'lucide-react'
import { Phase } from '@/lib/companion/types'
import { PHASE_COLORS, formatMinutes } from '@/lib/companion/constants'
interface PhaseTimelineProps {
phases: Phase[]
currentPhaseIndex: number
onPhaseClick?: (index: number) => void
compact?: boolean
}
export function PhaseTimeline({
phases,
currentPhaseIndex,
onPhaseClick,
compact = false,
}: PhaseTimelineProps) {
return (
<div className={`flex items-center ${compact ? 'gap-2' : 'gap-3'}`}>
{phases.map((phase, index) => {
const isActive = index === currentPhaseIndex
const isCompleted = phase.status === 'completed'
const isPast = index < currentPhaseIndex
const colors = PHASE_COLORS[phase.id]
return (
<div key={phase.id} className="flex items-center">
{/* Phase Dot/Circle */}
<button
onClick={() => onPhaseClick?.(index)}
disabled={!onPhaseClick}
className={`
relative flex items-center justify-center
${compact ? 'w-8 h-8' : 'w-10 h-10'}
rounded-full font-semibold text-sm
transition-all duration-300
${onPhaseClick ? 'cursor-pointer hover:scale-110' : 'cursor-default'}
${isActive
? `ring-4 ring-offset-2 ${colors.tailwind} text-white`
: isCompleted || isPast
? `${colors.tailwind} text-white opacity-80`
: 'bg-slate-200 text-slate-500'
}
`}
style={{
backgroundColor: isActive || isCompleted || isPast ? colors.hex : undefined,
// Use CSS custom property for ring color with Tailwind
'--tw-ring-color': isActive ? colors.hex : undefined,
} as React.CSSProperties}
title={`${phase.displayName} (${formatMinutes(phase.duration)})`}
>
{isCompleted ? (
<Check className={compact ? 'w-4 h-4' : 'w-5 h-5'} />
) : (
phase.shortName
)}
{/* Active indicator pulse */}
{isActive && (
<span
className="absolute inset-0 rounded-full animate-ping opacity-30"
style={{ backgroundColor: colors.hex }}
/>
)}
</button>
{/* Connector Line */}
{index < phases.length - 1 && (
<div
className={`
${compact ? 'w-4' : 'w-8'} h-1 mx-1
${isPast || isCompleted
? 'bg-gradient-to-r'
: 'bg-slate-200'
}
`}
style={{
background: isPast || isCompleted
? `linear-gradient(to right, ${colors.hex}, ${PHASE_COLORS[phases[index + 1].id].hex})`
: undefined,
}}
/>
)}
</div>
)
})}
</div>
)
}
/**
* Detailed Phase Timeline with labels and durations
*/
export function PhaseTimelineDetailed({
phases,
currentPhaseIndex,
onPhaseClick,
}: PhaseTimelineProps) {
return (
<div className="bg-white border border-slate-200 rounded-xl p-6">
<h3 className="text-sm font-medium text-slate-500 mb-4">Unterrichtsphasen</h3>
<div className="flex items-start justify-between">
{phases.map((phase, index) => {
const isActive = index === currentPhaseIndex
const isCompleted = phase.status === 'completed'
const isPast = index < currentPhaseIndex
const colors = PHASE_COLORS[phase.id]
return (
<div key={phase.id} className="flex flex-col items-center flex-1">
{/* Top connector line */}
<div className="w-full flex items-center mb-2">
{index > 0 && (
<div
className="flex-1 h-1"
style={{
background: isPast || isCompleted
? PHASE_COLORS[phases[index - 1].id].hex
: '#e2e8f0',
}}
/>
)}
{index === 0 && <div className="flex-1" />}
{/* Phase Circle */}
<button
onClick={() => onPhaseClick?.(index)}
disabled={!onPhaseClick}
className={`
relative w-12 h-12 rounded-full
flex items-center justify-center
font-bold text-lg
transition-all duration-300
${onPhaseClick ? 'cursor-pointer hover:scale-110' : 'cursor-default'}
${isActive ? 'ring-4 ring-offset-2 shadow-lg' : ''}
`}
style={{
backgroundColor: isActive || isCompleted || isPast ? colors.hex : '#e2e8f0',
color: isActive || isCompleted || isPast ? 'white' : '#64748b',
'--tw-ring-color': isActive ? `${colors.hex}40` : undefined,
} as React.CSSProperties}
>
{isCompleted ? (
<Check className="w-6 h-6" />
) : (
phase.shortName
)}
{isActive && (
<span
className="absolute inset-0 rounded-full animate-ping opacity-20"
style={{ backgroundColor: colors.hex }}
/>
)}
</button>
{index < phases.length - 1 && (
<div
className="flex-1 h-1"
style={{
background: isCompleted ? colors.hex : '#e2e8f0',
}}
/>
)}
{index === phases.length - 1 && <div className="flex-1" />}
</div>
{/* Phase Label */}
<span
className={`
text-sm font-medium mt-2
${isActive ? 'text-slate-900' : 'text-slate-500'}
`}
>
{phase.displayName}
</span>
{/* Duration */}
<span
className={`
text-xs mt-1
${isActive ? 'text-slate-700' : 'text-slate-400'}
`}
>
{formatMinutes(phase.duration)}
</span>
{/* Actual time if completed */}
{phase.actualTime !== undefined && phase.actualTime > 0 && (
<span className="text-xs text-slate-400 mt-0.5">
(tatsaechlich: {Math.round(phase.actualTime / 60)} Min)
</span>
)}
</div>
)
})}
</div>
</div>
)
}