import { useMemo, useState } from 'react'; import { Button } from '@astryxdesign/core/Button'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Table, type TableColumn } from '@astryxdesign/core/Table'; import { Text } from '@astryxdesign/core/Text'; import { TextInput } from '@astryxdesign/core/TextInput'; import { VStack } from '@astryxdesign/core/VStack'; import { AlertTriangle, Check, ChevronDown, ChevronRight, EyeOff, Monitor, Pencil, Send, } from 'lucide-react'; import type { DateRange } from '@/api/insights'; import { KpiCard } from '@/components/KpiCard'; import { PageHeader } from '@/components/PageHeader'; import { SectionHeader } from '@/components/SectionHeader'; import { useBranchScope } from '../BranchScope'; import { DateRangePicker, presetRange, type RangePreset } from '../DateRangePicker'; import { branchLabel } from '../format'; import { useCounterLabels, type CounterLabels } from '../counterLabels'; import { clockTime, spokenAge, UNASSIGNED } from '../posStatus'; import { BUCKET_LABEL, rupees, type Bucket } from '../terminalProblems'; import { useTerminalBoard, type BoardCard, type HealthyCounter } from '../useTerminalBoard'; import { TablePager } from '@/components/TablePager'; import { usePaged } from '@/components/usePaged'; /** * Counters — the till board, written for the person who actually opens it. * * That person is a shop supervisor who knows the shop and nothing about * networks. The previous version of this page was accurate and unusable by * them: it led with a terminal code and a word like "Stale", put the money in * the smallest grey type at the bottom, and expected the reader to translate * severity into action. * * So: three plainly-named groups, one problem per card, the stake in the * headline, one instruction in bold, and every technical reading — version, * battery, storage, exact timestamps — behind a Details toggle, because that is * support information and support is not who is looking. * * A shop with nothing wrong sees a nearly empty page that says so. That is the * correct output, and it is the one the old design could not produce. */ export function TerminalsPage() { const { branches, scoped, selected, current } = useBranchScope(); const [preset, setPreset] = useState('today'); const [range, setRange] = useState(() => presetRange('today')); const [showFine, setShowFine] = useState(false); const [showHidden, setShowHidden] = useState(false); const labels = useCounterLabels(); const board = useTerminalBoard({ branches: scoped, range, isHidden: labels.isHidden }); const needs = board.now.length + board.look.length; /** * The header line, now that the tiles carry the counts. * * It used to lead with "2 counters need attention. 41 sales haven't reached * us yet" — which the KPI row above now states in larger type, so keeping it * here printed the same two numbers twice, ten pixels apart. What is left is * the one thing no tile can say: why every counter is called T4A9. */ const summary = useMemo(() => { if (board.isLoading && board.total === 0) return 'Checking your counters…'; if (board.total === 0) return undefined; if (needs === 0) { return 'Everything they’ve sold has reached us. Each counter is listed by the code printed on your receipts, until you rename it.'; } return 'Counters are listed by the code printed on your receipts, until you rename them.'; }, [board.isLoading, board.total, needs]); return ( { setPreset(nextPreset); setRange(nextRange); }} /> } /> {/* The console's opening move on every operations page — Console, Sales, Inventory, Reports all lead with this same grid. This page used to open with a sentence instead, which is most of why it read as a bolt-on rather than part of the product. It also puts the stake where the eye lands. The sales waiting on an unreachable till were previously a clause in a paragraph, and they are the reason anybody opens this page. */} {board.total > 0 ? (
0 ? 'sales we can’t see' : 'everything is in'} tone={board.strandedBills > 0 ? 'error' : 'neutral'} icon={} /> 0 ? 'warning' : 'neutral'} icon={} /> } />
) : null} {/* A failed read is not an answer. Kept visually distinct from "this shop has no counters", because only one of the two means nothing is wrong. */} {board.failed.length > 0 ? ( We couldn’t check {board.failed.map(branchLabel).join(', ')} This is a problem reaching our own system, not a problem with your counters — they may be trading normally. ) : null} {board.now.length > 0 ? ( {board.now.map((card) => ( ))} ) : null} {board.look.length > 0 ? ( {board.look.map((card) => ( ))} ) : null} {board.fine.length > 0 ? ( setShowFine((open) => !open)} > ) : null} {board.total === 0 && !board.isLoading ? : null} {labels.hiddenCount > 0 ? ( {showHidden ? : null} ) : null}
); } /** * The page's one surface. * * A hairline and a radius, no fill. Every block here used to be a white * `elevation="low"` card, which on a tinted page reads as a stack of raised * slabs — and the console has been moving away from that everywhere else. * Defined once so the edges cannot drift apart the next time one of these is * edited. */ const panelStyle: React.CSSProperties = { borderRadius: 14, border: '1px solid var(--color-line)', }; /* ── Groups ──────────────────────────────────────────────────────────────── */ function Group({ bucket, count, children, isCollapsible, isOpen, onToggle, }: { bucket: Bucket; count: number; children: React.ReactNode; isCollapsible?: boolean; isOpen?: boolean; onToggle?: () => void; }) { return ( {/* `SectionHeader`, like every other section in the console. This used to be a hand-built row — an 8px coloured dot, an uppercase micro-label and a bare number — which is a heading style that exists nowhere else in the product. The colour went with it: severity is already carried by the wording and by the order the groups appear in, and saying it a third time in red was the page shouting. */} : } onClick={onToggle ?? (() => {})} /> ), } : {})} /> {!isCollapsible || isOpen ? {children} : null} ); } /* ── The card ────────────────────────────────────────────────────────────── */ /** * One counter, one problem. * * Full width and stacked rather than in a grid: each card is a short paragraph * now, and a paragraph wants a line length. Three columns of prose is the * layout that made the last version unreadable. */ function ProblemCard({ card, labels }: { card: BoardCard; labels: CounterLabels }) { const [isRenaming, setIsRenaming] = useState(false); const [showDetails, setShowDetails] = useState(false); const { problem } = card; return (
{/* The tinted icon tile `KpiCard` uses, in place of the 3px coloured rail this card used to carry. The rail existed nowhere else in the console; the tile is the shape the product already uses to mark what a block is about. */} {problem.bucket === 'now' ? : } {problem.headline} {problem.detail} {problem.action ? ( {problem.action} ) : null} {/* The reassurance. On a stuck-sales card this is the most important sentence on the page: it is the difference between a supervisor phoning head office and a supervisor checking a router. */} {problem.comfort ? ( {problem.comfort} ) : null} {/* Everything else wrong with this counter at this urgency. One line each, under the instruction — the same walk fixes them, and three full cards with the same name at the top read as a broken page. */} {card.also.length > 0 ? ( {card.also.map((extra) => ( Also: {extra.headline.toLowerCase()} {extra.action ? ` — ${extra.action.toLowerCase()}` : ''} ))} ) : null}
); } /** * The counter's name, and the code underneath it. * * The code stays visible even after renaming, because `models/poshealth.go` * calls it "the short code printed on invoices — the thing a support call * starts with". Someone reading a receipt needs to match it to a card. */ function CounterName({ card, labels, isRenaming, setIsRenaming, }: { card: BoardCard | HealthyCounter; labels: CounterLabels; isRenaming: boolean; setIsRenaming: (value: boolean) => void; }) { const [draft, setDraft] = useState(() => labels.nameFor(card.terminalId)); const named = labels.isNamed(card.terminalId); if (isRenaming) { return (