feat: change email template + add list of items

This commit is contained in:
2026-10-09 09:30:26 +02:00
parent 6f237a937a
commit 5246aeabc1
17 changed files with 474 additions and 55 deletions
@@ -142,7 +142,6 @@ export default function DashboardLayout({
</nav>
<div className="p-4 border-t">
<LiveIndicator />
<LanguageSelector />
{user && (
<div className="mb-3 text-sm">
+59 -7
View File
@@ -1,7 +1,7 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { api, commandsApi, plannedTabsApi, remindersApi } from '@/lib/api/client';
import { PageHeader } from '@/components/page-header';
@@ -17,7 +17,7 @@ import { LoadingState } from '@/components/loading-state';
import { ErrorState } from '@/components/error-state';
import { toast } from '@/components/ui/toast';
import { formatCents, formatRelativeTime } from '@/lib/utils';
import { RefreshCw, Plus, X, Users, Mail, Send, Upload } from 'lucide-react';
import { RefreshCw, Plus, X, Users, Mail, Send, Upload, ChevronRight, ChevronDown } from 'lucide-react';
import { ImportCustomersDialog } from '@/components/import-customers-dialog';
import { useLang } from '@/lib/i18n';
import { useRealtime } from '@/lib/realtime';
@@ -29,6 +29,7 @@ export default function TabsPage() {
const queryClient = useQueryClient();
const [venueId, setVenueId] = useState<string | null>(null);
const [closeDialog, setCloseDialog] = useState<{ open: boolean; tab: any }>({ open: false, tab: null });
const [expandedTabs, setExpandedTabs] = useState<Set<string>>(new Set());
const [paymentMethod, setPaymentMethod] = useState<string>('');
const [creating, setCreating] = useState(false);
const [pendingCommandIds, setPendingCommandIds] = useState<Set<string>>(new Set());
@@ -233,9 +234,22 @@ export default function TabsPage() {
</TableRow>
</TableHeader>
<TableBody>
{tabs.map((tab: any) => (
<TableRow key={tab.tabId}>
<TableCell className="font-medium">{tab.customerName}</TableCell>
{tabs.map((tab: any) => {
const expanded = expandedTabs.has(tab.tabId);
const toggle = () => setExpandedTabs((current) => {
const next = new Set(current);
if (next.has(tab.tabId)) next.delete(tab.tabId); else next.add(tab.tabId);
return next;
});
return (
<Fragment key={tab.tabId}>
<TableRow>
<TableCell className="font-medium">
<button onClick={toggle} className="flex items-center gap-1 hover:text-primary" aria-expanded={expanded} aria-label={`${expanded ? 'Hide' : 'Show'} items for ${tab.customerName}`}>
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
{tab.customerName}
</button>
</TableCell>
<TableCell>
{tab.email ? (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
@@ -265,7 +279,19 @@ export default function TabsPage() {
<Button size="sm" disabled={tab.totalInCents <= 0} onClick={() => { setCloseDialog({ open: true, tab }); setPaymentMethod(''); }}>Close</Button>
</TableCell>
</TableRow>
))}
{expanded && (
<TableRow className="bg-muted/30 hover:bg-muted/30">
<TableCell colSpan={7} className="py-3 pl-4 sm:pl-10">
{/* Pinned to the visible area so lines stay readable while the table scrolls sideways on phones */}
<div className="sticky left-4 max-w-[min(28rem,calc(100vw-7rem))]">
<TabLines items={tab.items} />
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
</div>
@@ -290,6 +316,7 @@ export default function TabsPage() {
<div className="space-y-3 py-4">
<div className="flex justify-between text-sm"><span className="text-muted-foreground">Amount</span><span className="font-semibold">{formatCents(closeDialog.tab.totalInCents)}</span></div>
<div className="flex justify-between text-sm"><span className="text-muted-foreground">Items</span><span>{closeDialog.tab.itemCount}</span></div>
<div className="rounded-md border p-3"><TabLines items={closeDialog.tab.items} /></div>
<div className="flex justify-between text-sm"><span className="text-muted-foreground">Version</span><span>{closeDialog.tab.version}</span></div>
<div className="space-y-3 pt-2">
<Label className="text-base font-semibold">Payment method</Label>
@@ -310,4 +337,29 @@ export default function TabsPage() {
)}
</div>
);
}
}
type TabLine = { productName: string; quantity: number; unitPriceInCents: number };
/** The lines on a tab, e.g. "10× Cola · €2.50 each · €25.00". */
function TabLines({ items }: { items: TabLine[] | null | undefined }) {
if (!items) {
return <p className="text-sm text-muted-foreground">Items aren't reported by this terminal yet. Update the app on the tablet to see them.</p>;
}
if (items.length === 0) {
return <p className="text-sm text-muted-foreground">No items on this tab.</p>;
}
return (
<ul className="space-y-1 text-sm">
{items.map((item, i) => (
<li key={i} className="flex justify-between gap-4">
<span>
<span className="font-medium">{item.quantity}×</span> {item.productName}
<span className="text-muted-foreground"> · {formatCents(item.unitPriceInCents)} each</span>
</span>
<span className="tabular-nums">{formatCents(item.quantity * item.unitPriceInCents)}</span>
</li>
))}
</ul>
);
}
File diff suppressed because one or more lines are too long