Split 1260-LOC client page into _types.ts and six tab components under _components/ (Overview, Policies, SoA, Objectives, Audits, Reviews) plus a shared helpers module. Behavior preserved exactly; page.tsx is now a thin wiring shell. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
|
|
// =============================================================================
|
|
// HELPER COMPONENTS
|
|
// =============================================================================
|
|
|
|
export function StatusBadge({ status, size = 'sm' }: { status: string; size?: 'sm' | 'md' }) {
|
|
const colors: Record<string, string> = {
|
|
ready: 'bg-green-100 text-green-700',
|
|
compliant: 'bg-green-100 text-green-700',
|
|
approved: 'bg-green-100 text-green-700',
|
|
pass: 'bg-green-100 text-green-700',
|
|
implemented: 'bg-green-100 text-green-700',
|
|
completed: 'bg-green-100 text-green-700',
|
|
verified: 'bg-green-100 text-green-700',
|
|
achieved: 'bg-green-100 text-green-700',
|
|
closed: 'bg-green-100 text-green-700',
|
|
at_risk: 'bg-yellow-100 text-yellow-700',
|
|
partial: 'bg-yellow-100 text-yellow-700',
|
|
warning: 'bg-yellow-100 text-yellow-700',
|
|
planned: 'bg-blue-100 text-blue-700',
|
|
draft: 'bg-gray-100 text-gray-700',
|
|
active: 'bg-blue-100 text-blue-700',
|
|
in_progress: 'bg-blue-100 text-blue-700',
|
|
not_ready: 'bg-red-100 text-red-700',
|
|
non_compliant: 'bg-red-100 text-red-700',
|
|
fail: 'bg-red-100 text-red-700',
|
|
open: 'bg-red-100 text-red-700',
|
|
corrective_action_pending: 'bg-orange-100 text-orange-700',
|
|
verification_pending: 'bg-yellow-100 text-yellow-700',
|
|
}
|
|
const cls = colors[status] || 'bg-gray-100 text-gray-600'
|
|
const labels: Record<string, string> = {
|
|
ready: 'Bereit', not_ready: 'Nicht bereit', at_risk: 'Risiko',
|
|
compliant: 'Konform', non_compliant: 'Nicht konform', partial: 'Teilweise',
|
|
approved: 'Genehmigt', draft: 'Entwurf', pass: 'Bestanden', fail: 'Fehlgeschlagen',
|
|
warning: 'Warnung', implemented: 'Implementiert', planned: 'Geplant',
|
|
active: 'Aktiv', achieved: 'Erreicht', completed: 'Abgeschlossen',
|
|
open: 'Offen', closed: 'Geschlossen', verified: 'Verifiziert',
|
|
corrective_action_pending: 'CAPA ausstehend', verification_pending: 'Verifizierung',
|
|
in_progress: 'In Bearbeitung',
|
|
not_applicable: 'N/A',
|
|
}
|
|
const pad = size === 'md' ? 'px-3 py-1 text-sm' : 'px-2 py-0.5 text-xs'
|
|
return <span className={`${cls} ${pad} rounded-full font-medium`}>{labels[status] || status}</span>
|
|
}
|
|
|
|
export function StatCard({ label, value, sub, color = 'purple' }: { label: string; value: string | number; sub?: string; color?: string }) {
|
|
const colors: Record<string, string> = {
|
|
purple: 'border-purple-200 bg-purple-50',
|
|
green: 'border-green-200 bg-green-50',
|
|
red: 'border-red-200 bg-red-50',
|
|
yellow: 'border-yellow-200 bg-yellow-50',
|
|
blue: 'border-blue-200 bg-blue-50',
|
|
}
|
|
return (
|
|
<div className={`border rounded-xl p-4 ${colors[color] || colors.purple}`}>
|
|
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
|
<div className="text-sm font-medium text-gray-700 mt-1">{label}</div>
|
|
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function LoadingSpinner() {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function EmptyState({ text, action, onAction }: { text: string; action?: string; onAction?: () => void }) {
|
|
return (
|
|
<div className="text-center py-16 text-gray-500">
|
|
<p>{text}</p>
|
|
{action && onAction && (
|
|
<button onClick={onAction} className="mt-3 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
|
|
{action}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|