@fayz-ai/plugin-tables 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/ComandaSettingsView-ZB5RK7TP.js +176 -0
  2. package/dist/ComandaSettingsView-ZB5RK7TP.js.map +1 -0
  3. package/dist/TablesPage-VVDNK3GN.js +1145 -0
  4. package/dist/TablesPage-VVDNK3GN.js.map +1 -0
  5. package/dist/chunk-XHRPH676.js +12 -0
  6. package/dist/chunk-XHRPH676.js.map +1 -0
  7. package/dist/context.d.ts +10 -0
  8. package/dist/context.d.ts.map +1 -1
  9. package/dist/data/core-supabase.d.ts +32 -0
  10. package/dist/data/core-supabase.d.ts.map +1 -0
  11. package/dist/data/mock.d.ts.map +1 -1
  12. package/dist/data/registries.d.ts.map +1 -1
  13. package/dist/data/types.d.ts +39 -1
  14. package/dist/data/types.d.ts.map +1 -1
  15. package/dist/index.d.ts +7 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1234 -21
  18. package/dist/index.js.map +1 -1
  19. package/dist/locales/en.d.ts.map +1 -1
  20. package/dist/locales/pt-BR.d.ts.map +1 -1
  21. package/dist/migrations/index.d.ts +7 -0
  22. package/dist/migrations/index.d.ts.map +1 -0
  23. package/dist/store.d.ts +40 -1
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/types.d.ts +138 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/views/ComandaActions.d.ts +63 -0
  28. package/dist/views/ComandaActions.d.ts.map +1 -0
  29. package/dist/views/ComandaSettingsView.d.ts +3 -0
  30. package/dist/views/ComandaSettingsView.d.ts.map +1 -0
  31. package/dist/views/FloorPlanView.d.ts.map +1 -1
  32. package/dist/views/TableRailPage.d.ts +3 -0
  33. package/dist/views/TableRailPage.d.ts.map +1 -0
  34. package/package.json +4 -4
  35. package/dist/TablesPage-GKTE4SD2.js +0 -274
  36. package/dist/TablesPage-GKTE4SD2.js.map +0 -1
@@ -0,0 +1,1145 @@
1
+ import { TablesContextProvider, useTablesConfig, useTablesStore, useTablesProvider } from './chunk-XHRPH676.js';
2
+ import React, { useEffect, useMemo } from 'react';
3
+ import { UtensilsCrossed, Settings, Loader2, Receipt, Users, Minus, Plus, ChevronLeft, Sparkles, Utensils, CheckCircle2, X, UserPlus, Trash2, Percent, ArrowRightLeft, History, AlertTriangle } from 'lucide-react';
4
+ import { PageHeaderActions, Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Checkbox } from '@fayz-ai/ui';
5
+ import { RightRailPage, RecordPickerModal } from '@fayz-ai/admin';
6
+ import { useTranslation } from '@fayz-ai/core';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+
9
+ function money(v, currency = "BRL") {
10
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(v);
11
+ }
12
+ function guestLabel(g) {
13
+ return g.label?.trim() || `Pessoa ${g.seatNo}`;
14
+ }
15
+ function ParticipantBar({ order }) {
16
+ const activeGuestId = useTablesStore((s) => s.activeGuestId);
17
+ const selectGuest = useTablesStore((s) => s.selectGuest);
18
+ const addGuest = useTablesStore((s) => s.addGuest);
19
+ const removeGuest = useTablesStore((s) => s.removeGuest);
20
+ const tableLines = order.lines.filter((l) => !l.guestId && !l.cancelledAt);
21
+ const tableTotal = tableLines.reduce((sum, l) => sum + l.total, 0);
22
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
23
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
24
+ /* @__PURE__ */ jsxs(
25
+ "button",
26
+ {
27
+ type: "button",
28
+ onClick: () => selectGuest(null),
29
+ className: `rounded-full border px-2.5 py-1 text-xs transition-colors ${activeGuestId == null ? "border-primary bg-primary/10 font-medium text-primary" : "border-border text-muted-foreground hover:bg-muted/50"}`,
30
+ children: [
31
+ "Mesa",
32
+ tableTotal > 0 ? ` \xB7 ${money(tableTotal, order.currency)}` : ""
33
+ ]
34
+ }
35
+ ),
36
+ order.participants.map((g) => /* @__PURE__ */ jsxs("span", { className: "group/chip relative", children: [
37
+ /* @__PURE__ */ jsxs(
38
+ "button",
39
+ {
40
+ type: "button",
41
+ onClick: () => selectGuest(g.id),
42
+ className: `rounded-full border py-1 pl-2.5 pr-6 text-xs transition-colors ${activeGuestId === g.id ? "border-primary bg-primary/10 font-medium text-primary" : "border-border text-muted-foreground hover:bg-muted/50"}`,
43
+ children: [
44
+ guestLabel(g),
45
+ g.subtotal > 0 ? ` \xB7 ${money(g.subtotal, order.currency)}` : "",
46
+ g.ownServiceChargePercent != null && /* @__PURE__ */ jsxs("span", { className: "ml-1 opacity-70", children: [
47
+ "(",
48
+ g.ownServiceChargePercent,
49
+ "%)"
50
+ ] })
51
+ ]
52
+ }
53
+ ),
54
+ /* @__PURE__ */ jsx(
55
+ "button",
56
+ {
57
+ type: "button",
58
+ "aria-label": `Remover ${guestLabel(g)}`,
59
+ onClick: () => void removeGuest(g.id),
60
+ className: "absolute right-1 top-1/2 -translate-y-1/2 rounded-full p-0.5 opacity-0 transition-opacity hover:bg-muted group-hover/chip:opacity-100 focus-visible:opacity-100",
61
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
62
+ }
63
+ )
64
+ ] }, g.id)),
65
+ /* @__PURE__ */ jsxs(
66
+ Button,
67
+ {
68
+ variant: "outline",
69
+ size: "sm",
70
+ className: "h-7 rounded-full px-2 text-xs",
71
+ onClick: () => void addGuest(),
72
+ children: [
73
+ /* @__PURE__ */ jsx(UserPlus, { className: "mr-1 h-3 w-3" }),
74
+ " Pessoa"
75
+ ]
76
+ }
77
+ )
78
+ ] }),
79
+ activeGuestId != null && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
80
+ "O que for lan\xE7ado agora entra na conta de",
81
+ " ",
82
+ guestLabel(order.participants.find((g) => g.id === activeGuestId) ?? { seatNo: 0}),
83
+ "."
84
+ ] })
85
+ ] });
86
+ }
87
+ function LinePicker({ order, selected, onToggle, onAll }) {
88
+ const eligible = order.lines.filter((l) => !l.cancelledAt);
89
+ const allIds = eligible.map((l) => l.id);
90
+ const all = selected.length === eligible.length && eligible.length > 0;
91
+ const byGuest = new Map(
92
+ order.participants.map((g) => [g.id, guestLabel(g)])
93
+ );
94
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
95
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted/50", children: [
96
+ /* @__PURE__ */ jsx(Checkbox, { checked: all, onChange: () => onAll(all ? [] : allIds), "aria-label": "Todos os itens" }),
97
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Todos os itens" })
98
+ ] }),
99
+ /* @__PURE__ */ jsxs("div", { className: "max-h-64 space-y-1 overflow-y-auto", children: [
100
+ eligible.map((line) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-muted/50", children: [
101
+ /* @__PURE__ */ jsx(Checkbox, { checked: selected.includes(line.id), onChange: () => onToggle(line.id), "aria-label": line.name }),
102
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
103
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-sm", children: [
104
+ line.quantity,
105
+ "\xD7 ",
106
+ line.name
107
+ ] }),
108
+ /* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
109
+ line.guestId ? byGuest.get(line.guestId) ?? "Participante" : "Mesa",
110
+ line.discount > 0 && ` \xB7 j\xE1 com ${money(line.discount, order.currency)} de desconto`
111
+ ] })
112
+ ] }),
113
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums", children: money(line.total, order.currency) })
114
+ ] }, line.id)),
115
+ eligible.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-2 py-4 text-center text-sm text-muted-foreground", children: "Nada a escolher nesta comanda." })
116
+ ] })
117
+ ] });
118
+ }
119
+ function DiscountDialog({ order, onClose }) {
120
+ const discountLines = useTablesStore((s) => s.discountLines);
121
+ const [selected, setSelected] = React.useState([]);
122
+ const [mode, setMode] = React.useState("percent");
123
+ const [value, setValue] = React.useState("");
124
+ const [saving, setSaving] = React.useState(false);
125
+ const amount = Number(value.replace(",", ".")) || 0;
126
+ const chosen = order.lines.filter((l) => selected.includes(l.id));
127
+ const preview = chosen.reduce((sum, l) => {
128
+ const gross = l.unitPrice * l.quantity;
129
+ return sum + Math.min(Math.max(mode === "percent" ? gross * (amount / 100) : amount, 0), gross);
130
+ }, 0);
131
+ async function confirm() {
132
+ setSaving(true);
133
+ try {
134
+ await discountLines({ lineIds: selected, mode, value: amount });
135
+ onClose();
136
+ } finally {
137
+ setSaving(false);
138
+ }
139
+ }
140
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
141
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Desconto" }) }),
142
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
143
+ /* @__PURE__ */ jsx(
144
+ LinePicker,
145
+ {
146
+ order,
147
+ selected,
148
+ onToggle: (id) => setSelected((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]),
149
+ onAll: setSelected
150
+ }
151
+ ),
152
+ /* @__PURE__ */ jsxs("div", { className: "fz-container fz-fields fz-fields-2", children: [
153
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
154
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Tipo" }),
155
+ /* @__PURE__ */ jsxs(Select, { value: mode, onValueChange: (v) => setMode(v), children: [
156
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
157
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
158
+ /* @__PURE__ */ jsx(SelectItem, { value: "percent", children: "Percentual %" }),
159
+ /* @__PURE__ */ jsx(SelectItem, { value: "amount", children: "Valor R$" })
160
+ ] })
161
+ ] })
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
164
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: mode === "percent" ? "Percentual" : "Valor por item" }),
165
+ /* @__PURE__ */ jsx(
166
+ Input,
167
+ {
168
+ value,
169
+ onChange: (e) => setValue(e.target.value),
170
+ placeholder: mode === "percent" ? "10" : "5,00",
171
+ inputMode: "decimal"
172
+ }
173
+ )
174
+ ] })
175
+ ] }),
176
+ mode === "amount" && selected.length > 1 && /* @__PURE__ */ jsx("p", { className: "rounded-lg bg-muted/50 px-3 py-2 text-xs text-muted-foreground", children: "O valor \xE9 aplicado a CADA item escolhido, n\xE3o dividido entre eles." }),
177
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: selected.length === 0 ? "Escolha os itens." : `Sai ${money(preview, order.currency)} de ${selected.length} ${selected.length === 1 ? "item" : "itens"}.` })
178
+ ] }) }),
179
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
180
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
181
+ /* @__PURE__ */ jsx(Button, { onClick: confirm, disabled: selected.length === 0 || amount <= 0 || saving, children: saving ? "Aplicando\u2026" : "Aplicar desconto" })
182
+ ] })
183
+ ] }) });
184
+ }
185
+ function CancelDialog({ order, onClose }) {
186
+ const cancelLines = useTablesStore((s) => s.cancelLines);
187
+ const reasons = useTablesStore((s) => s.cancelReasons);
188
+ const fetchCancelReasons = useTablesStore((s) => s.fetchCancelReasons);
189
+ const [selected, setSelected] = React.useState([]);
190
+ const [reason, setReason] = React.useState("");
191
+ const [saving, setSaving] = React.useState(false);
192
+ React.useEffect(() => {
193
+ void fetchCancelReasons();
194
+ }, [fetchCancelReasons]);
195
+ const active = reasons.filter((r) => r.isActive);
196
+ const chosen = order.lines.filter((l) => selected.includes(l.id));
197
+ const amount = chosen.reduce((sum, l) => sum + l.total, 0);
198
+ async function confirm() {
199
+ setSaving(true);
200
+ try {
201
+ await cancelLines({ lineIds: selected, reason });
202
+ onClose();
203
+ } finally {
204
+ setSaving(false);
205
+ }
206
+ }
207
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
208
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Cancelar itens" }) }),
209
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
210
+ /* @__PURE__ */ jsx(
211
+ LinePicker,
212
+ {
213
+ order,
214
+ selected,
215
+ onToggle: (id) => setSelected((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]),
216
+ onAll: setSelected
217
+ }
218
+ ),
219
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
220
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Motivo" }),
221
+ active.length > 0 ? /* @__PURE__ */ jsxs(Select, { value: reason, onValueChange: setReason, children: [
222
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Escolha o motivo\u2026" }) }),
223
+ /* @__PURE__ */ jsx(SelectContent, { children: active.map((r) => /* @__PURE__ */ jsx(SelectItem, { value: r.label, children: r.label }, r.id)) })
224
+ ] }) : (
225
+ // Sem motivos cadastrados a tela não trava: escreve-se um. O
226
+ // caminho certo é cadastrá-los em Configurações, e é o que a
227
+ // frase abaixo diz.
228
+ /* @__PURE__ */ jsx(
229
+ Input,
230
+ {
231
+ value: reason,
232
+ onChange: (e) => setReason(e.target.value),
233
+ placeholder: "Por que este item sai da comanda?"
234
+ }
235
+ )
236
+ ),
237
+ active.length === 0 && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Nenhum motivo cadastrado. Configura\xE7\xF5es \u2192 Comanda." })
238
+ ] }),
239
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: selected.length === 0 ? "Escolha os itens." : `${money(amount, order.currency)} sai da conta. O item continua na comanda, riscado.` })
240
+ ] }) }),
241
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
242
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Voltar" }),
243
+ /* @__PURE__ */ jsx(
244
+ Button,
245
+ {
246
+ variant: "destructive",
247
+ onClick: confirm,
248
+ disabled: selected.length === 0 || reason.trim().length === 0 || saving,
249
+ children: saving ? "Cancelando\u2026" : "Cancelar itens"
250
+ }
251
+ )
252
+ ] })
253
+ ] }) });
254
+ }
255
+ function TransferDialog({ order, tables, onClose }) {
256
+ const transfer = useTablesStore((s) => s.transfer);
257
+ const [kind, setKind] = React.useState("table");
258
+ const [toTableId, setToTableId] = React.useState("");
259
+ const [lineIds, setLineIds] = React.useState([]);
260
+ const [toGuestId, setToGuestId] = React.useState("");
261
+ const [saving, setSaving] = React.useState(false);
262
+ const free = tables.filter((tb) => !tb.currentOrderId);
263
+ async function confirm() {
264
+ setSaving(true);
265
+ try {
266
+ await transfer(kind === "table" ? { kind: "table", toTableId } : { kind: "items", lineIds, toGuestId: toGuestId || null });
267
+ onClose();
268
+ } finally {
269
+ setSaving(false);
270
+ }
271
+ }
272
+ const ready = kind === "table" ? !!toTableId : lineIds.length > 0;
273
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
274
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Transferir" }) }),
275
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
276
+ /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: [["table", "Mesa inteira"], ["items", "Itens entre participantes"]].map(([id, label]) => /* @__PURE__ */ jsx(
277
+ "button",
278
+ {
279
+ type: "button",
280
+ onClick: () => setKind(id),
281
+ className: `flex-1 rounded-lg border-2 p-2 text-sm transition-colors ${kind === id ? "border-primary bg-primary/10 font-medium" : "border-border hover:bg-muted/50"}`,
282
+ children: label
283
+ },
284
+ id
285
+ )) }),
286
+ kind === "table" ? /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
287
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Para qual mesa" }),
288
+ /* @__PURE__ */ jsxs(Select, { value: toTableId, onValueChange: setToTableId, children: [
289
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Escolha a mesa\u2026" }) }),
290
+ /* @__PURE__ */ jsx(SelectContent, { children: free.map((tb) => /* @__PURE__ */ jsxs(SelectItem, { value: tb.id, children: [
291
+ "Mesa ",
292
+ tb.number,
293
+ tb.zoneName ? ` \xB7 ${tb.zoneName}` : "",
294
+ " \xB7 ",
295
+ tb.seats,
296
+ " lugares"
297
+ ] }, tb.id)) })
298
+ ] }),
299
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "S\xF3 mesas livres. A comanda inteira muda de lugar e a mesa atual fica livre." })
300
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
301
+ /* @__PURE__ */ jsx(
302
+ LinePicker,
303
+ {
304
+ order,
305
+ selected: lineIds,
306
+ onToggle: (id) => setLineIds((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]),
307
+ onAll: setLineIds
308
+ }
309
+ ),
310
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
311
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Para quem" }),
312
+ /* @__PURE__ */ jsxs(Select, { value: toGuestId || "__table", onValueChange: (v) => setToGuestId(v === "__table" ? "" : v), children: [
313
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
314
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
315
+ /* @__PURE__ */ jsx(SelectItem, { value: "__table", children: "A mesa" }),
316
+ order.participants.map((g) => /* @__PURE__ */ jsx(SelectItem, { value: g.id, children: guestLabel(g) }, g.id))
317
+ ] })
318
+ ] })
319
+ ] })
320
+ ] })
321
+ ] }) }),
322
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
323
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
324
+ /* @__PURE__ */ jsx(Button, { onClick: confirm, disabled: !ready || saving, children: saving ? "Transferindo\u2026" : "Transferir" })
325
+ ] })
326
+ ] }) });
327
+ }
328
+ function ServiceChargeDialog({ order, housePercent, onClose }) {
329
+ const setOrderServiceCharge = useTablesStore((s) => s.setOrderServiceCharge);
330
+ const setGuestServiceCharge = useTablesStore((s) => s.setGuestServiceCharge);
331
+ const [tablePct, setTablePct] = React.useState(
332
+ order.serviceChargePercent == null ? "" : String(order.serviceChargePercent)
333
+ );
334
+ const [guestPct, setGuestPct] = React.useState(
335
+ () => Object.fromEntries(order.participants.map((g) => [
336
+ g.id,
337
+ g.ownServiceChargePercent == null ? "" : String(g.ownServiceChargePercent)
338
+ ]))
339
+ );
340
+ const [saving, setSaving] = React.useState(false);
341
+ const parse = (v) => {
342
+ const trimmed = v.trim();
343
+ if (trimmed === "") return null;
344
+ const n = Number(trimmed.replace(",", "."));
345
+ return Number.isFinite(n) ? n : null;
346
+ };
347
+ const effectiveTable = parse(tablePct) ?? housePercent;
348
+ async function confirm() {
349
+ setSaving(true);
350
+ try {
351
+ await setOrderServiceCharge(parse(tablePct));
352
+ for (const g of order.participants) {
353
+ const next = parse(guestPct[g.id] ?? "");
354
+ if (next !== (g.ownServiceChargePercent ?? null)) {
355
+ await setGuestServiceCharge(g.id, next);
356
+ }
357
+ }
358
+ onClose();
359
+ } finally {
360
+ setSaving(false);
361
+ }
362
+ }
363
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
364
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Taxa de servi\xE7o" }) }),
365
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
366
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
367
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Esta mesa" }),
368
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
369
+ /* @__PURE__ */ jsx("span", { className: "w-28 shrink-0", children: /* @__PURE__ */ jsx(
370
+ Input,
371
+ {
372
+ value: tablePct,
373
+ onChange: (e) => setTablePct(e.target.value),
374
+ placeholder: String(housePercent),
375
+ inputMode: "decimal"
376
+ }
377
+ ) }),
378
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "%" })
379
+ ] }),
380
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
381
+ "Em branco usa a taxa da casa (",
382
+ housePercent,
383
+ "%)."
384
+ ] })
385
+ ] }),
386
+ order.participants.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
387
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
388
+ /* @__PURE__ */ jsx(Users, { className: "h-3.5 w-3.5" }),
389
+ " Por participante"
390
+ ] }),
391
+ order.participants.map((g) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
392
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: guestLabel(g) }),
393
+ /* @__PURE__ */ jsx("span", { className: "w-24 shrink-0", children: /* @__PURE__ */ jsx(
394
+ Input,
395
+ {
396
+ value: guestPct[g.id] ?? "",
397
+ onChange: (e) => setGuestPct((s) => ({ ...s, [g.id]: e.target.value })),
398
+ placeholder: String(effectiveTable),
399
+ inputMode: "decimal",
400
+ "aria-label": `Taxa de ${guestLabel(g)}`
401
+ }
402
+ ) }),
403
+ /* @__PURE__ */ jsx("span", { className: "w-4 text-sm text-muted-foreground", children: "%" })
404
+ ] }, g.id)),
405
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
406
+ "Em branco usa a taxa da mesa (",
407
+ effectiveTable,
408
+ "%)."
409
+ ] })
410
+ ] })
411
+ ] }) }),
412
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
413
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
414
+ /* @__PURE__ */ jsx(Button, { onClick: confirm, disabled: saving, children: saving ? "Salvando\u2026" : "Confirmar taxa" })
415
+ ] })
416
+ ] }) });
417
+ }
418
+ var TRANSFER_WORD = {
419
+ table: "Mesa inteira",
420
+ guest: "Participante",
421
+ items: "Itens"
422
+ };
423
+ function TransferHistoryDialog({ onClose }) {
424
+ const transfers = useTablesStore((s) => s.transfers);
425
+ const fetchTransfers = useTablesStore((s) => s.fetchTransfers);
426
+ React.useEffect(() => {
427
+ void fetchTransfers();
428
+ }, [fetchTransfers]);
429
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
430
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Hist\xF3rico de transfer\xEAncias" }) }),
431
+ /* @__PURE__ */ jsx(ModalBody, { children: transfers.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "Nenhuma transfer\xEAncia nesta comanda." }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: transfers.map((tr) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 border-b pb-2 last:border-b-0", children: [
432
+ /* @__PURE__ */ jsx(ArrowRightLeft, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
433
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
434
+ /* @__PURE__ */ jsxs("span", { className: "block text-sm", children: [
435
+ TRANSFER_WORD[tr.kind] ?? tr.kind,
436
+ tr.fromTableName && tr.toTableName && ` \xB7 ${tr.fromTableName} \u2192 ${tr.toTableName}`,
437
+ tr.kind === "items" && ` \xB7 ${tr.itemCount} ${tr.itemCount === 1 ? "item" : "itens"}`
438
+ ] }),
439
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: new Date(tr.createdAt).toLocaleString("pt-BR") })
440
+ ] })
441
+ ] }, tr.id)) }) }),
442
+ /* @__PURE__ */ jsx(ModalFooter, { children: /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Fechar" }) })
443
+ ] }) });
444
+ }
445
+ function ComandaVerbs({ onPick, disabled }) {
446
+ const verbs = [
447
+ ["discount", "Desconto", Percent],
448
+ ["cancel", "Cancelar", X],
449
+ ["transfer", "Transferir", ArrowRightLeft],
450
+ ["service", "Taxa", Percent],
451
+ ["history", "Hist\xF3rico", History]
452
+ ];
453
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-1.5", children: verbs.map(([id, label, Icon]) => /* @__PURE__ */ jsxs(
454
+ Button,
455
+ {
456
+ variant: "outline",
457
+ size: "sm",
458
+ disabled,
459
+ className: "h-8 justify-center px-1 text-xs",
460
+ onClick: () => onPick(id),
461
+ children: [
462
+ /* @__PURE__ */ jsx(Icon, { className: "mr-1 h-3 w-3 shrink-0" }),
463
+ " ",
464
+ label
465
+ ]
466
+ },
467
+ id
468
+ )) });
469
+ }
470
+ var STATUS_STYLE = {
471
+ available: { bg: "bg-success/10", fg: "text-success", Icon: CheckCircle2 },
472
+ occupied: { bg: "bg-primary/10", fg: "text-primary", Icon: Utensils },
473
+ reserved: { bg: "bg-accent/10", fg: "text-accent-foreground", Icon: Sparkles },
474
+ cleaning: { bg: "bg-muted", fg: "text-muted-foreground", Icon: Loader2 }
475
+ };
476
+ var QUICK_GUESTS = [1, 2, 3, 4, 5, 6, 8, 10];
477
+ function fill(text, values) {
478
+ return Object.entries(values).reduce(
479
+ (out, [key, value]) => out.replace(`{${key}}`, String(value)),
480
+ text
481
+ );
482
+ }
483
+ function useCurrency(currency = "BRL") {
484
+ return React.useCallback(
485
+ (value) => new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value),
486
+ [currency]
487
+ );
488
+ }
489
+ function useElapsed(seatedAt) {
490
+ const [, tick] = React.useReducer((n) => n + 1, 0);
491
+ React.useEffect(() => {
492
+ if (!seatedAt) return;
493
+ const id = window.setInterval(tick, 3e4);
494
+ return () => window.clearInterval(id);
495
+ }, [seatedAt]);
496
+ if (!seatedAt) return null;
497
+ return Math.max(0, Math.round((Date.now() - new Date(seatedAt).getTime()) / 6e4));
498
+ }
499
+ function formatElapsed(minutes) {
500
+ if (minutes == null) return null;
501
+ if (minutes < 60) return `${minutes}min`;
502
+ const h = Math.floor(minutes / 60);
503
+ const m = minutes % 60;
504
+ return m > 0 ? `${h}h${String(m).padStart(2, "0")}` : `${h}h`;
505
+ }
506
+ function splitEvenly(total, ways) {
507
+ if (ways <= 0) return total;
508
+ return Math.round(total / ways * 100) / 100;
509
+ }
510
+ function StatusBadge({ status, label }) {
511
+ const { bg, fg, Icon } = STATUS_STYLE[status];
512
+ return /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium ${bg} ${fg}`, children: [
513
+ /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3", "aria-hidden": true }),
514
+ label
515
+ ] });
516
+ }
517
+ function GuestPicker({ seats, value, onChange, t }) {
518
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
519
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
520
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("tables.panel.howManyGuests") }),
521
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
522
+ /* @__PURE__ */ jsx(
523
+ Button,
524
+ {
525
+ variant: "outline",
526
+ size: "icon",
527
+ className: "h-8 w-8",
528
+ "aria-label": t("tables.panel.oneLess"),
529
+ onClick: () => onChange(Math.max(1, value - 1)),
530
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-3.5 w-3.5" })
531
+ }
532
+ ),
533
+ /* @__PURE__ */ jsx("span", { className: "w-8 text-center text-lg font-bold tabular-nums", children: value }),
534
+ /* @__PURE__ */ jsx(
535
+ Button,
536
+ {
537
+ variant: "outline",
538
+ size: "icon",
539
+ className: "h-8 w-8",
540
+ "aria-label": t("tables.panel.oneMore"),
541
+ onClick: () => onChange(value + 1),
542
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" })
543
+ }
544
+ )
545
+ ] })
546
+ ] }),
547
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: QUICK_GUESTS.map((n) => /* @__PURE__ */ jsx(
548
+ "button",
549
+ {
550
+ onClick: () => onChange(n),
551
+ className: `fz-tap h-8 min-w-8 rounded-md border px-2 text-sm tabular-nums transition-colors ${n === value ? "border-primary bg-primary text-primary-foreground" : "hover:bg-accent"}`,
552
+ children: n
553
+ },
554
+ n
555
+ )) }),
556
+ value > seats && /* @__PURE__ */ jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [
557
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500", "aria-hidden": true }),
558
+ fill(t("tables.panel.overCapacity"), { seats, guests: value })
559
+ ] })
560
+ ] });
561
+ }
562
+ function LineRow({ line, owner, money: money2, onQuantity, onRemove, t }) {
563
+ const cancelled = !!line.cancelledAt;
564
+ return /* @__PURE__ */ jsxs("div", { className: `group flex items-center gap-2 border-b py-2 last:border-b-0 ${cancelled ? "opacity-60" : ""}`, children: [
565
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
566
+ /* @__PURE__ */ jsx("p", { className: `truncate text-sm font-medium ${cancelled ? "line-through" : ""}`, children: line.name }),
567
+ line.notes && /* @__PURE__ */ jsxs("p", { className: "truncate text-xs italic text-muted-foreground", children: [
568
+ "\u201C",
569
+ line.notes,
570
+ "\u201D"
571
+ ] }),
572
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs text-muted-foreground tabular-nums", children: [
573
+ money2(line.unitPrice),
574
+ " ",
575
+ t("tables.panel.each"),
576
+ owner && /* @__PURE__ */ jsxs("span", { className: "ml-1 not-italic", children: [
577
+ "\xB7 ",
578
+ owner
579
+ ] }),
580
+ line.discount > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-1", children: [
581
+ "\xB7 \u2212 ",
582
+ money2(line.discount)
583
+ ] })
584
+ ] }),
585
+ cancelled && /* @__PURE__ */ jsxs("p", { className: "truncate text-xs text-destructive", children: [
586
+ "Cancelado",
587
+ line.cancelReason ? ` \xB7 ${line.cancelReason}` : ""
588
+ ] })
589
+ ] }),
590
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 ${cancelled ? "invisible" : ""}`, children: [
591
+ /* @__PURE__ */ jsx(
592
+ Button,
593
+ {
594
+ variant: "outline",
595
+ size: "icon",
596
+ className: "h-7 w-7",
597
+ "aria-label": fill(t("tables.panel.removeOne"), { item: line.name }),
598
+ onClick: () => onQuantity(line.quantity - 1),
599
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" })
600
+ }
601
+ ),
602
+ /* @__PURE__ */ jsx("span", { className: "w-6 text-center text-sm font-semibold tabular-nums", children: line.quantity }),
603
+ /* @__PURE__ */ jsx(
604
+ Button,
605
+ {
606
+ variant: "outline",
607
+ size: "icon",
608
+ className: "h-7 w-7",
609
+ "aria-label": fill(t("tables.panel.addOne"), { item: line.name }),
610
+ onClick: () => onQuantity(line.quantity + 1),
611
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" })
612
+ }
613
+ )
614
+ ] }),
615
+ /* @__PURE__ */ jsx("span", { className: `w-20 shrink-0 text-right text-sm font-semibold tabular-nums ${cancelled ? "line-through" : ""}`, children: money2(line.total) }),
616
+ /* @__PURE__ */ jsx(
617
+ Button,
618
+ {
619
+ variant: "ghost",
620
+ size: "icon",
621
+ className: `h-7 w-7 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 ${cancelled ? "invisible" : ""}`,
622
+ "aria-label": fill(t("tables.panel.removeItem"), { item: line.name }),
623
+ onClick: onRemove,
624
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5 text-destructive" })
625
+ }
626
+ )
627
+ ] });
628
+ }
629
+ function AddItemsButton({ money: money2, t }) {
630
+ const provider = useTablesProvider();
631
+ const menuCategories = useTablesStore((s) => s.menuCategories);
632
+ const fetchMenuCategories = useTablesStore((s) => s.fetchMenuCategories);
633
+ const addOrderLine = useTablesStore((s) => s.addOrderLine);
634
+ const activeGuestId = useTablesStore((s) => s.activeGuestId);
635
+ const [open, setOpen] = React.useState(false);
636
+ const [category, setCategory] = React.useState(null);
637
+ React.useEffect(() => {
638
+ if (open) void fetchMenuCategories();
639
+ }, [open, fetchMenuCategories]);
640
+ const search = React.useCallback(
641
+ async (term) => provider.getMenuItems?.(term || void 0, category ?? void 0) ?? [],
642
+ [provider, category]
643
+ );
644
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
645
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "w-full", onClick: () => setOpen(true), children: [
646
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
647
+ " ",
648
+ t("tables.panel.addItem")
649
+ ] }),
650
+ /* @__PURE__ */ jsx(
651
+ RecordPickerModal,
652
+ {
653
+ open,
654
+ onClose: () => {
655
+ setOpen(false);
656
+ setCategory(null);
657
+ },
658
+ title: t("tables.panel.pickerTitle"),
659
+ placeholder: t("tables.panel.searchMenu"),
660
+ confirmLabel: t("tables.panel.addItem"),
661
+ emptyLabel: t("tables.panel.menuEmpty"),
662
+ columns: { product: t("tables.panel.columnItem"), meta: t("tables.panel.columnCategory"), value: t("tables.panel.columnPrice") },
663
+ filters: {
664
+ options: menuCategories.map((c) => ({ id: c.id, label: c.name })),
665
+ value: category,
666
+ onChange: setCategory,
667
+ allLabel: t("tables.panel.allCategories")
668
+ },
669
+ search,
670
+ rowKey: (item) => item.id,
671
+ renderRow: (item) => ({
672
+ imageUrl: item.imageUrl,
673
+ title: item.name,
674
+ meta: item.categoryName,
675
+ value: money2(item.price)
676
+ }),
677
+ onConfirm: (rows) => {
678
+ void (async () => {
679
+ for (const item of rows) {
680
+ await addOrderLine({
681
+ productId: item.id,
682
+ name: item.name,
683
+ quantity: 1,
684
+ unitPrice: item.price,
685
+ // Quem está selecionado na barra recebe a linha. Null = a mesa.
686
+ guestId: activeGuestId ?? void 0
687
+ });
688
+ }
689
+ })();
690
+ }
691
+ }
692
+ )
693
+ ] });
694
+ }
695
+ function OpenTableModal({ table, onClose }) {
696
+ const t = useTranslation();
697
+ const seatGuests = useTablesStore((s) => s.seatGuests);
698
+ const updateTableStatus = useTablesStore((s) => s.updateTableStatus);
699
+ const [guests, setGuests] = React.useState(() => Math.min(table.seats || 2, 4) || 2);
700
+ const [busy, setBusy] = React.useState(false);
701
+ const guestWord = (n) => t(n === 1 ? "tables.panel.guestOne" : "tables.panel.guestMany");
702
+ async function run(action, keepSelection = false) {
703
+ setBusy(true);
704
+ try {
705
+ await action();
706
+ if (!keepSelection) onClose();
707
+ } finally {
708
+ setBusy(false);
709
+ }
710
+ }
711
+ const title = table.name || `Mesa ${table.number}`;
712
+ const meta = [table.zoneName, `${table.seats} ${t("tables.floorPlan.seats")}`].filter(Boolean).join(" \xB7 ");
713
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
714
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: title }) }),
715
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
716
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: meta }),
717
+ table.status === "cleaning" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("tables.panel.cleaningDesc") }) : /* @__PURE__ */ jsx(GuestPicker, { seats: table.seats, value: guests, onChange: setGuests, t })
718
+ ] }) }),
719
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
720
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
721
+ table.status === "reserved" && /* @__PURE__ */ jsx(
722
+ Button,
723
+ {
724
+ variant: "outline",
725
+ disabled: busy,
726
+ onClick: () => void run(() => updateTableStatus({ tableId: table.id, status: "available" })),
727
+ children: t("tables.panel.cancelReservation")
728
+ }
729
+ ),
730
+ table.status === "cleaning" ? /* @__PURE__ */ jsx(
731
+ Button,
732
+ {
733
+ disabled: busy,
734
+ onClick: () => void run(() => updateTableStatus({ tableId: table.id, status: "available" })),
735
+ children: t("tables.detail.markClean")
736
+ }
737
+ ) : /* @__PURE__ */ jsxs(
738
+ Button,
739
+ {
740
+ disabled: busy,
741
+ onClick: () => void run(() => seatGuests({ tableId: table.id, guests }), true),
742
+ children: [
743
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Users, { className: "mr-1.5 h-4 w-4" }),
744
+ t("tables.panel.openTable"),
745
+ " \xB7 ",
746
+ guests,
747
+ " ",
748
+ guestWord(guests)
749
+ ]
750
+ }
751
+ )
752
+ ] })
753
+ ] }) });
754
+ }
755
+ function TableRailPage() {
756
+ const t = useTranslation();
757
+ const config = useTablesConfig();
758
+ const tables = useTablesStore((s) => s.tables);
759
+ const selectedTableId = useTablesStore((s) => s.selectedTableId);
760
+ const selectTable = useTablesStore((s) => s.selectTable);
761
+ const order = useTablesStore((s) => s.openOrder);
762
+ const orderLoading = useTablesStore((s) => s.openOrderLoading);
763
+ const comandaSupported = useTablesStore((s) => s.comandaSupported);
764
+ const closeSession = useTablesStore((s) => s.closeSession);
765
+ const setOrderLineQuantity = useTablesStore((s) => s.setOrderLineQuantity);
766
+ const setSessionGuests = useTablesStore((s) => s.setSessionGuests);
767
+ const settings = useTablesStore((s) => s.settings);
768
+ const fetchSettings = useTablesStore((s) => s.fetchSettings);
769
+ const table = selectedTableId ? tables.find((t2) => t2.id === selectedTableId) : void 0;
770
+ const money2 = useCurrency(order?.currency);
771
+ const elapsed = useElapsed(order?.seatedAt);
772
+ const [stage, setStage] = React.useState("order");
773
+ const [busy, setBusy] = React.useState(false);
774
+ const [verb, setVerb] = React.useState(null);
775
+ React.useEffect(() => {
776
+ void fetchSettings();
777
+ }, [fetchSettings]);
778
+ React.useEffect(() => {
779
+ setStage("order");
780
+ }, [table?.id]);
781
+ const ownerOf = React.useMemo(() => {
782
+ const byId = new Map((order?.participants ?? []).map((g) => [g.id, guestLabel(g)]));
783
+ return (guestId) => guestId ? byId.get(guestId) : void 0;
784
+ }, [order?.participants]);
785
+ if (!table) return null;
786
+ if (table.status !== "occupied") {
787
+ return /* @__PURE__ */ jsx(OpenTableModal, { table, onClose: () => selectTable(null) });
788
+ }
789
+ const statusLabel = t(`tables.floorPlan.${table.status}`);
790
+ const guestWord = (n) => t(n === 1 ? "tables.panel.guestOne" : "tables.panel.guestMany");
791
+ const lines = order?.lines ?? [];
792
+ const liveLines = lines.filter((l) => !l.cancelledAt);
793
+ const itemCount = liveLines.reduce((sum, l) => sum + l.quantity, 0);
794
+ const cancelledTotal = lines.filter((l) => l.cancelledAt).reduce((sum, l) => sum + l.total, 0);
795
+ const subtotal = order?.subtotal ?? 0;
796
+ const service = order?.tax ?? 0;
797
+ const housePercent = settings?.serviceChargePercent ?? config.serviceChargePercent ?? 0;
798
+ const servicePercent = order?.serviceChargePercent ?? housePercent;
799
+ const total = order?.total ?? 0;
800
+ const perPerson = splitEvenly(total, order?.guests || 1);
801
+ const meta = [
802
+ table.zoneName,
803
+ order ? `${order.guests} ${guestWord(order.guests)}` : `${table.seats} ${t("tables.floorPlan.seats")}`,
804
+ order?.waiterName,
805
+ formatElapsed(elapsed)
806
+ ].filter(Boolean).join(" \xB7 ");
807
+ async function run(action) {
808
+ setBusy(true);
809
+ try {
810
+ await action();
811
+ } finally {
812
+ setBusy(false);
813
+ }
814
+ }
815
+ return /* @__PURE__ */ jsxs(RightRailPage, { title: table.name || `Mesa ${table.number}`, open: true, onClose: () => selectTable(null), width: "max-w-md", children: [
816
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
817
+ /* @__PURE__ */ jsxs("div", { className: "border-b px-4 py-3", children: [
818
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
819
+ /* @__PURE__ */ jsx(StatusBadge, { status: table.status, label: statusLabel }),
820
+ order?.reference && /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs text-muted-foreground", children: [
821
+ t("tables.panel.ticket"),
822
+ " ",
823
+ order.reference
824
+ ] })
825
+ ] }),
826
+ /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-xs text-muted-foreground", children: meta })
827
+ ] }),
828
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: table.status === "occupied" && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
829
+ orderLoading && !order && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
830
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
831
+ " ",
832
+ t("tables.panel.ticketLoading")
833
+ ] }),
834
+ !comandaSupported && /* @__PURE__ */ jsx("p", { className: "rounded-md border bg-muted/40 p-3 text-sm text-muted-foreground", children: t("tables.panel.noReader") }),
835
+ order && lines.length === 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed p-6 text-center", children: [
836
+ /* @__PURE__ */ jsx(Receipt, { className: "mx-auto mb-2 h-8 w-8 text-muted-foreground/40", "aria-hidden": true }),
837
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("tables.panel.ticketEmpty") }),
838
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("tables.panel.ticketEmptyDesc") }),
839
+ /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(AddItemsButton, { money: money2, t }) })
840
+ ] }),
841
+ order && /* @__PURE__ */ jsx(ParticipantBar, { order }),
842
+ lines.length > 0 && /* @__PURE__ */ jsx("div", { children: lines.map((line) => /* @__PURE__ */ jsx(
843
+ LineRow,
844
+ {
845
+ line,
846
+ owner: ownerOf(line.guestId),
847
+ money: money2,
848
+ t,
849
+ onQuantity: (q) => void setOrderLineQuantity(line.id, q),
850
+ onRemove: () => void setOrderLineQuantity(line.id, 0)
851
+ },
852
+ line.id
853
+ )) }),
854
+ order && lines.length > 0 && /* @__PURE__ */ jsx(AddItemsButton, { money: money2, t }),
855
+ order && /* @__PURE__ */ jsx(ComandaVerbs, { onPick: setVerb, disabled: busy }),
856
+ order && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [
857
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [
858
+ /* @__PURE__ */ jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": true }),
859
+ " ",
860
+ t("tables.panel.guestsAtTable")
861
+ ] }),
862
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
863
+ /* @__PURE__ */ jsx(
864
+ Button,
865
+ {
866
+ variant: "outline",
867
+ size: "icon",
868
+ className: "h-7 w-7",
869
+ "aria-label": t("tables.panel.oneLess"),
870
+ onClick: () => void setSessionGuests(Math.max(1, (order.guests || 1) - 1)),
871
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" })
872
+ }
873
+ ),
874
+ /* @__PURE__ */ jsx("span", { className: "w-6 text-center text-sm font-semibold tabular-nums", children: order.guests }),
875
+ /* @__PURE__ */ jsx(
876
+ Button,
877
+ {
878
+ variant: "outline",
879
+ size: "icon",
880
+ className: "h-7 w-7",
881
+ "aria-label": t("tables.panel.oneMore"),
882
+ onClick: () => void setSessionGuests((order.guests || 0) + 1),
883
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" })
884
+ }
885
+ )
886
+ ] })
887
+ ] }),
888
+ order?.notes && /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-muted/40 p-3", children: [
889
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t("tables.panel.tableNote") }),
890
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm italic", children: [
891
+ "\u201C",
892
+ order.notes,
893
+ "\u201D"
894
+ ] })
895
+ ] })
896
+ ] }) }),
897
+ table.status === "occupied" && order && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 border-t px-4 py-3 text-sm", children: [
898
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
899
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("tables.panel.subtotal") }),
900
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(subtotal) })
901
+ ] }),
902
+ service > 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
903
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: order.participants.some((g) => g.ownServiceChargePercent != null) ? t("tables.panel.serviceMixed") : fill(t("tables.panel.service"), { percent: servicePercent }) }),
904
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(service) })
905
+ ] }),
906
+ cancelledTotal > 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs", children: [
907
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("tables.panel.cancelledTotal") }),
908
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground line-through", children: money2(cancelledTotal) })
909
+ ] }),
910
+ order.discount > 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
911
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("tables.panel.discount") }),
912
+ /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
913
+ "\u2212 ",
914
+ money2(order.discount)
915
+ ] })
916
+ ] }),
917
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between border-t pt-2", children: [
918
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("tables.panel.total") }),
919
+ /* @__PURE__ */ jsx("span", { className: "text-xl font-bold tabular-nums", children: money2(total) })
920
+ ] }),
921
+ (order.guests || 0) > 1 && order.participants.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-muted-foreground", children: [
922
+ /* @__PURE__ */ jsx("span", { children: fill(t("tables.panel.perPerson"), { guests: order.guests }) }),
923
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(perPerson) })
924
+ ] }),
925
+ order.participants.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-0.5 border-t pt-2 text-xs", children: order.participants.map((g) => /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-muted-foreground", children: [
926
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
927
+ guestLabel(g),
928
+ g.serviceCharge > 0 && ` \xB7 servi\xE7o ${g.serviceChargePercent}%`
929
+ ] }),
930
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(g.subtotal + g.serviceCharge) })
931
+ ] }, g.id)) }),
932
+ (order.paid > 0 || order.due !== total) && /* @__PURE__ */ jsxs("div", { className: "flex justify-between border-t pt-2 text-xs", children: [
933
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("tables.panel.paid") }),
934
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(order.paid) })
935
+ ] }),
936
+ order.paid > 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs font-medium", children: [
937
+ /* @__PURE__ */ jsx("span", { children: t("tables.panel.due") }),
938
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(order.due) })
939
+ ] })
940
+ ] }),
941
+ /* @__PURE__ */ jsx("div", { className: "border-t p-4", children: order && (stage === "order" ? /* @__PURE__ */ jsxs(Button, { className: "w-full", disabled: busy, onClick: () => setStage("closing"), children: [
942
+ t("tables.panel.closeBill"),
943
+ /* @__PURE__ */ jsx("span", { className: "ml-1.5 tabular-nums", children: money2(total) })
944
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
945
+ /* @__PURE__ */ jsxs(
946
+ "button",
947
+ {
948
+ className: "fz-tap flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground",
949
+ onClick: () => setStage("order"),
950
+ children: [
951
+ /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }),
952
+ " ",
953
+ t("tables.panel.backToTicket")
954
+ ]
955
+ }
956
+ ),
957
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
958
+ itemCount === 0 ? t("tables.panel.closingEmpty") : fill(
959
+ t(itemCount === 1 ? "tables.panel.closingSummaryOne" : "tables.panel.closingSummary"),
960
+ { count: itemCount, total: money2(total) }
961
+ ),
962
+ itemCount > 0 && (order.guests || 0) > 1 && ` \xB7 ${fill(t("tables.panel.closingPerPerson"), { amount: money2(perPerson) })}`
963
+ ] }),
964
+ /* @__PURE__ */ jsxs(
965
+ Button,
966
+ {
967
+ className: "w-full",
968
+ disabled: busy,
969
+ onClick: () => void run(async () => {
970
+ if (table.currentSessionId) await closeSession(table.currentSessionId);
971
+ setStage("order");
972
+ selectTable(null);
973
+ }),
974
+ children: [
975
+ busy && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
976
+ t("tables.panel.confirmClose")
977
+ ]
978
+ }
979
+ )
980
+ ] })) })
981
+ ] }),
982
+ order && verb === "discount" && /* @__PURE__ */ jsx(DiscountDialog, { order, onClose: () => setVerb(null) }),
983
+ order && verb === "cancel" && /* @__PURE__ */ jsx(CancelDialog, { order, onClose: () => setVerb(null) }),
984
+ order && verb === "transfer" && /* @__PURE__ */ jsx(TransferDialog, { order, tables, onClose: () => setVerb(null) }),
985
+ order && verb === "service" && /* @__PURE__ */ jsx(ServiceChargeDialog, { order, housePercent, onClose: () => setVerb(null) }),
986
+ order && verb === "history" && /* @__PURE__ */ jsx(TransferHistoryDialog, { onClose: () => setVerb(null) })
987
+ ] });
988
+ }
989
+ var statusConfig = {
990
+ available: { color: "text-success", bg: "bg-success/10 border-success/30 hover:bg-success/20" },
991
+ occupied: { color: "text-primary", bg: "bg-primary/10 border-primary/30 hover:bg-primary/20" },
992
+ reserved: { color: "text-accent", bg: "bg-accent/10 border-accent/30 hover:bg-accent/20" },
993
+ cleaning: { color: "text-muted-foreground", bg: "bg-muted border-border hover:bg-muted/80" }
994
+ };
995
+ function formatElapsed2(minutes, minLabel) {
996
+ if (minutes == null) return "";
997
+ if (minutes < 60) return `${minutes}${minLabel}`;
998
+ const h = Math.floor(minutes / 60);
999
+ const m = minutes % 60;
1000
+ return m > 0 ? `${h}h ${m}${minLabel}` : `${h}h`;
1001
+ }
1002
+ function TableCard({ table, isSelected, onClick, t }) {
1003
+ const config = statusConfig[table.status];
1004
+ const isLarge = table.seats >= 6;
1005
+ const minLabel = t("tables.floorPlan.min");
1006
+ return /* @__PURE__ */ jsxs(
1007
+ "button",
1008
+ {
1009
+ onClick,
1010
+ className: `
1011
+ relative flex flex-col items-center justify-center rounded-lg border-2 p-3 transition-all cursor-pointer
1012
+ ${config.bg}
1013
+ ${isLarge ? "col-span-2" : ""}
1014
+ ${isSelected ? "ring-2 ring-primary ring-offset-2" : ""}
1015
+ `,
1016
+ style: { minHeight: "120px" },
1017
+ children: [
1018
+ /* @__PURE__ */ jsx("span", { className: `text-2xl font-bold ${config.color}`, children: table.number }),
1019
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground mt-1", children: [
1020
+ table.seats,
1021
+ " ",
1022
+ t("tables.floorPlan.seats")
1023
+ ] }),
1024
+ table.status === "occupied" && /* @__PURE__ */ jsxs(Fragment, { children: [
1025
+ /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium mt-1", children: [
1026
+ table.currentGuests,
1027
+ " ",
1028
+ t("tables.detail.guests").toLowerCase()
1029
+ ] }),
1030
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: formatElapsed2(table.currentElapsedMinutes, minLabel) })
1031
+ ] }),
1032
+ /* @__PURE__ */ jsx(
1033
+ "span",
1034
+ {
1035
+ className: `
1036
+ absolute -top-2 -right-2 text-[10px] px-1.5 py-0.5 rounded-full font-medium border
1037
+ ${table.status === "occupied" ? "bg-primary text-primary-foreground border-primary" : "bg-secondary text-secondary-foreground border-border"}
1038
+ `,
1039
+ children: t(`tables.floorPlan.${table.status}`)
1040
+ }
1041
+ )
1042
+ ]
1043
+ }
1044
+ );
1045
+ }
1046
+ function FloorPlanSkeleton() {
1047
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
1048
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-4 w-20 rounded bg-muted animate-pulse" }, i)) }),
1049
+ /* @__PURE__ */ jsx("div", { className: "space-y-6", children: Array.from({ length: 2 }).map((_, z) => /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-5", children: [
1050
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-24 rounded bg-muted animate-pulse mb-4" }),
1051
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-6 gap-3", children: Array.from({ length: 6 }).map((_2, i) => /* @__PURE__ */ jsx("div", { className: "h-[120px] rounded-lg bg-muted animate-pulse" }, i)) })
1052
+ ] }, z)) })
1053
+ ] });
1054
+ }
1055
+ function FloorPlanView() {
1056
+ const t = useTranslation();
1057
+ useTablesConfig();
1058
+ const tables = useTablesStore((s) => s.tables);
1059
+ const zones = useTablesStore((s) => s.zones);
1060
+ const tablesLoading = useTablesStore((s) => s.tablesLoading);
1061
+ const fetchTables = useTablesStore((s) => s.fetchTables);
1062
+ const fetchZones = useTablesStore((s) => s.fetchZones);
1063
+ const selectedTableId = useTablesStore((s) => s.selectedTableId);
1064
+ const selectTable = useTablesStore((s) => s.selectTable);
1065
+ useEffect(() => {
1066
+ fetchTables();
1067
+ fetchZones();
1068
+ }, [fetchTables, fetchZones]);
1069
+ const tablesByZone = useMemo(() => {
1070
+ const map = /* @__PURE__ */ new Map();
1071
+ for (const zone of zones) {
1072
+ map.set(zone.id, { zoneName: zone.name, color: zone.color, tables: [] });
1073
+ }
1074
+ for (const table of tables) {
1075
+ const zoneId = table.zone;
1076
+ if (!map.has(zoneId)) {
1077
+ map.set(zoneId, { zoneName: table.zoneName || zoneId, tables: [] });
1078
+ }
1079
+ map.get(zoneId).tables.push(table);
1080
+ }
1081
+ return Array.from(map.entries()).filter(([, v]) => v.tables.length > 0).map(([id, v]) => ({ id, ...v }));
1082
+ }, [tables, zones]);
1083
+ const occupiedCount = tables.filter((t2) => t2.status === "occupied").length;
1084
+ const availableCount = tables.filter((t2) => t2.status === "available").length;
1085
+ const selectedTable = selectedTableId ? tables.find((t2) => t2.id === selectedTableId) ?? null : null;
1086
+ if (tablesLoading && tables.length === 0) {
1087
+ return /* @__PURE__ */ jsx(FloorPlanSkeleton, {});
1088
+ }
1089
+ if (!tablesLoading && tables.length === 0) {
1090
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-20 text-center", children: [
1091
+ /* @__PURE__ */ jsx(UtensilsCrossed, { className: "h-12 w-12 text-muted-foreground/40 mb-4" }),
1092
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: t("tables.floorPlan.noTables") }),
1093
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t("tables.floorPlan.noTablesDesc") })
1094
+ ] });
1095
+ }
1096
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
1097
+ /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
1098
+ "a",
1099
+ {
1100
+ href: "#/settings/tables",
1101
+ "aria-label": t("tables.floorPlan.manage"),
1102
+ title: t("tables.floorPlan.manage"),
1103
+ className: "fz-tap inline-flex h-9 w-9 items-center justify-center rounded-md border border-input text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground",
1104
+ children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
1105
+ }
1106
+ ) }),
1107
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between flex-wrap gap-4", children: [
1108
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: t("tables.floorPlan.occupiedOf").replace("{occupied}", String(occupiedCount)).replace("{available}", String(availableCount)).replace("{total}", String(tables.length)) }) }),
1109
+ /* @__PURE__ */ jsx("div", { className: "flex gap-3 flex-wrap", children: Object.keys(statusConfig).map((status) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [
1110
+ /* @__PURE__ */ jsx("div", { className: `h-3 w-3 rounded-full ${statusConfig[status].bg} border` }),
1111
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t(`tables.floorPlan.${status}`) })
1112
+ ] }, status)) })
1113
+ ] }),
1114
+ /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsx("div", { className: "space-y-6", children: tablesByZone.map((zone) => /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-5", children: [
1115
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-4", children: [
1116
+ zone.color && /* @__PURE__ */ jsx(
1117
+ "div",
1118
+ {
1119
+ className: "h-3 w-3 rounded-full border",
1120
+ style: { backgroundColor: zone.color }
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-muted-foreground uppercase tracking-wider", children: zone.zoneName })
1124
+ ] }),
1125
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-6 gap-3", children: zone.tables.map((table) => /* @__PURE__ */ jsx(
1126
+ TableCard,
1127
+ {
1128
+ table,
1129
+ isSelected: table.id === selectedTableId,
1130
+ onClick: () => selectTable(table.id === selectedTableId ? null : table.id),
1131
+ t
1132
+ },
1133
+ table.id
1134
+ )) })
1135
+ ] }, zone.id)) }) }),
1136
+ selectedTable && /* @__PURE__ */ jsx(TableRailPage, {})
1137
+ ] });
1138
+ }
1139
+ function TablesPage({ config, provider, store, registries }) {
1140
+ return /* @__PURE__ */ jsx(TablesContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(FloorPlanView, {}) });
1141
+ }
1142
+
1143
+ export { TablesPage };
1144
+ //# sourceMappingURL=TablesPage-VVDNK3GN.js.map
1145
+ //# sourceMappingURL=TablesPage-VVDNK3GN.js.map