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 && (
-
- )}
-
- {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'] })}
/>
+
+
!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',