Phase 10 Implementation: Bulk color system compliance across frontend Applied DESIGN_COLOR_RULES.md to 40 component and page files: - Indigo (3) → primary/secondary (primary actions) - Green (12) → tertiary (#00e639) (success/healthy status) - Red/Rose (20) → error (#ffb4ab) (destructive actions) - Amber/Sky (9) → primary/secondary (warnings/info) - Blue/Slate (various) → primary/design system colors (focus states) - Black → bg-surface-container-lowest (proper design color) Files updated: 40 components + pages Color replacements: 44+ instances Build status: ✓ Zero errors, compiled in 6.3s Test status: Pending (npm run test) All colors now follow DESIGN.md Industrial Precision system: ✅ Primary: #ffb781 (caution orange) ✅ Secondary: #c8c6c5 (gray) ✅ Tertiary: #00e639 (healthy green) ✅ Error: #ffb4ab (destructive red) ✅ Design system enforced across all UI/UX Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
211 lines
9.5 KiB
TypeScript
211 lines
9.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { X, ArrowDownCircle, ArrowUpCircle } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface AuditLog {
|
|
id: string;
|
|
action: string;
|
|
username?: string;
|
|
timestamp: string;
|
|
resolved_name: string;
|
|
quantity_change?: number;
|
|
target_snapshot?: string;
|
|
details?: string;
|
|
}
|
|
|
|
interface LogsTableProps {
|
|
logs: AuditLog[];
|
|
loading: boolean;
|
|
}
|
|
|
|
export default function LogsTable({ logs, loading }: LogsTableProps) {
|
|
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-20 text-secondary gap-4 animate-pulse">
|
|
<div className="w-10 h-10 border-4 border-primary/20 border-t-primary rounded-none animate-spin" />
|
|
<p className="text-xs font-normal">Securing Audit Stream...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (logs.length === 0) {
|
|
return (
|
|
<div className="bg-surface-container border border-border border-dashed py-20 flex flex-col items-center justify-center text-center gap-4">
|
|
<div className="w-16 h-16 bg-surface-bright flex items-center justify-center text-secondary border border-border">
|
|
<ArrowDownCircle size={32} />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-normal text-secondary">No events found</p>
|
|
<p className="text-xs text-secondary font-normal mt-1">Refine your strategic filters</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="border border-border level-1 overflow-hidden">
|
|
{/* Header Row */}
|
|
<div className="hidden md:grid grid-cols-[120px_1fr_120px_180px_80px] table-header">
|
|
<div className="p-2">Action</div>
|
|
<div className="p-2">Item</div>
|
|
<div className="p-2">Operator</div>
|
|
<div className="p-2">Timestamp</div>
|
|
<div className="p-2 text-right">Delta</div>
|
|
</div>
|
|
|
|
<div className="divide-y divide-border">
|
|
{logs.map((log) => (
|
|
<button
|
|
key={log.id}
|
|
onClick={() => setSelectedLog(log)}
|
|
className="w-full text-left p-2 md:p-3 hover:bg-surface-bright transition-colors group active:bg-surface-bright/80 relative"
|
|
>
|
|
<div className="md:grid md:grid-cols-[120px_1fr_120px_180px_80px] items-center gap-4">
|
|
{/* Action Badge - Mobile/Tablet shown as badge, Desktop as column */}
|
|
<div className="mb-2 md:mb-0">
|
|
<div className={cn(
|
|
"text-[10px] font-normal px-2 py-0.5 border text-center inline-block min-w-[100px]",
|
|
log.action.includes('CHECK_IN') ? "bg-tertiary/10 text-tertiary border-tertiary/20" :
|
|
(log.action.includes('TRASH') ? "bg-error/10 text-error border-error/20" :
|
|
(log.action.includes('DB') ? "bg-secondary/10 text-secondary border-secondary/20" :
|
|
(log.action.includes('DELETE') ? "bg-error/10 text-error border-error/30" :
|
|
(log.action.includes('CREATE') ? "bg-primary/10 text-primary border-primary/20" : "bg-primary/10 text-primary border-primary/20"))))
|
|
)}>
|
|
{log.action.replace('_', ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="min-w-0">
|
|
<h3 className="text-sm md:text-base text-foreground group-hover:text-primary transition-colors truncate">
|
|
{log.resolved_name}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="hidden md:block text-sm text-secondary truncate">
|
|
{log.username || 'System'}
|
|
</div>
|
|
|
|
<div className="hidden md:block text-sm text-secondary tabular-nums">
|
|
{new Date(log.timestamp).toLocaleDateString()} {new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</div>
|
|
|
|
<div className={cn(
|
|
"text-base md:text-sm font-normal tabular-nums text-right",
|
|
(log.quantity_change || 0) > 0 ? "text-tertiary" : ((log.quantity_change || 0) < 0 ? "text-error" : "text-primary/50")
|
|
)}>
|
|
{log.quantity_change ? (log.quantity_change > 0 ? `+${log.quantity_change}` : log.quantity_change) : (log.action.includes('DB') ? 'SYS' : '±')}
|
|
</div>
|
|
|
|
{/* Mobile-only secondary info */}
|
|
<div className="md:hidden mt-1 flex items-center gap-2 text-xs text-secondary opacity-80">
|
|
<span>{log.username || 'System'}</span>
|
|
<span>·</span>
|
|
<span>{new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selected Log Modal */}
|
|
{selectedLog && (
|
|
<div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/90 animate-in fade-in duration-300">
|
|
<div className="level-2 p-4 md:p-6 max-w-lg w-full space-y-4 animate-in slide-in-from-bottom-10 duration-300 overflow-hidden">
|
|
<div className="flex justify-between items-start">
|
|
<div className="space-y-1 pr-4">
|
|
<div className={cn(
|
|
"text-xs font-normal px-3 py-1 border inline-block",
|
|
selectedLog.action.includes('CHECK_IN') ? "status-success" :
|
|
(selectedLog.action.includes('TRASH') ? "bg-error text-on-error" : "status-alert")
|
|
)}>
|
|
{selectedLog.action.replace('_', ' ').toLowerCase()}
|
|
</div>
|
|
<h2 className="text-2xl font-normal text-foreground pt-2 leading-tight">
|
|
{selectedLog.resolved_name}
|
|
</h2>
|
|
</div>
|
|
<button onClick={() => setSelectedLog(null)} className="p-2 bg-surface-bright hover:bg-border text-secondary transition-colors border border-border shrink-0">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-px bg-border border border-border">
|
|
<div className="space-y-1 level-1 p-3">
|
|
<p className="text-[10px] font-normal text-secondary">Protocol Operator</p>
|
|
<p className="text-sm font-normal text-foreground">{selectedLog.username || 'Automated Process'}</p>
|
|
</div>
|
|
<div className="space-y-1 level-1 p-3">
|
|
<p className="text-[10px] font-normal text-secondary">Quantity Delta</p>
|
|
<p className={cn(
|
|
"text-xl font-normal tabular-nums",
|
|
(selectedLog.quantity_change || 0) > 0 ? "text-tertiary" : "text-error"
|
|
)}>
|
|
{selectedLog.quantity_change
|
|
? `${selectedLog.quantity_change > 0 ? '+' : ''}${selectedLog.quantity_change}`
|
|
: (selectedLog.action.includes('DB') ? 'SYS' : '±')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<p className="text-[10px] font-normal text-secondary ml-1">Universal Timestamp</p>
|
|
<p className="text-xs font-normal text-foreground bg-surface-bright/30 p-3 border border-border tabular-nums">
|
|
{new Date(selectedLog.timestamp).toLocaleString(undefined, { dateStyle: 'full', timeStyle: 'medium' })}
|
|
</p>
|
|
</div>
|
|
|
|
{selectedLog.target_snapshot && (() => {
|
|
try {
|
|
const snap = JSON.parse(selectedLog.target_snapshot) as Record<string, any>;
|
|
return (
|
|
<div className="space-y-2 pt-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-px flex-1 bg-border" />
|
|
<p className="text-[10px] font-normal text-secondary">Snapshot Record</p>
|
|
<div className="h-px flex-1 bg-border" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-px bg-border border border-border">
|
|
{Object.entries(snap).map(([key, val]) => (
|
|
(val && key !== 'image_url' && key !== 'id') ? (
|
|
<div key={key} className="level-1 p-2">
|
|
<p className="text-[10px] font-normal text-secondary mb-1 opacity-70">{key.replace('_', ' ').toLowerCase()}</p>
|
|
<p className="text-xs font-normal text-foreground truncate" title={String(val)}>{String(val)}</p>
|
|
</div>
|
|
) : null
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
} catch (e) { return null; }
|
|
})()}
|
|
|
|
{selectedLog.details && (
|
|
<div className="space-y-1">
|
|
<p className="text-[10px] font-normal text-secondary ml-1">Intervention Details</p>
|
|
<div className="log-viewer h-auto">
|
|
{selectedLog.details}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setSelectedLog(null)}
|
|
className="btn-primary w-full py-3"
|
|
>
|
|
Close Audit Insight
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|