import { useCallback, useEffect, useRef, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Avatar, Button, Card, EmptyState, Icon, PageHeader } from '../components/ui' import Timeline from '../components/common/Timeline' import { fetchDashboard } from '../api/dashboard' import { ASSET_STATE } from '../api/assets' import { useAuth } from '../store/AuthContext' import { formatCurrency } from '../utils/format' /** * Colour per asset state — categorical slots 1–3, fixed to the state (never to * its rank), so a state keeps its colour whatever the counts. Validated * all-pairs on the white card surface; aqua is under 3:1 there, so every * segment also carries a visible label in the legend. */ const STATE_COLOR = { [ASSET_STATE.ASSIGNED]: 'var(--series-1)', [ASSET_STATE.AVAILABLE]: 'var(--series-3)', [ASSET_STATE.CLOSED]: 'var(--series-2)', } const OTHER_COLOR = 'var(--border-strong)' const pct = (n, total) => (total ? Math.round((n / total) * 100) : 0) /** Overview of the inventory, read live from the asset and employee workflows. */ export default function Dashboard() { const navigate = useNavigate() const { logout } = useAuth() const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const requestId = useRef(0) const load = useCallback(async () => { const id = ++requestId.current setLoading(true) setError('') try { const next = await fetchDashboard() if (id === requestId.current) setData(next) } catch (err) { if (id !== requestId.current) return if (err?.status === 401) { logout() navigate('/login', { replace: true }) return } setError(err?.message || 'Could not load the dashboard.') } finally { if (id === requestId.current) setLoading(false) } }, [logout, navigate]) useEffect(() => { load() }, [load]) const header = ( {loading ? 'Refreshing…' : 'Refresh'} } /> ) if (!data) { return ( <> {header} {error ? ( Try again} /> ) : ( )} ) } const count = (state) => data.states.find((s) => s.state === state)?.count ?? 0 return ( <> {header} {error && (
{error} Showing the last loaded numbers.
)} {/* A refetch keeps the previous numbers, dimmed, instead of flashing. */}
View all } flush > Full history } flush > {data.recent.length ? ( ({ ...e, title: [e.action, e.asset_code].filter(Boolean).join(' · '), meta: [e.employee, e.meta].filter(Boolean).join(' · ') || undefined, }))} /> ) : ( )}
) } /** One headline number. Proportional figures; the label says what it counts. */ function StatTile({ label, value, note, to }) { const body = ( <> {label} {typeof value === 'number' ? value.toLocaleString('en-IN') : value} {note && {note}} ) return to ? ( {body} ) : (
{body}
) } /** * Part-to-whole of the asset states: one stacked bar with 2px surface gaps, * and a legend that always shows each state's count and share, so no value * lives only in the tooltip. */ function StateMeter({ states, total }) { const [tip, setTip] = useState(null) const shown = states.filter((s) => s.count > 0) if (!total) return const show = (s, e) => { const box = e.currentTarget.parentElement.getBoundingClientRect() const mark = e.currentTarget.getBoundingClientRect() const x = e.clientX ?? mark.left + mark.width / 2 setTip({ s, x: x - box.left }) } return (
`${s.state} ${s.count}`).join(', ')}> {shown.map((s) => ( show(s, e)} onPointerLeave={() => setTip(null)} onFocus={(e) => show(s, e)} onBlur={() => setTip(null)} /> ))} {tip && (
{tip.s.count} {tip.s.state} · {pct(tip.s.count, total)}%
)}
    {states.map((s) => (
  • {s.state} {s.count} · {pct(s.count, total)}%
  • ))}
) } /** * Horizontal bars, one series (slot 1), most first. Each bar's value sits at * its tip, so the chart is also its own table; hover lifts the bar. */ function CategoryBars({ categories }) { if (!categories.length) return const max = Math.max(...categories.map((c) => c.count)) return (
    {categories.map((c) => (
  • {c.label} {c.count}
  • ))}
) } /** Employees with assets right now, most first. */ function HolderList({ holders }) { if (!holders.length) { return } return (
    {holders.slice(0, 6).map((h) => (
  • {h.name} {h.count} asset{h.count === 1 ? '' : 's'}
  • ))}
) }