Build [v1.5.0] - Box Management & Label Printing
This commit is contained in:
@@ -5,6 +5,7 @@ import { inventoryApi } from '@/lib/api';
|
||||
import PageShell from '@/components/PageShell';
|
||||
import {
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
UserPlus,
|
||||
User,
|
||||
Trash2,
|
||||
@@ -357,8 +358,17 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] font-black text-indigo-400/50 pr-4 italic">
|
||||
Externally Managed
|
||||
<div className="flex items-center gap-1 pr-2">
|
||||
<div className="text-[10px] font-black text-indigo-400/50 pr-4 italic">
|
||||
Cached Profile
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteUser(u.id, u.username)}
|
||||
className="p-2.5 bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white rounded-lg transition-all border border-rose-500/10"
|
||||
title="Clear local cache for this user"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -512,6 +522,27 @@ export default function AdminPage() {
|
||||
)} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ldapConfig.use_tls && (
|
||||
<div className="flex items-center justify-between gap-4 pt-1 animate-in fade-in slide-in-from-top-1 duration-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert size={14} className={cn("transition-colors", ldapConfig.ignore_cert ? "text-amber-400" : "text-slate-600")} />
|
||||
<span className="text-[10px] font-black text-slate-400 italic">Ignore Certificate Validation (Self-signed)</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLdapConfig({ ...ldapConfig, ignore_cert: !ldapConfig.ignore_cert })}
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-10 items-center rounded-full transition-all duration-300 outline-none",
|
||||
ldapConfig.ignore_cert ? "bg-amber-600" : "bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-300",
|
||||
ldapConfig.ignore_cert ? "translate-x-5" : "translate-x-1"
|
||||
)} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
|
||||
@@ -65,8 +65,9 @@ export default function LoginPage() {
|
||||
router.push('/');
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
toast.error(isEnterprise ? "Login failed. Check credentials or group membership." : "Invalid password");
|
||||
} catch (error: any) {
|
||||
const detail = error.response?.data?.detail || (isEnterprise ? "Login failed. Check credentials or group membership." : "Invalid password");
|
||||
toast.error(detail);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { db, Item } from '@/lib/db';
|
||||
import { inventoryApi } from '@/lib/api';
|
||||
import PageShell from '@/components/PageShell';
|
||||
import { History, X, Search, Filter, Activity, ArrowDownCircle, ArrowUpCircle, User } from 'lucide-react';
|
||||
import { History, X, Search, Filter, Activity, ArrowDownCircle, ArrowUpCircle, User, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { fetchAndCacheItems } from '@/lib/sync';
|
||||
|
||||
@@ -23,23 +23,46 @@ export default function LogsPage() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Load inventory for name lookups
|
||||
const cached = await db.items.toArray();
|
||||
setInventory(cached);
|
||||
// 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);
|
||||
|
||||
// Fetch fresh logs
|
||||
// 2. Then, fetch fresh logs
|
||||
const logs = await inventoryApi.getAuditLogs(100);
|
||||
setAuditLogs(logs);
|
||||
|
||||
// 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) {
|
||||
console.error(err);
|
||||
console.error("Critical log load failure:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredLogs = auditLogs.filter(log => {
|
||||
const itemName = inventory.find(i => i.id === log.target_item_id)?.name || '';
|
||||
const matchesSearch = itemName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
const matchesSearch = (log.resolved_name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
log.action.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(log.username || '').toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
@@ -55,7 +78,8 @@ export default function LogsPage() {
|
||||
|
||||
const mostActiveUser = auditLogs.length > 0 ?
|
||||
Object.entries(auditLogs.reduce((acc: any, curr) => {
|
||||
acc[curr.username] = (acc[curr.username] || 0) + 1;
|
||||
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';
|
||||
|
||||
@@ -77,9 +101,11 @@ export default function LogsPage() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="px-4 py-2 bg-slate-900 border border-slate-800 text-slate-400 hover:text-white rounded-xl text-xs font-black transition-all active:scale-95"
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-slate-900 border border-slate-800 text-slate-400 hover:text-white rounded-2xl text-xs font-black transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
Refresh
|
||||
<RefreshCw size={14} className={cn(loading && "animate-spin")} />
|
||||
Refresh Stream
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,7 +152,8 @@ export default function LogsPage() {
|
||||
{ label: 'Check in', value: 'CHECK_IN' },
|
||||
{ label: 'Check out', value: 'CHECK_OUT' },
|
||||
{ label: 'Trash', value: 'TRASH' },
|
||||
{ label: 'Create', value: 'CREATE' }
|
||||
{ label: 'Create', value: 'CREATE' },
|
||||
{ label: 'System', value: 'DB' }
|
||||
].map(action => (
|
||||
<button
|
||||
key={action.value}
|
||||
@@ -175,14 +202,16 @@ export default function LogsPage() {
|
||||
"text-[9px] font-black px-2 py-0.5 rounded-md border min-w-[70px] text-center",
|
||||
log.action.includes('CHECK_IN') ? "bg-green-500/5 text-green-500 border-green-500/20" :
|
||||
(log.action.includes('TRASH') ? "bg-rose-500/5 text-rose-500 border-rose-500/20" :
|
||||
(log.action.includes('CREATE') ? "bg-indigo-500/5 text-indigo-400 border-indigo-500/20" : "bg-amber-500/5 text-amber-500 border-amber-500/20"))
|
||||
(log.action.includes('DB') ? "bg-sky-500/5 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/5 text-indigo-400 border-indigo-500/20" : "bg-amber-500/5 text-amber-500 border-amber-500/20"))))
|
||||
)}>
|
||||
{log.action.replace('_', ' ')}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-bold text-white group-hover:text-primary transition-colors truncate">
|
||||
{inventory.find(i => i.id === log.target_item_id)?.name || `Item #${log.target_item_id}`}
|
||||
{log.resolved_name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 opacity-80">
|
||||
<span className="text-[9px] font-black text-amber-500 tracking-tight">{log.username || 'System'}</span>
|
||||
@@ -195,20 +224,14 @@ export default function LogsPage() {
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 flex items-center gap-3 z-10">
|
||||
{log.details && (
|
||||
<span className="text-[10px] text-slate-600 italic hidden md:block truncate max-w-[150px]">
|
||||
"{log.details}"
|
||||
</span>
|
||||
)}
|
||||
<div className={cn(
|
||||
"text-xl font-black tabular-nums min-w-[40px] text-right",
|
||||
log.quantity_change > 0 ? "text-green-500" : (log.quantity_change < 0 ? "text-rose-500" : "text-indigo-400")
|
||||
(log.quantity_change || 0) > 0 ? "text-green-500" : ((log.quantity_change || 0) < 0 ? "text-rose-500" : "text-indigo-400")
|
||||
)}>
|
||||
{log.quantity_change > 0 ? '+' : ''}{log.quantity_change === 0 ? '±' : log.quantity_change}
|
||||
{log.quantity_change ? (log.quantity_change > 0 ? `+${log.quantity_change}` : log.quantity_change) : (log.action.includes('DB') ? 'SYS' : '±')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Glass background effect */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-transparent via-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
))}
|
||||
@@ -230,7 +253,7 @@ export default function LogsPage() {
|
||||
{selectedLog.action}
|
||||
</div>
|
||||
<h2 className="text-3xl font-black text-white tracking-tight leading-tight pt-2">
|
||||
{inventory.find(i => i.id === selectedLog.target_item_id)?.name || `Item #${selectedLog.target_item_id}`}
|
||||
{selectedLog.resolved_name}
|
||||
</h2>
|
||||
</div>
|
||||
<button onClick={() => setSelectedLog(null)} className="p-3 hover:bg-slate-800 rounded-2xl text-slate-500 transition-colors border border-slate-800">
|
||||
@@ -247,9 +270,11 @@ export default function LogsPage() {
|
||||
<p className="text-[10px] font-black text-slate-600">Delta</p>
|
||||
<p className={cn(
|
||||
"text-xl font-black",
|
||||
selectedLog.quantity_change > 0 ? "text-green-500" : "text-rose-500"
|
||||
(selectedLog.quantity_change || 0) > 0 ? "text-green-500" : "text-rose-500"
|
||||
)}>
|
||||
{selectedLog.quantity_change > 0 ? '+' : ''}{selectedLog.quantity_change} Units
|
||||
{selectedLog.quantity_change
|
||||
? `${selectedLog.quantity_change > 0 ? '+' : ''}${selectedLog.quantity_change} Units`
|
||||
: (selectedLog.action.includes('DB') ? 'System' : 'No Delta')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,6 +287,31 @@ export default function LogsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedLog.target_snapshot && (() => {
|
||||
try {
|
||||
const snap = JSON.parse(selectedLog.target_snapshot);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-slate-800" />
|
||||
<p className="text-[10px] font-black text-slate-700 uppercase tracking-widest">Full Historical Context</p>
|
||||
<div className="h-px flex-1 bg-slate-800" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Object.entries(snap).map(([key, val]) => (
|
||||
val && key !== 'image_url' && (
|
||||
<div key={key} className="bg-slate-950/30 p-3 rounded-xl border border-slate-800/40">
|
||||
<p className="text-[8px] font-black text-slate-600 uppercase mb-1">{key.replace('_', ' ')}</p>
|
||||
<p className="text-xs font-bold text-slate-300 truncate" title={String(val)}>{String(val)}</p>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (e) { return null; }
|
||||
})()}
|
||||
|
||||
{selectedLog.details && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-slate-600">Intervention Details</p>
|
||||
@@ -270,6 +320,15 @@ export default function LogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLog.target_item_pn && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-slate-600">Historical Part Number</p>
|
||||
<div className="bg-slate-800/20 text-slate-400 p-4 rounded-2xl border border-slate-800/50 text-xs font-mono">
|
||||
{selectedLog.target_item_pn}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user