diff --git a/apps/dashboard/src/app/dashboard/tabs/page.tsx b/apps/dashboard/src/app/dashboard/tabs/page.tsx index bae015c..610d1d3 100644 --- a/apps/dashboard/src/app/dashboard/tabs/page.tsx +++ b/apps/dashboard/src/app/dashboard/tabs/page.tsx @@ -5,7 +5,6 @@ import { Fragment, useState } from 'react'; import { commandsApi, plannedTabsApi, tabsApi, type Tab } from '@/lib/api/client'; import { TabOrders } from '@/components/tab-orders'; import { PageHeader } from '@/components/page-header'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { SortableHead } from '@/components/ui/sortable-head'; @@ -20,7 +19,7 @@ import { LoadingState } from '@/components/loading-state'; import { ErrorState } from '@/components/error-state'; import { toast } from '@/components/ui/toast'; import { formatCents } from '@/lib/utils'; -import { RefreshCw, Plus, X, Users, Mail, Upload, ChevronRight, ChevronDown, Search, Trash2 } from 'lucide-react'; +import { RefreshCw, Plus, X, Clock, Mail, Upload, ChevronRight, ChevronDown, Search, Trash2 } from 'lucide-react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { ImportCustomersDialog } from '@/components/import-customers-dialog'; import { useLang } from '@/lib/i18n'; @@ -44,6 +43,8 @@ export default function TabsPage() { const [plannedName, setPlannedName] = useState(''); const [plannedEmail, setPlannedEmail] = useState(''); const [importOpen, setImportOpen] = useState(false); + const [addOpen, setAddOpen] = useState(false); + const [adding, setAdding] = useState(false); const { t, tn, te, relativeTime } = useLang(); const { data, isLoading, error } = useQuery({ @@ -65,7 +66,7 @@ export default function TabsPage() { refetchInterval: live ? false : 10000, }); - // Tiles only show customers the terminal has not opened a tab for yet + // Customers the terminal has not opened a tab for yet; they get one on its next sync const pendingPlanned = (plannedQuery.data?.plannedTabs ?? []).filter((pt) => !pt.deliveredAt); const createCommandMutation = useMutation({ @@ -110,17 +111,25 @@ export default function TabsPage() { createCommandMutation.mutate({ tab: closeTab, paymentMethod }); } + function openAddDialog() { + setPlannedName(''); + setPlannedEmail(''); + setAddOpen(true); + } + async function addPlannedName() { const name = plannedName.trim(); - if (!name) return; + if (!name || adding) return; + setAdding(true); try { await plannedTabsApi.create(venueId, name, plannedEmail.trim() || undefined); - setPlannedName(''); - setPlannedEmail(''); + setAddOpen(false); queryClient.invalidateQueries({ queryKey: ['planned-tabs'] }); toast({ title: t('planned.added'), description: t('planned.added_desc', { name }) }); } catch (err) { toast({ title: t('common.failed'), description: te(err), variant: 'destructive' }); + } finally { + setAdding(false); } } @@ -139,7 +148,8 @@ export default function TabsPage() { const tabs = data?.tabs ?? []; const outstanding = tabs.reduce((sum, tab) => sum + tab.totalInCents, 0); const query = search.trim().toLowerCase(); - const visibleTabs = sort.apply(tabs.filter((tab) => !query || tab.customerName.toLowerCase().includes(query)), { + const visiblePlanned = pendingPlanned.filter((pt) => !query || pt.customerName.toLowerCase().includes(query)); + const visibleTabs =sort.apply(tabs.filter((tab) => !query || tab.customerName.toLowerCase().includes(query)), { customer: (tab) => tab.customerName, email: (tab) => tab.email, amount: (tab) => tab.totalInCents, @@ -151,65 +161,25 @@ export default function TabsPage() {
queryClient.invalidateQueries({ queryKey: ['tabs'] })}>{t('common.refresh')}} + description={[ + tn('tabs.count', tabs.length), + t('tabs.outstanding', { amount: formatCents(outstanding) }), + ...(pendingPlanned.length > 0 ? [tn('planned.pending_count', pendingPlanned.length)] : []), + ].join(' · ')} + actions={ +
+ + {canManage && ( + <> + + + + )} +
+ } /> - {/* Planned Customers */} - - -
- {t('planned.title')} - {canManage && ( - - )} -
-
- - {canManage && ( -
- setPlannedName(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && addPlannedName()} - className="max-w-xs" - /> - setPlannedEmail(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && addPlannedName()} - className="max-w-xs" - /> - -
- )} -
- {plannedQuery.data && pendingPlanned.length === 0 && ( -

{t('planned.waiting_empty')}

- )} - {pendingPlanned.map((pt) => ( -
- {pt.customerName} - {pt.email && } - {canManage && ( - - )} -
- ))} -
-
-
- - {tabs.length === 0 ? ( + {tabs.length === 0 && pendingPlanned.length === 0 ? ( ) : ( <> @@ -240,13 +210,52 @@ export default function TabsPage() { - {visibleTabs.length === 0 && ( + {visibleTabs.length === 0 && visiblePlanned.length === 0 && ( {t('tabs.no_matches', { query: search.trim() })} )} + {/* Not on the terminal yet: listed first so new customers are easy to spot until the sync picks them up */} + {visiblePlanned.map((pt) => ( + + + {pt.customerName} + + + {pt.email ? ( +
+ + {pt.email} +
+ ) : ( + — + )} +
+ — + — + + + {t('planned.pending')} + + + + {canManage && ( + + )} + +
+ ))} {visibleTabs.map((tab) => { const expanded = expandedTabs.has(tab.tabId); const toggle = () => setExpandedTabs((current) => { @@ -325,6 +334,36 @@ export default function TabsPage() { onImported={() => queryClient.invalidateQueries({ queryKey: ['planned-tabs'] })} /> + !adding && setAddOpen(open)}> + + + {t('planned.add')} + {t('planned.add_desc')} + +
{ + e.preventDefault(); + addPlannedName(); + }} + > +
+ + setPlannedName(e.target.value)} /> +
+
+ + setPlannedEmail(e.target.value)} /> +
+
+ + + + +
+
+ !open && !deleteTabMutation.isPending && setDeleteTab(null)} diff --git a/apps/dashboard/src/lib/i18n.tsx b/apps/dashboard/src/lib/i18n.tsx index 33f9244..3089694 100644 --- a/apps/dashboard/src/lib/i18n.tsx +++ b/apps/dashboard/src/lib/i18n.tsx @@ -133,12 +133,17 @@ const translations: Record> = { 'history.previous': 'Previous', 'history.next': 'Next', 'history.page': 'Page {page} of {total}', - 'planned.title': 'Planned Customers', - 'planned.name_placeholder': 'Add customer name...', + 'planned.name_placeholder': 'Customer name', 'planned.email_placeholder': 'Email (optional)', + 'planned.name': 'Name', + 'planned.email': 'Email', 'planned.add': 'Add customer', + 'planned.add_desc': 'The customer gets a tab on the terminal at its next sync.', 'planned.remove': 'Remove {name}', - 'planned.waiting_empty': 'No customers waiting for the terminal.', + 'planned.pending': 'Next sync', + 'planned.pending_hint': 'Gets a tab on the terminal at its next sync', + 'planned.pending_count': '{count} waiting for next sync', + 'planned.pending_count_plural': '{count} waiting for next sync', 'planned.added': 'Added', 'planned.added_desc': '"{name}" will get a tab on the terminal', 'import.button': 'Import from Excel', @@ -725,12 +730,17 @@ const translations: Record> = { 'history.previous': 'Vorige', 'history.next': 'Volgende', 'history.page': 'Pagina {page} van {total}', - 'planned.title': 'Geplande klanten', - 'planned.name_placeholder': 'Naam van de klant...', + 'planned.name_placeholder': 'Naam van de klant', 'planned.email_placeholder': 'E-mail (optioneel)', + 'planned.name': 'Naam', + 'planned.email': 'E-mail', 'planned.add': 'Klant toevoegen', + 'planned.add_desc': 'De klant krijgt een poef op de terminal bij de volgende synchronisatie.', 'planned.remove': '{name} verwijderen', - 'planned.waiting_empty': 'Geen klanten die op de terminal wachten.', + 'planned.pending': 'Volgende sync', + 'planned.pending_hint': 'Krijgt een poef op de terminal bij de volgende synchronisatie', + 'planned.pending_count': '{count} wacht op volgende sync', + 'planned.pending_count_plural': '{count} wachten op volgende sync', 'planned.added': 'Toegevoegd', 'planned.added_desc': '"{name}" krijgt een poef op de terminal', 'import.button': 'Importeren uit Excel',