import { useMemo, useState } from 'react'; import { Activity, AlertTriangle, IndianRupee, Monitor, Search } from 'lucide-react'; import type { DateRange } from '@/api/insights'; import { useBranchScope } from '../BranchScope'; import { DateRangePicker, presetRange, type RangePreset } from '../DateRangePicker'; import { useCounterLabels } from '../counterLabels'; import { byUrgency, countsOf, filterRows, rowsOf, type CounterRow, type CounterState } from '../counterRows'; import { branchLabel, count, money } from '../format'; import { shortAge } from '../posStatus'; import { useTerminalBoard } from '../useTerminalBoard'; import { CounterDrawer } from '../CounterDrawer'; import '@/features/console/console.css'; import './counters.css'; /** * Counters — every till, what it is doing, and which need somebody. * * ── Why this replaced the triage board ────────────────────────────────────── * * The old page grouped counters into "act now", "look at this" and "fine", each * a stack of cards. That reads well with three counters and badly with fifteen: * the groups grew into three long columns, there was no way to search for the * code printed on a receipt, and a shop with everything working got a page of * cards saying so. * * A list answers the question people actually arrive with — "what is T4289 * doing" — and the triage survives as the chips above it and the cards below. * * ── One page, both workspaces ─────────────────────────────────────────────── * * Scope comes from `useBranchScope`, which pins a store user to their own * outlet. The branch column and the branch filter simply have nothing to offer * when there is one branch, so they are not drawn. */ const PAGE_SIZE = 5; const STATE_LABEL: Record = { active: 'Active', attention: 'Attention', never: 'Never used', offline: 'Offline', }; const STATE_TONE: Record = { active: 'healthy', attention: 'attention', never: 'neutral', offline: 'critical', }; export function CountersPage() { const { branches, scoped, selected, current, isPinned } = useBranchScope(); const [preset, setPreset] = useState('today'); const [range, setRange] = useState(() => presetRange('today')); const [chip, setChip] = useState('all'); const [branchId, setBranchId] = useState(null); const [term, setTerm] = useState(''); const [page, setPage] = useState(1); const [open, setOpen] = useState(null); const labels = useCounterLabels(); const board = useTerminalBoard({ branches: scoped, range, isHidden: labels.isHidden }); const rows = useMemo(() => rowsOf(board), [board]); const counts = useMemo(() => countsOf(rows), [rows]); const shown = useMemo( () => filterRows(rows, { state: chip, branchId, term, nameFor: labels.nameFor }).sort(byUrgency), [rows, chip, branchId, term, labels], ); // Clamped rather than reset: narrowing a filter while on page three should // land on the last page of the new result, not silently on page one. const pages = Math.max(1, Math.ceil(shown.length / PAGE_SIZE)); const current_ = Math.min(page, pages); const visible = shown.slice((current_ - 1) * PAGE_SIZE, current_ * PAGE_SIZE); const takings = rows.reduce((sum, row) => sum + row.periodAmount, 0); const needing = counts.attention + counts.offline; const showBranch = !isPinned && branches.length > 1; return (

Counters

{selected === null ? `${branches.length} ${branches.length === 1 ? 'branch' : 'branches'}` : branchLabel(current?.locationname) || 'This branch'} Live

Monitor your counters, activity and connection status.

{ setPreset(nextPreset); setRange(nextRange); }} />
} label="Total counters" value={count(counts.all)} note={showBranch ? `across ${branches.length} branches` : 'at this branch'} /> } label="Active" value={count(counts.active)} note={counts.active === counts.all ? 'all operating normally' : 'operating normally'} tone="healthy" /> } label="Needs attention" value={count(needing)} note={needing === 0 ? 'nothing to do' : 'requires action'} tone={needing > 0 ? 'attention' : 'healthy'} /> } label="Counter sales" value={money(takings)} note="in the selected period" />
setChip('all')} /> setChip('active')} /> setChip('attention')} /> setChip('never')} /> setChip('offline')} />
{showBranch ? ( ) : null}
{board.isLoading && rows.length === 0 ? (
Checking your counters…
) : visible.length === 0 ? (
{rows.length === 0 ? 'No counters connected' : 'Nothing matches'} {rows.length === 0 ? 'A counter appears here the first time a till reports in. Until then, only app orders are counted.' : 'Try a different code, branch or status.'}
) : ( <>
{showBranch ? : null} {visible.map((row) => ( {showBranch ? : null} ))}
CounterBranchStatus Last heard Period Last sale Action
{row.terminalId} {labels.isNamed(row.terminalId) ? labels.nameFor(row.terminalId) : row.branchName}
{row.branchName} {STATE_LABEL[row.state]} {row.status?.silentForMs == null ? 'Never' : `${shortAge(row.status.silentForMs)} ago`} {money(row.periodAmount)} {lastSale(row)}
Showing {(current_ - 1) * PAGE_SIZE + 1} to{' '} {Math.min(current_ * PAGE_SIZE, shown.length)} of {shown.length} counters {pages > 1 ? (
{Array.from({ length: pages }, (_, i) => i + 1).map((n) => ( ))}
) : null}
)}
{open ? ( setOpen(null)} /> ) : null}
); } /* ── Pieces ───────────────────────────────────────────────────────────────── */ function Kpi({ icon, label, value, note, tone, }: { icon: React.ReactNode; label: string; value: string; note: string; tone?: string }) { return (
{icon}
{label} {value} {note}
); } function Chip({ label, n, tone, active, onClick, }: { label: string; n: number; tone?: string; active: boolean; onClick: () => void }) { return ( ); } /** * The counters somebody has to walk to. * * Capped at three. The chips above already say how many there are, and a * fifteen-card wall is the thing this page was redesigned away from. */ function NeedsAttention({ rows, onOpen, }: { rows: readonly CounterRow[]; onOpen: (row: CounterRow) => void; }) { const problems = rows.filter((row) => row.state !== 'active').sort(byUrgency); if (problems.length === 0) { return (
Every counter is reporting Everything they have sold has reached us.
); } return (

Needs your attention

These counters need somebody to look at them.

{problems.slice(0, 3).map((row) => (
{row.terminalId}{' '} {STATE_LABEL[row.state]} {reasonFor(row)} {row.branchName}
))}
); } /** What a person needs to know before walking over. */ function reasonFor(row: CounterRow): string { if (row.state === 'never') return 'This counter has never recorded a sale.'; if (row.state === 'offline') { return row.status?.silentForMs == null ? 'No heartbeat received.' : `Last heard ${shortAge(row.status.silentForMs)} ago.`; } if (row.status && row.status.pendingBills > 0) { const n = row.status.pendingBills; return `${n} bill${n === 1 ? '' : 's'} taken here have not reached the books.`; } return row.card?.problem.detail ?? 'Reporting late.'; } function lastSale(row: CounterRow): string { const at = row.status?.lastBillAt; if (!at) return '—'; return `${shortAge(Date.now() - at.getTime())} ago`; }