Files
tfm_ainventory/frontend/app/logs/page.tsx
Daniel Bedeleanu 9a87064dbe fix(design): replace all 40+ hardcoded colors with DESIGN.md semantic system
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>
2026-04-25 17:19:53 +03:00

188 lines
7.3 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { db, Item } from '@/lib/db';
import { inventoryApi } from '@/lib/api';
import PageShell from '@/components/PageShell';
import { History, Search, Activity, ArrowDownCircle, ArrowUpCircle, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils';
import { fetchAndCacheItems } from '@/lib/sync';
import StatCard from '@/components/StatCard';
import LogsTable from '@/components/LogsTable';
export default function LogsPage() {
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [inventory, setInventory] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [filterAction, setFilterAction] = useState('ALL');
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
// 1. First, try to get fresh items to resolve names
let freshItems: Item[] = [];
try {
freshItems = await fetchAndCacheItems();
} catch (itemErr) {
console.warn("Item sync failed, using local cache for names", itemErr);
freshItems = await db.items.toArray();
}
setInventory(freshItems);
// 2. Then, fetch fresh logs
const logs = await inventoryApi.getAuditLogs(100);
// 3. Pre-resolve names to avoid UI flickering/mismatches
const enrichedLogs = (logs || []).map((log: any) => {
// [AUDIT HARDENING] Prioritize the historical snapshot from the backend
if (log.target_item_name) {
return { ...log, resolved_name: log.target_item_name };
}
// Fallback for legacy logs or system operations
const hasTarget = log.target_item_id && String(log.target_item_id) !== 'null';
const item = hasTarget ? freshItems.find(i => String(i.id) === String(log.target_item_id)) : null;
return {
...log,
resolved_name: item ? item.name : (hasTarget ? `Item #${log.target_item_id}` : "System Operation")
};
});
setAuditLogs(enrichedLogs);
} catch (err: any) {
console.error("Critical log load failure:", err);
} finally {
setLoading(false);
}
};
const filteredLogs = auditLogs.filter(log => {
const matchesSearch = (log.resolved_name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
log.action.toLowerCase().includes(searchQuery.toLowerCase()) ||
(log.username || '').toLowerCase().includes(searchQuery.toLowerCase());
const matchesAction = filterAction === 'ALL' || log.action.includes(filterAction);
return matchesSearch && matchesAction;
});
// Calculate stats
const totalCount = auditLogs.length;
const inCount = auditLogs.filter(l => l.action.includes('IN') || l.action.includes('ADD')).length;
const outCount = auditLogs.filter(l => l.action.includes('OUT') || l.action.includes('TRASH') || l.action.includes('DELETE')).length;
const criticalLogsCount = auditLogs.filter(l => l.action.includes('DELETE') || l.action.includes('TRASH')).length;
const mostActiveUser = auditLogs.length > 0 ?
Object.entries(auditLogs.reduce((acc: any, curr) => {
const user = curr.username || 'System';
acc[user] = (acc[user] || 0) + 1;
return acc;
}, {})).sort((a: any, b: any) => b[1] - a[1])[0]?.[0] : 'N/A';
return (
<PageShell>
<main className="p-4 md:p-8 max-w-7xl mx-auto space-y-3 md:space-y-6 mb-20">
<header className="flex flex-col sm:flex-row sm:items-end justify-between gap-3 md:gap-4 mb-6 md:mb-8">
<div className="flex items-center gap-4">
<div className="p-4 bg-primary/10 text-primary border border-primary/20">
<History size={28} className="md:w-8 md:h-8" />
</div>
<div>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">Operations Audit</h1>
<p className="text-xs md:text-sm text-secondary font-normal mt-0.5">Real-time Intervention Tracking</p>
</div>
</div>
<div className="flex gap-2 ml-auto sm:ml-0">
<button
onClick={loadData}
disabled={loading}
className="flex items-center gap-2 px-5 py-2.5 bg-surface border border-border text-secondary hover:text-white text-xs font-normal transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw size={14} className={cn(loading && "animate-spin")} />
Sync Logs
</button>
</div>
</header>
{/* Stats Grid */}
<section className="grid grid-cols-2 lg:grid-cols-4 gap-2.5 md:gap-4">
<StatCard
label="Total Events"
value={totalCount}
icon={Activity}
/>
<StatCard
label="Inbound"
value={inCount}
icon={ArrowDownCircle}
/>
<StatCard
label="Outbound"
value={outCount}
icon={ArrowUpCircle}
/>
<StatCard
label="Integrity"
value={totalCount - criticalLogsCount}
icon={History}
/>
</section>
<section className="space-y-3 md:space-y-4">
<div className="relative group">
<input
type="text"
placeholder="Filter by user, action or asset..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-surface/50 border border-border py-3 lg:py-4 xl:py-5.5 pr-4 pl-12 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl focus:border-primary outline-none transition-all placeholder:text-secondary"
/>
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary transition-colors group-focus-within:text-primary">
<Search size={18} />
</div>
</div>
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar pb-1 px-1 -mx-1">
<button
onClick={() => setFilterAction('ALL')}
className={cn(
"px-5 py-2 text-xs font-normal transition-all whitespace-nowrap border",
filterAction === 'ALL'
? "bg-primary/10 text-on-background border-white"
: "bg-surface/70 text-secondary border-border hover:border-border"
)}
>
All Streams
</button>
{['ADD', 'REMOVE', 'ADJUST', 'DELETE', 'LOGIN'].map((f) => (
<button
key={f}
onClick={() => setFilterAction(f)}
className={cn(
"px-5 py-2 text-xs font-normal transition-all whitespace-nowrap border",
filterAction === f
? "bg-primary text-white border-primary"
: "bg-surface/70 text-secondary border-border hover:border-border"
)}
>
{f}
</button>
))}
</div>
</section>
<section className="space-y-3">
<LogsTable logs={filteredLogs} loading={loading} />
</section>
</main>
</PageShell>
);
}