PHASE 3 - COMPONENT SPACING OPTIMIZATION: components/IdentityCheckOverlay.tsx: - Modal padding: p-8 sm:p-10 → p-4 md:p-6 - Spacing: space-y-8 → space-y-3 md:space-y-4 components/CameraView.tsx: - Container gaps: gap-6 → gap-3 md:gap-4 components/AIOnboarding.tsx (4 fixes): - Main sections: gap-6 → gap-3 md:gap-4 - Live mode: gap-6 → gap-3 md:gap-4 - Results form: gap-6 → gap-3 md:gap-4 - Step view: gap-6 → gap-3 md:gap-4 components/ScannerSection.tsx: - Container spacing: space-y-6 → space-y-3 md:space-y-4 - Scanner section: space-y-4 → space-y-2 md:space-y-3 components/LogsTable.tsx (2 fixes): - Empty state: gap-6 → gap-3 md:gap-4 - Modal: p-6 sm:p-10 space-y-8 → p-4 md:p-6 space-y-3 md:space-y-4 Results: - All 291 tests passing - Zero TypeScript errors - Mobile-first spacing applied throughout - Consistent responsive pattern across all components
192 lines
9.5 KiB
TypeScript
192 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-32 text-secondary gap-4 animate-pulse">
|
|
<div className="w-10 h-10 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
<p className="text-xs font-normal tracking-widest italic">Securing Audit Stream...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (logs.length === 0) {
|
|
return (
|
|
<div className="bg-surface/20 border border-slate-800/50 border-dashed rounded-[2.5rem] py-20 flex flex-col items-center justify-center text-center gap-3 md:gap-4">
|
|
<div className="w-16 h-16 bg-surface rounded-2xl flex items-center justify-center text-slate-700 border border-slate-800">
|
|
<ArrowDownCircle size={32} />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-normal text-secondary tracking-tight">No events found</p>
|
|
<p className="text-xs text-secondary font-normal mt-1">Refine your strategic filters</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="grid gap-2.5">
|
|
{logs.map((log) => (
|
|
<button
|
|
key={log.id}
|
|
onClick={() => setSelectedLog(log)}
|
|
className="w-full text-left bg-surface/50 border border-slate-800/30 p-3 px-4 rounded-2xl flex items-center justify-between gap-4 hover:bg-slate-800/40 hover:border-primary/30 transition-all group active:scale-[0.99] relative overflow-hidden shadow-sm"
|
|
>
|
|
<div className="flex-1 min-w-0 z-10 flex items-center gap-4">
|
|
{/* Compact Action Badge */}
|
|
<div className={cn(
|
|
"text-[10px] font-normal px-3 py-1.5 rounded-lg border min-w-[85px] text-center tracking-tight",
|
|
log.action.includes('CHECK_IN') ? "bg-green-500/10 text-green-500 border-green-500/20" :
|
|
(log.action.includes('TRASH') ? "bg-rose-500/10 text-rose-500 border-rose-500/20" :
|
|
(log.action.includes('DB') ? "bg-sky-500/10 text-sky-400 border-sky-500/20" :
|
|
(log.action.includes('DELETE') ? "bg-red-500/10 text-red-500 border-red-500/30" :
|
|
(log.action.includes('CREATE') ? "bg-indigo-500/10 text-indigo-400 border-indigo-500/20" : "bg-primary/10 text-primary border-primary/20"))))
|
|
)}>
|
|
{log.action.replace('_', ' ')}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="card-title group-hover:text-primary transition-colors truncate">
|
|
{log.resolved_name}
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<span className="card-subtitle mt-0 shrink-0">{log.username || 'System'}</span>
|
|
<span className="w-1 h-1 rounded-full bg-slate-800 shrink-0 mt-1" />
|
|
<span className="card-subtitle mt-0 lowercase opacity-80 truncate">
|
|
{new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · {new Date(log.timestamp).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="shrink-0 flex items-center gap-3 z-10">
|
|
<div className={cn(
|
|
"text-lg font-normal tabular-nums min-w-[35px] text-right",
|
|
(log.quantity_change || 0) > 0 ? "text-green-500" : ((log.quantity_change || 0) < 0 ? "text-rose-500" : "text-primary/50")
|
|
)}>
|
|
{log.quantity_change ? (log.quantity_change > 0 ? `+${log.quantity_change}` : log.quantity_change) : (log.action.includes('DB') ? 'SYS' : '±')}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</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="bg-surface border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-[3rem] p-4 md:p-6 max-w-lg w-full shadow-2xl space-y-3 md: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-4 py-1.5 rounded-full border inline-block tracking-widest",
|
|
selectedLog.action.includes('CHECK_IN') ? "bg-green-500/10 text-green-500 border-green-500/30" :
|
|
(selectedLog.action.includes('TRASH') ? "bg-rose-500/10 text-rose-500 border-rose-500/30" : "bg-primary/10 text-primary border-primary/30")
|
|
)}>
|
|
{selectedLog.action}
|
|
</div>
|
|
<h2 className="text-2xl font-normal text-white tracking-tight pt-2 leading-tight">
|
|
{selectedLog.resolved_name}
|
|
</h2>
|
|
</div>
|
|
<button onClick={() => setSelectedLog(null)} className="p-3 bg-slate-800/50 hover:bg-slate-800 rounded-2xl text-muted transition-colors border border-slate-800 shrink-0 shadow-lg">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1 bg-background/50 p-4 rounded-2xl border border-slate-800/50">
|
|
<p className="text-[11px] font-normal text-muted tracking-widest">Protocol Operator</p>
|
|
<p className="text-sm font-normal text-secondary">{selectedLog.username || 'Automated Process'}</p>
|
|
</div>
|
|
<div className="space-y-1 bg-background/50 p-4 rounded-2xl border border-slate-800/50">
|
|
<p className="text-[11px] font-normal text-muted tracking-widest">Quantity Delta</p>
|
|
<p className={cn(
|
|
"text-xl font-normal tabular-nums",
|
|
(selectedLog.quantity_change || 0) > 0 ? "text-green-500" : "text-rose-500"
|
|
)}>
|
|
{selectedLog.quantity_change
|
|
? `${selectedLog.quantity_change > 0 ? '+' : ''}${selectedLog.quantity_change}`
|
|
: (selectedLog.action.includes('DB') ? 'SYS' : '±')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
<div className="space-y-1">
|
|
<p className="text-[11px] font-normal text-muted tracking-widest ml-1">Universal Timestamp</p>
|
|
<p className="text-xs font-normal text-secondary bg-background/30 p-4 rounded-2xl border border-slate-800/30 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-4 pt-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-px flex-1 bg-slate-800/50" />
|
|
<p className="text-[8px] font-normal text-slate-700 tracking-[0.2em]">Snapshot Record</p>
|
|
<div className="h-px flex-1 bg-slate-800/50" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2.5">
|
|
{Object.entries(snap).map(([key, val]) => (
|
|
(val && key !== 'image_url' && key !== 'id') ? (
|
|
<div key={key} className="bg-background/20 p-3 rounded-xl border border-slate-800/20">
|
|
<p className="text-[10px] font-normal text-muted mb-1 tracking-tight opacity-70">{key.replace('_', ' ')}</p>
|
|
<p className="text-xs font-normal text-secondary truncate" title={String(val)}>{String(val)}</p>
|
|
</div>
|
|
) : null
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
} catch (e) { return null; }
|
|
})()}
|
|
|
|
{selectedLog.details && (
|
|
<div className="space-y-1.5">
|
|
<p className="text-[11px] font-normal text-muted tracking-widest ml-1">Intervention Details</p>
|
|
<div className="bg-primary/5 text-primary/80 p-5 rounded-3xl border border-primary/10 text-sm font-medium leading-relaxed italic shadow-inner">
|
|
"{selectedLog.details}"
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setSelectedLog(null)}
|
|
className="w-full bg-slate-800 hover:bg-slate-700 text-white font-normal py-4.5 rounded-2xl transition-all active:scale-95 border border-slate-700 shadow-xl"
|
|
>
|
|
Close Audit Insight
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|