@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.
- package/dist/ComandaSettingsView-ZB5RK7TP.js +176 -0
- package/dist/ComandaSettingsView-ZB5RK7TP.js.map +1 -0
- package/dist/TablesPage-VVDNK3GN.js +1145 -0
- package/dist/TablesPage-VVDNK3GN.js.map +1 -0
- package/dist/chunk-XHRPH676.js +12 -0
- package/dist/chunk-XHRPH676.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/data/core-supabase.d.ts +32 -0
- package/dist/data/core-supabase.d.ts.map +1 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/registries.d.ts.map +1 -1
- package/dist/data/types.d.ts +39 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1234 -21
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +7 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/store.d.ts +40 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/ComandaActions.d.ts +63 -0
- package/dist/views/ComandaActions.d.ts.map +1 -0
- package/dist/views/ComandaSettingsView.d.ts +3 -0
- package/dist/views/ComandaSettingsView.d.ts.map +1 -0
- package/dist/views/FloorPlanView.d.ts.map +1 -1
- package/dist/views/TableRailPage.d.ts +3 -0
- package/dist/views/TableRailPage.d.ts.map +1 -0
- package/package.json +4 -4
- package/dist/TablesPage-GKTE4SD2.js +0 -274
- package/dist/TablesPage-GKTE4SD2.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { TablesContextProvider } from './chunk-XHRPH676.js';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { createStore } from 'zustand/vanilla';
|
|
3
4
|
import { PluginSettingsPanel, dedup } from '@fayz-ai/admin';
|
|
4
5
|
import { toast } from 'sonner';
|
|
6
|
+
import { registerComponent, getSupabaseClientOptional } from '@fayz-ai/core';
|
|
5
7
|
import { createFayzClient } from '@fayz-ai/sdk';
|
|
6
8
|
|
|
7
|
-
// src/index.ts
|
|
8
|
-
|
|
9
9
|
// src/data/mock.ts
|
|
10
10
|
var uid = 0;
|
|
11
11
|
function nextId(prefix) {
|
|
@@ -60,13 +60,43 @@ function seedTables(zones) {
|
|
|
60
60
|
tables.push(makeTable(nextId("table"), 12, 2, bar.id, bar.name, "bar", 1, 0));
|
|
61
61
|
return tables;
|
|
62
62
|
}
|
|
63
|
+
var MENU_CATEGORIES = [
|
|
64
|
+
{ id: "c-mains", name: "Pratos" },
|
|
65
|
+
{ id: "c-starters", name: "Entradas" },
|
|
66
|
+
{ id: "c-drinks", name: "Bebidas" },
|
|
67
|
+
{ id: "c-desserts", name: "Sobremesas" }
|
|
68
|
+
];
|
|
69
|
+
var MENU = [
|
|
70
|
+
{ id: "m-1", name: "Picanha na chapa", price: 129.9, categoryId: "c-mains", categoryName: "Pratos" },
|
|
71
|
+
{ id: "m-2", name: "Moqueca de peixe", price: 98, categoryId: "c-mains", categoryName: "Pratos" },
|
|
72
|
+
{ id: "m-3", name: "Bolinho de bacalhau", price: 42, categoryId: "c-starters", categoryName: "Entradas" },
|
|
73
|
+
{ id: "m-4", name: "P\xE3o de alho", price: 18, categoryId: "c-starters", categoryName: "Entradas" },
|
|
74
|
+
{ id: "m-5", name: "Chopp 300ml", price: 14, categoryId: "c-drinks", categoryName: "Bebidas" },
|
|
75
|
+
{ id: "m-6", name: "Suco de laranja", price: 12, categoryId: "c-drinks", categoryName: "Bebidas" },
|
|
76
|
+
{ id: "m-7", name: "Pudim", price: 22, categoryId: "c-desserts", categoryName: "Sobremesas" }
|
|
77
|
+
];
|
|
63
78
|
function createMockTablesProvider() {
|
|
64
79
|
const zones = seedZones();
|
|
65
80
|
const tables = seedTables(zones);
|
|
66
81
|
const sessions = [];
|
|
82
|
+
const linesBySession = /* @__PURE__ */ new Map();
|
|
67
83
|
function findTable(id) {
|
|
68
84
|
return tables.find((t) => t.id === id) ?? null;
|
|
69
85
|
}
|
|
86
|
+
function linesOf(sessionId) {
|
|
87
|
+
let lines = linesBySession.get(sessionId);
|
|
88
|
+
if (!lines) {
|
|
89
|
+
lines = [];
|
|
90
|
+
linesBySession.set(sessionId, lines);
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
function recalc(sessionId) {
|
|
95
|
+
const total = linesOf(sessionId).reduce((sum, l) => sum + l.total, 0);
|
|
96
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
97
|
+
const table = session ? findTable(session.tableId) : null;
|
|
98
|
+
if (table) table.currentTotal = total;
|
|
99
|
+
}
|
|
70
100
|
function matchesQuery(table, query) {
|
|
71
101
|
if (!query) return true;
|
|
72
102
|
if (query.zone && table.zone !== query.zone) return false;
|
|
@@ -156,6 +186,8 @@ function createMockTablesProvider() {
|
|
|
156
186
|
sessions.push(session);
|
|
157
187
|
table.status = "occupied";
|
|
158
188
|
table.currentSessionId = session.id;
|
|
189
|
+
table.currentOrderId = session.id;
|
|
190
|
+
table.currentOrderReference = `#${session.id}`;
|
|
159
191
|
table.currentGuests = input.guests;
|
|
160
192
|
table.currentWaiterName = session.waiterName;
|
|
161
193
|
table.currentElapsedMinutes = 0;
|
|
@@ -168,6 +200,7 @@ function createMockTablesProvider() {
|
|
|
168
200
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
169
201
|
session.status = "closed";
|
|
170
202
|
session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
203
|
+
linesBySession.delete(session.id);
|
|
171
204
|
const table = findTable(session.tableId);
|
|
172
205
|
if (table) {
|
|
173
206
|
table.status = "cleaning";
|
|
@@ -216,6 +249,84 @@ function createMockTablesProvider() {
|
|
|
216
249
|
const idx = zones.findIndex((z) => z.id === id);
|
|
217
250
|
if (idx !== -1) zones.splice(idx, 1);
|
|
218
251
|
},
|
|
252
|
+
// ---- The comanda ----
|
|
253
|
+
async getTableOrder(tableId) {
|
|
254
|
+
const session = sessions.find((s) => s.tableId === tableId && s.status === "active");
|
|
255
|
+
if (!session) return null;
|
|
256
|
+
const lines = linesOf(session.id);
|
|
257
|
+
const subtotal = lines.reduce((sum, l) => sum + l.total, 0);
|
|
258
|
+
return {
|
|
259
|
+
id: session.id,
|
|
260
|
+
reference: `#${session.id}`,
|
|
261
|
+
status: "new",
|
|
262
|
+
guests: session.guests,
|
|
263
|
+
seatedAt: session.seatedAt,
|
|
264
|
+
notes: session.notes,
|
|
265
|
+
waiterId: session.waiterId,
|
|
266
|
+
waiterName: session.waiterName,
|
|
267
|
+
lines,
|
|
268
|
+
// O provider de demonstração não divide conta: participante é o eixo
|
|
269
|
+
// que só faz sentido com dado real atrás, e uma lista falsa de "Pessoa
|
|
270
|
+
// 1, Pessoa 2" ensinaria a tela errada a quem está avaliando o produto.
|
|
271
|
+
participants: [],
|
|
272
|
+
subtotal,
|
|
273
|
+
discount: 0,
|
|
274
|
+
tax: 0,
|
|
275
|
+
total: subtotal,
|
|
276
|
+
paid: 0,
|
|
277
|
+
due: subtotal,
|
|
278
|
+
currency: "BRL"
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
async getMenuCategories() {
|
|
282
|
+
return MENU_CATEGORIES;
|
|
283
|
+
},
|
|
284
|
+
async getMenuItems(search, categoryId) {
|
|
285
|
+
const term = search?.toLowerCase();
|
|
286
|
+
return MENU.filter((m) => (!term || m.name.toLowerCase().includes(term)) && (!categoryId || m.categoryId === categoryId));
|
|
287
|
+
},
|
|
288
|
+
async addOrderLine(sessionId, input) {
|
|
289
|
+
linesOf(sessionId).push({
|
|
290
|
+
id: nextId("line"),
|
|
291
|
+
productId: input.productId,
|
|
292
|
+
name: input.name,
|
|
293
|
+
quantity: input.quantity,
|
|
294
|
+
unitPrice: input.unitPrice,
|
|
295
|
+
discount: 0,
|
|
296
|
+
total: input.unitPrice * input.quantity,
|
|
297
|
+
notes: input.notes
|
|
298
|
+
});
|
|
299
|
+
recalc(sessionId);
|
|
300
|
+
},
|
|
301
|
+
async setOrderLineQuantity(lineId, quantity) {
|
|
302
|
+
for (const [sessionId, lines] of linesBySession) {
|
|
303
|
+
const idx = lines.findIndex((l) => l.id === lineId);
|
|
304
|
+
if (idx === -1) continue;
|
|
305
|
+
if (quantity <= 0) lines.splice(idx, 1);
|
|
306
|
+
else {
|
|
307
|
+
lines[idx].quantity = quantity;
|
|
308
|
+
lines[idx].total = lines[idx].unitPrice * quantity;
|
|
309
|
+
}
|
|
310
|
+
recalc(sessionId);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
async removeOrderLine(lineId) {
|
|
315
|
+
for (const [sessionId, lines] of linesBySession) {
|
|
316
|
+
const idx = lines.findIndex((l) => l.id === lineId);
|
|
317
|
+
if (idx === -1) continue;
|
|
318
|
+
lines.splice(idx, 1);
|
|
319
|
+
recalc(sessionId);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
async updateSessionGuests(sessionId, guests) {
|
|
324
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
325
|
+
if (!session) return;
|
|
326
|
+
session.guests = guests;
|
|
327
|
+
const table = findTable(session.tableId);
|
|
328
|
+
if (table) table.currentGuests = guests;
|
|
329
|
+
},
|
|
219
330
|
// ---- Summary ----
|
|
220
331
|
async getSummary() {
|
|
221
332
|
const available = tables.filter((t) => t.status === "available").length;
|
|
@@ -260,6 +371,14 @@ function createTablesStore(provider) {
|
|
|
260
371
|
summary: null,
|
|
261
372
|
summaryLoading: false,
|
|
262
373
|
selectedTableId: null,
|
|
374
|
+
openOrder: null,
|
|
375
|
+
openOrderLoading: false,
|
|
376
|
+
activeGuestId: null,
|
|
377
|
+
settings: null,
|
|
378
|
+
cancelReasons: [],
|
|
379
|
+
transfers: [],
|
|
380
|
+
comandaSupported: typeof provider.getTableOrder === "function",
|
|
381
|
+
menuCategories: [],
|
|
263
382
|
async fetchTables(query) {
|
|
264
383
|
return dedup("tables:tables:" + JSON.stringify(query), async () => {
|
|
265
384
|
set({ tablesLoading: true });
|
|
@@ -295,8 +414,217 @@ function createTablesStore(provider) {
|
|
|
295
414
|
set({ summary, summaryLoading: false });
|
|
296
415
|
});
|
|
297
416
|
},
|
|
417
|
+
/** After a write. Deliberately not `fetchTables`: that one dedups, and a
|
|
418
|
+
* refresh that joins an in-flight read shows the state before the write. */
|
|
419
|
+
async refreshAfterWrite() {
|
|
420
|
+
const tableId = get().selectedTableId;
|
|
421
|
+
const [tables, summary] = await Promise.all([
|
|
422
|
+
provider.getTables(),
|
|
423
|
+
provider.getSummary()
|
|
424
|
+
]);
|
|
425
|
+
set({ tables, summary });
|
|
426
|
+
if (tableId) await get().fetchOpenOrder(tableId);
|
|
427
|
+
},
|
|
298
428
|
selectTable(id) {
|
|
299
|
-
set({ selectedTableId: id });
|
|
429
|
+
set({ selectedTableId: id, openOrder: null, activeGuestId: null, transfers: [] });
|
|
430
|
+
if (id) void get().fetchOpenOrder(id);
|
|
431
|
+
},
|
|
432
|
+
async fetchOpenOrder(tableId) {
|
|
433
|
+
if (!provider.getTableOrder) return;
|
|
434
|
+
set({ openOrderLoading: true });
|
|
435
|
+
try {
|
|
436
|
+
const openOrder = await provider.getTableOrder(tableId);
|
|
437
|
+
if (get().selectedTableId !== tableId) return;
|
|
438
|
+
set({ openOrder });
|
|
439
|
+
} catch (err) {
|
|
440
|
+
set({ openOrder: null });
|
|
441
|
+
toast.error("N\xE3o foi poss\xEDvel ler a comanda", { description: err?.message });
|
|
442
|
+
} finally {
|
|
443
|
+
set({ openOrderLoading: false });
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
async fetchMenuCategories() {
|
|
447
|
+
if (!provider.getMenuCategories) return;
|
|
448
|
+
return dedup("tables:menuCategories", async () => {
|
|
449
|
+
try {
|
|
450
|
+
set({ menuCategories: await provider.getMenuCategories() });
|
|
451
|
+
} catch {
|
|
452
|
+
set({ menuCategories: [] });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
async addOrderLine(input) {
|
|
457
|
+
const order = get().openOrder;
|
|
458
|
+
if (!order || !provider.addOrderLine) return;
|
|
459
|
+
try {
|
|
460
|
+
await provider.addOrderLine(order.id, input);
|
|
461
|
+
await get().refreshAfterWrite();
|
|
462
|
+
} catch (err) {
|
|
463
|
+
toast.error("N\xE3o foi poss\xEDvel lan\xE7ar o item", { description: err?.message });
|
|
464
|
+
throw err;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
async setOrderLineQuantity(lineId, quantity) {
|
|
468
|
+
if (!provider.setOrderLineQuantity) return;
|
|
469
|
+
try {
|
|
470
|
+
await provider.setOrderLineQuantity(lineId, quantity);
|
|
471
|
+
await get().refreshAfterWrite();
|
|
472
|
+
} catch (err) {
|
|
473
|
+
toast.error("N\xE3o foi poss\xEDvel alterar o item", { description: err?.message });
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
selectGuest(guestId) {
|
|
477
|
+
set({ activeGuestId: guestId });
|
|
478
|
+
},
|
|
479
|
+
async fetchSettings() {
|
|
480
|
+
if (!provider.getSettings) return;
|
|
481
|
+
try {
|
|
482
|
+
set({ settings: await provider.getSettings() });
|
|
483
|
+
} catch {
|
|
484
|
+
set({ settings: null });
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
async saveSettings(patch) {
|
|
488
|
+
if (!provider.saveSettings) return;
|
|
489
|
+
try {
|
|
490
|
+
await provider.saveSettings(patch);
|
|
491
|
+
await get().fetchSettings();
|
|
492
|
+
await get().refreshAfterWrite();
|
|
493
|
+
toast.success("Configura\xE7\xF5es salvas");
|
|
494
|
+
} catch (err) {
|
|
495
|
+
toast.error("N\xE3o foi poss\xEDvel salvar", { description: err?.message });
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
async addGuest(label) {
|
|
499
|
+
const order = get().openOrder;
|
|
500
|
+
if (!order || !provider.addGuest) return;
|
|
501
|
+
try {
|
|
502
|
+
const guest = await provider.addGuest(order.id, label);
|
|
503
|
+
set({ activeGuestId: guest.id });
|
|
504
|
+
await get().refreshAfterWrite();
|
|
505
|
+
} catch (err) {
|
|
506
|
+
toast.error("N\xE3o foi poss\xEDvel acrescentar o participante", { description: err?.message });
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
async removeGuest(guestId) {
|
|
510
|
+
if (!provider.removeGuest) return;
|
|
511
|
+
try {
|
|
512
|
+
await provider.removeGuest(guestId);
|
|
513
|
+
if (get().activeGuestId === guestId) set({ activeGuestId: null });
|
|
514
|
+
await get().refreshAfterWrite();
|
|
515
|
+
} catch (err) {
|
|
516
|
+
toast.error("N\xE3o foi poss\xEDvel remover o participante", { description: err?.message });
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
async setGuestServiceCharge(guestId, percent) {
|
|
520
|
+
if (!provider.setGuestServiceCharge) return;
|
|
521
|
+
try {
|
|
522
|
+
await provider.setGuestServiceCharge(guestId, percent);
|
|
523
|
+
await get().refreshAfterWrite();
|
|
524
|
+
} catch (err) {
|
|
525
|
+
toast.error("N\xE3o foi poss\xEDvel mudar a taxa", { description: err?.message });
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
async setOrderServiceCharge(percent) {
|
|
529
|
+
const order = get().openOrder;
|
|
530
|
+
if (!order || !provider.setOrderServiceCharge) return;
|
|
531
|
+
try {
|
|
532
|
+
await provider.setOrderServiceCharge(order.id, percent);
|
|
533
|
+
await get().refreshAfterWrite();
|
|
534
|
+
} catch (err) {
|
|
535
|
+
toast.error("N\xE3o foi poss\xEDvel mudar a taxa", { description: err?.message });
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
async discountLines(input) {
|
|
539
|
+
const order = get().openOrder;
|
|
540
|
+
if (!order || !provider.discountLines) return;
|
|
541
|
+
try {
|
|
542
|
+
await provider.discountLines(order.id, input);
|
|
543
|
+
await get().refreshAfterWrite();
|
|
544
|
+
toast.success("Desconto aplicado");
|
|
545
|
+
} catch (err) {
|
|
546
|
+
toast.error("N\xE3o foi poss\xEDvel aplicar o desconto", { description: err?.message });
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
async fetchCancelReasons() {
|
|
550
|
+
if (!provider.getCancelReasons) return;
|
|
551
|
+
try {
|
|
552
|
+
set({ cancelReasons: await provider.getCancelReasons() });
|
|
553
|
+
} catch {
|
|
554
|
+
set({ cancelReasons: [] });
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
async addCancelReason(label) {
|
|
558
|
+
if (!provider.addCancelReason) return;
|
|
559
|
+
try {
|
|
560
|
+
await provider.addCancelReason(label);
|
|
561
|
+
await get().fetchCancelReasons();
|
|
562
|
+
} catch (err) {
|
|
563
|
+
toast.error("N\xE3o foi poss\xEDvel criar o motivo", { description: err?.message });
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
async updateCancelReason(id, patch) {
|
|
567
|
+
if (!provider.updateCancelReason) return;
|
|
568
|
+
try {
|
|
569
|
+
await provider.updateCancelReason(id, patch);
|
|
570
|
+
await get().fetchCancelReasons();
|
|
571
|
+
} catch (err) {
|
|
572
|
+
toast.error("N\xE3o foi poss\xEDvel alterar o motivo", { description: err?.message });
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
async deleteCancelReason(id) {
|
|
576
|
+
if (!provider.deleteCancelReason) return;
|
|
577
|
+
try {
|
|
578
|
+
await provider.deleteCancelReason(id);
|
|
579
|
+
await get().fetchCancelReasons();
|
|
580
|
+
} catch (err) {
|
|
581
|
+
toast.error("N\xE3o foi poss\xEDvel excluir o motivo", { description: err?.message });
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
async cancelLines(input) {
|
|
585
|
+
const order = get().openOrder;
|
|
586
|
+
if (!order || !provider.cancelLines) return;
|
|
587
|
+
try {
|
|
588
|
+
await provider.cancelLines(order.id, input);
|
|
589
|
+
await get().refreshAfterWrite();
|
|
590
|
+
toast.success(input.lineIds.length === 1 ? "Item cancelado" : `${input.lineIds.length} itens cancelados`);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
toast.error("N\xE3o foi poss\xEDvel cancelar", { description: err?.message });
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
async transfer(input) {
|
|
596
|
+
const order = get().openOrder;
|
|
597
|
+
if (!order || !provider.transfer) return;
|
|
598
|
+
try {
|
|
599
|
+
await provider.transfer(order.id, input);
|
|
600
|
+
await get().refreshAfterWrite();
|
|
601
|
+
await get().fetchTransfers();
|
|
602
|
+
toast.success("Transfer\xEAncia feita");
|
|
603
|
+
} catch (err) {
|
|
604
|
+
toast.error("N\xE3o foi poss\xEDvel transferir", { description: err?.message });
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
async fetchTransfers() {
|
|
608
|
+
const order = get().openOrder;
|
|
609
|
+
if (!order || !provider.getTransfers) {
|
|
610
|
+
set({ transfers: [] });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
set({ transfers: await provider.getTransfers(order.id) });
|
|
615
|
+
} catch {
|
|
616
|
+
set({ transfers: [] });
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
async setSessionGuests(guests) {
|
|
620
|
+
const order = get().openOrder;
|
|
621
|
+
if (!order || !provider.updateSessionGuests) return;
|
|
622
|
+
try {
|
|
623
|
+
await provider.updateSessionGuests(order.id, guests);
|
|
624
|
+
await get().refreshAfterWrite();
|
|
625
|
+
} catch (err) {
|
|
626
|
+
toast.error("N\xE3o foi poss\xEDvel mudar o n\xFAmero de pessoas", { description: err?.message });
|
|
627
|
+
}
|
|
300
628
|
},
|
|
301
629
|
async seatGuests(input) {
|
|
302
630
|
try {
|
|
@@ -307,10 +635,12 @@ function createTablesStore(provider) {
|
|
|
307
635
|
provider.getSummary()
|
|
308
636
|
]);
|
|
309
637
|
set({ tables, activeSessions, summary });
|
|
310
|
-
|
|
638
|
+
const tableId = get().selectedTableId;
|
|
639
|
+
if (tableId) await get().fetchOpenOrder(tableId);
|
|
640
|
+
toast.success("Mesa aberta");
|
|
311
641
|
return session;
|
|
312
642
|
} catch (err) {
|
|
313
|
-
toast.error("
|
|
643
|
+
toast.error("N\xE3o foi poss\xEDvel abrir a mesa", { description: err?.message });
|
|
314
644
|
throw err;
|
|
315
645
|
}
|
|
316
646
|
},
|
|
@@ -322,10 +652,10 @@ function createTablesStore(provider) {
|
|
|
322
652
|
provider.getActiveSessions(),
|
|
323
653
|
provider.getSummary()
|
|
324
654
|
]);
|
|
325
|
-
set({ tables, activeSessions, summary });
|
|
326
|
-
toast.success("
|
|
655
|
+
set({ tables, activeSessions, summary, openOrder: null });
|
|
656
|
+
toast.success("Conta fechada");
|
|
327
657
|
} catch (err) {
|
|
328
|
-
toast.error("
|
|
658
|
+
toast.error("N\xE3o foi poss\xEDvel fechar a conta", { description: err?.message });
|
|
329
659
|
throw err;
|
|
330
660
|
}
|
|
331
661
|
},
|
|
@@ -337,9 +667,9 @@ function createTablesStore(provider) {
|
|
|
337
667
|
provider.getSummary()
|
|
338
668
|
]);
|
|
339
669
|
set({ tables, summary });
|
|
340
|
-
toast.success("
|
|
670
|
+
toast.success("Mesa atualizada");
|
|
341
671
|
} catch (err) {
|
|
342
|
-
toast.error("
|
|
672
|
+
toast.error("N\xE3o foi poss\xEDvel atualizar a mesa", { description: err?.message });
|
|
343
673
|
throw err;
|
|
344
674
|
}
|
|
345
675
|
},
|
|
@@ -351,10 +681,10 @@ function createTablesStore(provider) {
|
|
|
351
681
|
provider.getSummary()
|
|
352
682
|
]);
|
|
353
683
|
set({ tables, summary });
|
|
354
|
-
toast.success("
|
|
684
|
+
toast.success("Mesa criada");
|
|
355
685
|
return table;
|
|
356
686
|
} catch (err) {
|
|
357
|
-
toast.error("
|
|
687
|
+
toast.error("N\xE3o foi poss\xEDvel criar a mesa", { description: err?.message });
|
|
358
688
|
throw err;
|
|
359
689
|
}
|
|
360
690
|
}
|
|
@@ -370,12 +700,67 @@ var zoneEntity = {
|
|
|
370
700
|
defaultSort: "sortOrder",
|
|
371
701
|
fields: [
|
|
372
702
|
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
373
|
-
{ key: "color", label: "Color", type: "
|
|
703
|
+
{ key: "color", label: "Color", type: "color", showInTable: true },
|
|
374
704
|
{ key: "sortOrder", label: "Order", type: "number", showInTable: true, defaultValue: 0 },
|
|
375
705
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
376
706
|
],
|
|
377
|
-
data: { table: "
|
|
707
|
+
data: { table: "plg_tables_zones", tenantScoped: true }
|
|
378
708
|
};
|
|
709
|
+
var tableEntity = {
|
|
710
|
+
name: "Table",
|
|
711
|
+
namePlural: "Tables",
|
|
712
|
+
icon: "Utensils",
|
|
713
|
+
displayField: "name",
|
|
714
|
+
defaultSort: "number",
|
|
715
|
+
fields: [
|
|
716
|
+
{ key: "number", label: "Number", type: "number", required: true, showInTable: true },
|
|
717
|
+
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
718
|
+
{ key: "seats", label: "Seats", type: "number", showInTable: true, defaultValue: 2 },
|
|
719
|
+
// The zone is a real FK, so it is picked from the zones registry rather
|
|
720
|
+
// than typed: the salão a mesa belongs to has to be one that exists, and
|
|
721
|
+
// `v_tables` joins on this id to group the floor plan.
|
|
722
|
+
{
|
|
723
|
+
key: "zoneId",
|
|
724
|
+
label: "Zone",
|
|
725
|
+
type: "relation",
|
|
726
|
+
relation: { table: "plg_tables_zones", labelField: "name" },
|
|
727
|
+
showInTable: true
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
key: "shape",
|
|
731
|
+
label: "Shape",
|
|
732
|
+
type: "select",
|
|
733
|
+
showInTable: true,
|
|
734
|
+
defaultValue: "square",
|
|
735
|
+
options: [
|
|
736
|
+
{ value: "square", label: "Square" },
|
|
737
|
+
{ value: "round", label: "Round" },
|
|
738
|
+
{ value: "rectangle", label: "Rectangle" },
|
|
739
|
+
{ value: "bar", label: "Bar" }
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
// `occupied` is deliberately absent: a mesa is occupied when a comanda is
|
|
743
|
+
// open on it, which `v_tables` derives. Offering it here would let someone
|
|
744
|
+
// type a state the floor plan then contradicts.
|
|
745
|
+
{
|
|
746
|
+
key: "status",
|
|
747
|
+
label: "Status",
|
|
748
|
+
type: "select",
|
|
749
|
+
showInTable: true,
|
|
750
|
+
defaultValue: "available",
|
|
751
|
+
options: [
|
|
752
|
+
{ value: "available", label: "Available" },
|
|
753
|
+
{ value: "reserved", label: "Reserved" },
|
|
754
|
+
{ value: "cleaning", label: "Cleaning" }
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
758
|
+
],
|
|
759
|
+
data: { table: "plg_tables_tables", tenantScoped: true }
|
|
760
|
+
};
|
|
761
|
+
zoneEntity.relatedLists = [
|
|
762
|
+
{ entity: tableEntity, foreignKey: "zoneId", label: "Tables", icon: "Utensils" }
|
|
763
|
+
];
|
|
379
764
|
var tablesRegistries = [
|
|
380
765
|
{
|
|
381
766
|
id: "zones",
|
|
@@ -383,11 +768,20 @@ var tablesRegistries = [
|
|
|
383
768
|
icon: "MapPin",
|
|
384
769
|
description: "Floor plan zones (indoor, outdoor, bar, etc.)",
|
|
385
770
|
display: "table",
|
|
771
|
+
// No seed ids: the primary key is a uuid the database generates, and the
|
|
772
|
+
// hand-written 'z-indoor' this used to carry could never be inserted.
|
|
386
773
|
seedData: [
|
|
387
|
-
{
|
|
388
|
-
{
|
|
389
|
-
{
|
|
774
|
+
{ name: "Indoor", color: "#3b82f6", sortOrder: 0, isActive: true },
|
|
775
|
+
{ name: "Outdoor", color: "#22c55e", sortOrder: 1, isActive: true },
|
|
776
|
+
{ name: "Bar", color: "#f59e0b", sortOrder: 2, isActive: true }
|
|
390
777
|
]
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
id: "tables",
|
|
781
|
+
entity: tableEntity,
|
|
782
|
+
icon: "Utensils",
|
|
783
|
+
description: "The mesas themselves \u2014 number, seats and which zone they sit in.",
|
|
784
|
+
display: "table"
|
|
391
785
|
}
|
|
392
786
|
];
|
|
393
787
|
|
|
@@ -398,6 +792,7 @@ var en = {
|
|
|
398
792
|
"tables.nav.zones": "Zones",
|
|
399
793
|
"tables.nav.history": "Session History",
|
|
400
794
|
"tables.floorPlan.title": "Floor Plan",
|
|
795
|
+
"tables.floorPlan.manage": "Manage tables and zones",
|
|
401
796
|
"tables.floorPlan.subtitle": "Manage table layout and seating",
|
|
402
797
|
"tables.floorPlan.available": "Available",
|
|
403
798
|
"tables.floorPlan.occupied": "Occupied",
|
|
@@ -435,7 +830,53 @@ var en = {
|
|
|
435
830
|
"tables.history.waiter": "Waiter",
|
|
436
831
|
"tables.history.duration": "Duration",
|
|
437
832
|
"tables.history.noSessions": "No session history yet",
|
|
438
|
-
"tables.settings.title": "Tables Settings"
|
|
833
|
+
"tables.settings.title": "Tables Settings",
|
|
834
|
+
// -- The table's panel (right rail) ----------------------------------------
|
|
835
|
+
"tables.panel.ticket": "Ticket",
|
|
836
|
+
"tables.panel.ticketLoading": "Reading the ticket\u2026",
|
|
837
|
+
"tables.panel.ticketEmpty": "Empty ticket",
|
|
838
|
+
"tables.panel.ticketEmptyDesc": "Nothing has been rung up for this table yet.",
|
|
839
|
+
"tables.panel.noReader": "This install does not read tickets from the floor plan \u2014 open the order under Orders.",
|
|
840
|
+
"tables.panel.addItem": "Add item",
|
|
841
|
+
"tables.panel.searchMenu": "Search the menu",
|
|
842
|
+
"tables.panel.menuEmpty": "Nothing by that name on the menu.",
|
|
843
|
+
"tables.panel.each": "each",
|
|
844
|
+
"tables.panel.addOne": "Add {item}",
|
|
845
|
+
"tables.panel.removeOne": "Remove one {item}",
|
|
846
|
+
"tables.panel.removeItem": "Remove {item}",
|
|
847
|
+
"tables.panel.howManyGuests": "How many guests",
|
|
848
|
+
"tables.panel.guestsAtTable": "Guests at the table",
|
|
849
|
+
"tables.panel.oneLess": "One guest fewer",
|
|
850
|
+
"tables.panel.oneMore": "One guest more",
|
|
851
|
+
"tables.panel.overCapacity": "This table seats {seats}, for {guests} guests.",
|
|
852
|
+
"tables.panel.tableNote": "Table note",
|
|
853
|
+
"tables.panel.cleaningDesc": "The table is out of service for cleaning. Mark it ready to free it up.",
|
|
854
|
+
"tables.panel.subtotal": "Subtotal",
|
|
855
|
+
"tables.panel.service": "Service {percent}%",
|
|
856
|
+
"tables.panel.serviceMixed": "Service (mixed rates)",
|
|
857
|
+
"tables.panel.cancelledTotal": "Cancelled",
|
|
858
|
+
"tables.panel.paid": "Paid",
|
|
859
|
+
"tables.panel.due": "Balance due",
|
|
860
|
+
"tables.panel.discount": "Discount",
|
|
861
|
+
"tables.panel.total": "Total",
|
|
862
|
+
"tables.panel.perPerson": "Per person ({guests})",
|
|
863
|
+
"tables.panel.openTable": "Open table",
|
|
864
|
+
"tables.panel.cancelReservation": "Cancel reservation",
|
|
865
|
+
"tables.panel.seat": "Seat",
|
|
866
|
+
"tables.panel.closeBill": "Close bill",
|
|
867
|
+
"tables.panel.backToTicket": "Back to the ticket",
|
|
868
|
+
"tables.panel.closingEmpty": "This ticket has nothing on it.",
|
|
869
|
+
"tables.panel.closingSummary": "{count} items \xB7 {total}",
|
|
870
|
+
"tables.panel.closingSummaryOne": "1 item \xB7 {total}",
|
|
871
|
+
"tables.panel.closingPerPerson": "{amount} per person",
|
|
872
|
+
"tables.panel.confirmClose": "Confirm close",
|
|
873
|
+
"tables.panel.guestOne": "guest",
|
|
874
|
+
"tables.panel.guestMany": "guests",
|
|
875
|
+
"tables.panel.pickerTitle": "Pick items",
|
|
876
|
+
"tables.panel.allCategories": "All",
|
|
877
|
+
"tables.panel.columnItem": "Item",
|
|
878
|
+
"tables.panel.columnCategory": "Category",
|
|
879
|
+
"tables.panel.columnPrice": "Price"
|
|
439
880
|
};
|
|
440
881
|
|
|
441
882
|
// src/locales/pt-BR.ts
|
|
@@ -445,6 +886,7 @@ var ptBR = {
|
|
|
445
886
|
"tables.nav.zones": "\xC1reas",
|
|
446
887
|
"tables.nav.history": "Hist\xF3rico",
|
|
447
888
|
"tables.floorPlan.title": "Mapa de Mesas",
|
|
889
|
+
"tables.floorPlan.manage": "Cadastrar mesas e sal\xF5es",
|
|
448
890
|
"tables.floorPlan.subtitle": "Gerencie o layout e a ocupa\xE7\xE3o das mesas",
|
|
449
891
|
"tables.floorPlan.available": "Dispon\xEDvel",
|
|
450
892
|
"tables.floorPlan.occupied": "Ocupada",
|
|
@@ -482,7 +924,53 @@ var ptBR = {
|
|
|
482
924
|
"tables.history.waiter": "Gar\xE7om",
|
|
483
925
|
"tables.history.duration": "Dura\xE7\xE3o",
|
|
484
926
|
"tables.history.noSessions": "Nenhum hist\xF3rico de sess\xF5es",
|
|
485
|
-
"tables.settings.title": "Configura\xE7\xF5es de Mesas"
|
|
927
|
+
"tables.settings.title": "Configura\xE7\xF5es de Mesas",
|
|
928
|
+
// -- The mesa's panel (right rail) -----------------------------------------
|
|
929
|
+
"tables.panel.ticket": "Comanda",
|
|
930
|
+
"tables.panel.ticketLoading": "Lendo a comanda\u2026",
|
|
931
|
+
"tables.panel.ticketEmpty": "Comanda vazia",
|
|
932
|
+
"tables.panel.ticketEmptyDesc": "Nada foi lan\xE7ado ainda para esta mesa.",
|
|
933
|
+
"tables.panel.noReader": "Esta instala\xE7\xE3o n\xE3o l\xEA comandas pelo mapa de mesas \u2014 abra o pedido em Pedidos.",
|
|
934
|
+
"tables.panel.addItem": "Lan\xE7ar item",
|
|
935
|
+
"tables.panel.searchMenu": "Buscar no card\xE1pio",
|
|
936
|
+
"tables.panel.menuEmpty": "Nada com esse nome no card\xE1pio.",
|
|
937
|
+
"tables.panel.each": "cada",
|
|
938
|
+
"tables.panel.addOne": "Adicionar {item}",
|
|
939
|
+
"tables.panel.removeOne": "Tirar um {item}",
|
|
940
|
+
"tables.panel.removeItem": "Remover {item}",
|
|
941
|
+
"tables.panel.howManyGuests": "Quantas pessoas",
|
|
942
|
+
"tables.panel.guestsAtTable": "Pessoas na mesa",
|
|
943
|
+
"tables.panel.oneLess": "Menos uma pessoa",
|
|
944
|
+
"tables.panel.oneMore": "Mais uma pessoa",
|
|
945
|
+
"tables.panel.overCapacity": "Esta mesa tem {seats} lugares para {guests} pessoas.",
|
|
946
|
+
"tables.panel.tableNote": "Observa\xE7\xE3o da mesa",
|
|
947
|
+
"tables.panel.cleaningDesc": "A mesa saiu de servi\xE7o e est\xE1 em limpeza. Marque como pronta para liber\xE1-la.",
|
|
948
|
+
"tables.panel.subtotal": "Subtotal",
|
|
949
|
+
"tables.panel.service": "Servi\xE7o {percent}%",
|
|
950
|
+
"tables.panel.serviceMixed": "Servi\xE7o (taxas diferentes)",
|
|
951
|
+
"tables.panel.cancelledTotal": "Cancelado",
|
|
952
|
+
"tables.panel.paid": "J\xE1 pago",
|
|
953
|
+
"tables.panel.due": "Saldo devedor",
|
|
954
|
+
"tables.panel.discount": "Desconto",
|
|
955
|
+
"tables.panel.total": "Total",
|
|
956
|
+
"tables.panel.perPerson": "Por pessoa ({guests})",
|
|
957
|
+
"tables.panel.openTable": "Abrir mesa",
|
|
958
|
+
"tables.panel.cancelReservation": "Cancelar reserva",
|
|
959
|
+
"tables.panel.seat": "Sentar",
|
|
960
|
+
"tables.panel.closeBill": "Fechar conta",
|
|
961
|
+
"tables.panel.backToTicket": "Voltar \xE0 comanda",
|
|
962
|
+
"tables.panel.closingEmpty": "Esta comanda n\xE3o tem nenhum item lan\xE7ado.",
|
|
963
|
+
"tables.panel.closingSummary": "{count} itens \xB7 {total}",
|
|
964
|
+
"tables.panel.closingSummaryOne": "1 item \xB7 {total}",
|
|
965
|
+
"tables.panel.closingPerPerson": "{amount} por pessoa",
|
|
966
|
+
"tables.panel.confirmClose": "Confirmar fechamento",
|
|
967
|
+
"tables.panel.guestOne": "pessoa",
|
|
968
|
+
"tables.panel.guestMany": "pessoas",
|
|
969
|
+
"tables.panel.pickerTitle": "Selecionar itens",
|
|
970
|
+
"tables.panel.allCategories": "Tudo",
|
|
971
|
+
"tables.panel.columnItem": "Item",
|
|
972
|
+
"tables.panel.columnCategory": "Categoria",
|
|
973
|
+
"tables.panel.columnPrice": "Pre\xE7o"
|
|
486
974
|
};
|
|
487
975
|
|
|
488
976
|
// src/locales/index.ts
|
|
@@ -752,9 +1240,702 @@ function createFayzTablesProvider(options = {}) {
|
|
|
752
1240
|
}
|
|
753
1241
|
};
|
|
754
1242
|
}
|
|
1243
|
+
var ZONES = "plg_tables_zones";
|
|
1244
|
+
var TABLES = "plg_tables_tables";
|
|
1245
|
+
var ORDER_EXT = "plg_tables_order_extensions";
|
|
1246
|
+
var VIEW = "v_tables";
|
|
1247
|
+
var LINES = "items";
|
|
1248
|
+
var CATALOG = "products";
|
|
1249
|
+
var SETTINGS = "plg_tables_settings";
|
|
1250
|
+
var GUESTS = "plg_tables_order_guests";
|
|
1251
|
+
var ITEM_GUESTS = "plg_tables_item_guests";
|
|
1252
|
+
var CANCEL_REASONS = "plg_tables_cancel_reasons";
|
|
1253
|
+
var TRANSFERS = "plg_tables_order_transfers";
|
|
1254
|
+
var GUESTS_VIEW = "v_comanda_guests";
|
|
1255
|
+
var BALANCES = "v_invoice_balances";
|
|
1256
|
+
var SECTIONS = "plg_menu_sections";
|
|
1257
|
+
var PLACEMENTS = "plg_menu_placements";
|
|
1258
|
+
var SEATING_KIND = "dine_in";
|
|
1259
|
+
var READ_ONLY = "This provider reads the floor plan; pass `writes` to let the app that owns it change tables.";
|
|
1260
|
+
function createCoreTablesProvider(options) {
|
|
1261
|
+
const db = () => {
|
|
1262
|
+
const client = getSupabaseClientOptional();
|
|
1263
|
+
if (!client) throw new Error("Supabase client is not configured");
|
|
1264
|
+
return client;
|
|
1265
|
+
};
|
|
1266
|
+
function requireWrites() {
|
|
1267
|
+
if (!options?.writes) throw new Error(READ_ONLY);
|
|
1268
|
+
return options.writes;
|
|
1269
|
+
}
|
|
1270
|
+
async function nextReference() {
|
|
1271
|
+
const spec = options?.writes?.reference;
|
|
1272
|
+
if (!spec) return void 0;
|
|
1273
|
+
try {
|
|
1274
|
+
const { data, error } = await db().rpc("next_sequence_authorized", {
|
|
1275
|
+
p_kind: spec.sequenceKind,
|
|
1276
|
+
p_permission: spec.permission ?? "orders.create"
|
|
1277
|
+
});
|
|
1278
|
+
if (error) throw new Error(error.message);
|
|
1279
|
+
return `${spec.prefix ?? ""}${String(data).padStart(spec.pad ?? 6, "0")}`;
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
console.warn("[tables] next_sequence_authorized unavailable:", err?.message);
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function tenant() {
|
|
1286
|
+
const id = requireWrites().tenantId();
|
|
1287
|
+
if (!id) throw new Error("No active organization: the floor plan cannot be changed without one");
|
|
1288
|
+
return id;
|
|
1289
|
+
}
|
|
1290
|
+
function shortRef(orderId) {
|
|
1291
|
+
return orderId ? `#${String(orderId).slice(0, 8)}` : void 0;
|
|
1292
|
+
}
|
|
1293
|
+
function num(value) {
|
|
1294
|
+
return Number(value) || 0;
|
|
1295
|
+
}
|
|
1296
|
+
function rowToLine(r) {
|
|
1297
|
+
const meta = r.metadata ?? {};
|
|
1298
|
+
return {
|
|
1299
|
+
id: r.id,
|
|
1300
|
+
productId: r.product_id ?? void 0,
|
|
1301
|
+
name: r.name ?? "",
|
|
1302
|
+
quantity: num(r.quantity),
|
|
1303
|
+
unitPrice: num(r.unit_price),
|
|
1304
|
+
discount: num(r.discount),
|
|
1305
|
+
total: num(r.total),
|
|
1306
|
+
notes: typeof meta.kitchenNotes === "string" ? meta.kitchenNotes : void 0,
|
|
1307
|
+
guestId: r.guest_id ?? void 0,
|
|
1308
|
+
cancelledAt: r.cancelled_at ?? void 0,
|
|
1309
|
+
cancelReason: r.cancel_reason ?? void 0
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function rowToGuest(r) {
|
|
1313
|
+
return {
|
|
1314
|
+
id: r.id,
|
|
1315
|
+
seatNo: Number(r.seat_no) || 0,
|
|
1316
|
+
label: r.label ?? void 0,
|
|
1317
|
+
ownServiceChargePercent: r.own_service_charge_percent == null ? void 0 : num(r.own_service_charge_percent),
|
|
1318
|
+
serviceChargePercent: num(r.service_charge_percent),
|
|
1319
|
+
subtotal: num(r.subtotal),
|
|
1320
|
+
serviceCharge: num(r.service_charge),
|
|
1321
|
+
cancelledTotal: num(r.cancelled_total),
|
|
1322
|
+
itemCount: Number(r.item_count) || 0
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
async function recalc(orderId) {
|
|
1326
|
+
const { error } = await db().rpc("comanda_recalc", { p_order_id: orderId });
|
|
1327
|
+
if (error) throw new Error(error.message);
|
|
1328
|
+
}
|
|
1329
|
+
function rowToTable2(r) {
|
|
1330
|
+
const elapsed = r.seated_at ? Math.max(0, Math.round((Date.now() - new Date(r.seated_at).getTime()) / 6e4)) : void 0;
|
|
1331
|
+
return {
|
|
1332
|
+
id: r.id,
|
|
1333
|
+
name: r.name,
|
|
1334
|
+
number: Number(r.number) || 0,
|
|
1335
|
+
seats: Number(r.seats) || 0,
|
|
1336
|
+
status: r.status ?? "available",
|
|
1337
|
+
zone: r.zone_id ?? "",
|
|
1338
|
+
zoneName: r.zone_name ?? void 0,
|
|
1339
|
+
shape: r.shape ?? "square",
|
|
1340
|
+
gridCol: Number(r.grid_col) || 0,
|
|
1341
|
+
gridRow: Number(r.grid_row) || 0,
|
|
1342
|
+
isActive: r.is_active !== false,
|
|
1343
|
+
// The session IS the comanda, so the two ids are deliberately the same:
|
|
1344
|
+
// closing one closes the other, and there is no third row to disagree.
|
|
1345
|
+
currentSessionId: r.current_order_id ?? void 0,
|
|
1346
|
+
currentOrderId: r.current_order_id ?? void 0,
|
|
1347
|
+
// A comanda opened by `seatGuests` has no reference_number — nothing in
|
|
1348
|
+
// the pool numbers one — so the short id stands in. It is still shorter
|
|
1349
|
+
// than the uuid the panel used to print in full.
|
|
1350
|
+
currentOrderReference: r.current_order_reference ?? shortRef(r.current_order_id),
|
|
1351
|
+
currentGuests: r.current_guests ?? void 0,
|
|
1352
|
+
currentElapsedMinutes: r.current_order_id ? elapsed : void 0,
|
|
1353
|
+
currentTotal: r.current_order_id ? Number(r.current_total) || 0 : void 0,
|
|
1354
|
+
tenantId: r.tenant_id,
|
|
1355
|
+
createdAt: r.created_at,
|
|
1356
|
+
updatedAt: r.updated_at
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
async function loadTables(query) {
|
|
1360
|
+
let q = db().from(VIEW).select("*").order("number");
|
|
1361
|
+
if (query?.zone) q = q.eq("zone_id", query.zone);
|
|
1362
|
+
if (query?.search) q = q.ilike("name", `%${query.search}%`);
|
|
1363
|
+
const { data, error } = await q;
|
|
1364
|
+
if (error) throw new Error(error.message);
|
|
1365
|
+
let tables = (data ?? []).map(rowToTable2);
|
|
1366
|
+
if (query?.status) {
|
|
1367
|
+
const wanted = Array.isArray(query.status) ? query.status : [query.status];
|
|
1368
|
+
tables = tables.filter((t) => wanted.includes(t.status));
|
|
1369
|
+
}
|
|
1370
|
+
return tables;
|
|
1371
|
+
}
|
|
1372
|
+
async function tableById(id) {
|
|
1373
|
+
const { data, error } = await db().from(VIEW).select("*").eq("id", id).maybeSingle();
|
|
1374
|
+
if (error) throw new Error(error.message);
|
|
1375
|
+
if (!data) throw new Error("Table not found");
|
|
1376
|
+
return rowToTable2(data);
|
|
1377
|
+
}
|
|
1378
|
+
async function openSeatings(tableId) {
|
|
1379
|
+
let q = db().from(ORDER_EXT).select("order_id, table_id, guests, seated_at, tenant_id, orders!inner(id, status, notes, assignee_id)").not("orders.status", "in", '("completed","cancelled")');
|
|
1380
|
+
const { data, error } = await q;
|
|
1381
|
+
if (error) throw new Error(error.message);
|
|
1382
|
+
return data ?? [];
|
|
1383
|
+
}
|
|
1384
|
+
function rowToSession(r, tableName) {
|
|
1385
|
+
const order = Array.isArray(r.orders) ? r.orders[0] : r.orders;
|
|
1386
|
+
const closed = order && ["completed", "cancelled"].includes(order.status);
|
|
1387
|
+
return {
|
|
1388
|
+
id: r.order_id,
|
|
1389
|
+
tableId: r.table_id,
|
|
1390
|
+
tableName,
|
|
1391
|
+
orderId: r.order_id,
|
|
1392
|
+
guests: r.guests ?? 0,
|
|
1393
|
+
waiterId: order?.assignee_id ?? void 0,
|
|
1394
|
+
seatedAt: r.seated_at,
|
|
1395
|
+
status: closed ? "closed" : "active",
|
|
1396
|
+
notes: order?.notes ?? void 0,
|
|
1397
|
+
tenantId: r.tenant_id,
|
|
1398
|
+
createdAt: r.seated_at
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
getTables: (query) => loadTables(query),
|
|
1403
|
+
async getTableById(id) {
|
|
1404
|
+
const { data, error } = await db().from(VIEW).select("*").eq("id", id).maybeSingle();
|
|
1405
|
+
if (error) throw new Error(error.message);
|
|
1406
|
+
return data ? rowToTable2(data) : null;
|
|
1407
|
+
},
|
|
1408
|
+
async createTable(input) {
|
|
1409
|
+
const tenantId = tenant();
|
|
1410
|
+
const { data, error } = await db().from(TABLES).insert({
|
|
1411
|
+
tenant_id: tenantId,
|
|
1412
|
+
zone_id: input.zone || null,
|
|
1413
|
+
name: input.name,
|
|
1414
|
+
number: input.number,
|
|
1415
|
+
seats: input.seats,
|
|
1416
|
+
shape: input.shape ?? "square",
|
|
1417
|
+
grid_col: input.gridCol ?? 0,
|
|
1418
|
+
grid_row: input.gridRow ?? 0
|
|
1419
|
+
}).select("id").single();
|
|
1420
|
+
if (error) throw new Error(error.message);
|
|
1421
|
+
return tableById(data.id);
|
|
1422
|
+
},
|
|
1423
|
+
async updateTable(id, data) {
|
|
1424
|
+
requireWrites();
|
|
1425
|
+
const patch = {};
|
|
1426
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
1427
|
+
if (data.number !== void 0) patch.number = data.number;
|
|
1428
|
+
if (data.seats !== void 0) patch.seats = data.seats;
|
|
1429
|
+
if (data.shape !== void 0) patch.shape = data.shape;
|
|
1430
|
+
if (data.zone !== void 0) patch.zone_id = data.zone || null;
|
|
1431
|
+
if (data.gridCol !== void 0) patch.grid_col = data.gridCol;
|
|
1432
|
+
if (data.gridRow !== void 0) patch.grid_row = data.gridRow;
|
|
1433
|
+
if (data.isActive !== void 0) patch.is_active = data.isActive;
|
|
1434
|
+
if (data.status !== void 0 && data.status !== "occupied") patch.status = data.status;
|
|
1435
|
+
if (Object.keys(patch).length) {
|
|
1436
|
+
const { error } = await db().from(TABLES).update(patch).eq("id", id);
|
|
1437
|
+
if (error) throw new Error(error.message);
|
|
1438
|
+
}
|
|
1439
|
+
return tableById(id);
|
|
1440
|
+
},
|
|
1441
|
+
async deleteTable(id) {
|
|
1442
|
+
requireWrites();
|
|
1443
|
+
const { error } = await db().from(TABLES).delete().eq("id", id);
|
|
1444
|
+
if (error) throw new Error(error.message);
|
|
1445
|
+
},
|
|
1446
|
+
async updateTableStatus(input) {
|
|
1447
|
+
requireWrites();
|
|
1448
|
+
if (input.status === "occupied") {
|
|
1449
|
+
throw new Error('Uma mesa fica ocupada ao abrir a comanda \u2014 use "sentar" em vez de marcar o estado.');
|
|
1450
|
+
}
|
|
1451
|
+
const { error } = await db().from(TABLES).update({ status: input.status }).eq("id", input.tableId);
|
|
1452
|
+
if (error) throw new Error(error.message);
|
|
1453
|
+
return tableById(input.tableId);
|
|
1454
|
+
},
|
|
1455
|
+
async updateTablePositions(positions) {
|
|
1456
|
+
requireWrites();
|
|
1457
|
+
for (const p of positions) {
|
|
1458
|
+
const { error } = await db().from(TABLES).update({ grid_col: p.gridCol, grid_row: p.gridRow }).eq("id", p.id);
|
|
1459
|
+
if (error) throw new Error(error.message);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
async seatGuests(input) {
|
|
1463
|
+
const tenantId = tenant();
|
|
1464
|
+
const table = await tableById(input.tableId);
|
|
1465
|
+
if (table.status === "occupied") throw new Error(`${table.name} j\xE1 tem uma comanda aberta`);
|
|
1466
|
+
const reference = await nextReference();
|
|
1467
|
+
const { data: order, error } = await db().from("orders").insert({
|
|
1468
|
+
tenant_id: tenantId,
|
|
1469
|
+
kind: SEATING_KIND,
|
|
1470
|
+
status: "new",
|
|
1471
|
+
channel: "dine_in",
|
|
1472
|
+
...reference ? { reference_number: reference } : {},
|
|
1473
|
+
...input.waiterId ? { assignee_id: input.waiterId } : {},
|
|
1474
|
+
...input.notes ? { notes: input.notes } : {},
|
|
1475
|
+
subtotal: 0,
|
|
1476
|
+
total: 0,
|
|
1477
|
+
currency: "BRL"
|
|
1478
|
+
}).select("id, status, notes, assignee_id").single();
|
|
1479
|
+
if (error) throw new Error(error.message);
|
|
1480
|
+
const { data: ext, error: extError } = await db().from(ORDER_EXT).insert({
|
|
1481
|
+
order_id: order.id,
|
|
1482
|
+
tenant_id: tenantId,
|
|
1483
|
+
table_id: input.tableId,
|
|
1484
|
+
guests: input.guests
|
|
1485
|
+
}).select("order_id, table_id, guests, seated_at, tenant_id").single();
|
|
1486
|
+
if (extError) {
|
|
1487
|
+
await db().from("orders").delete().eq("id", order.id);
|
|
1488
|
+
throw new Error(extError.message);
|
|
1489
|
+
}
|
|
1490
|
+
return rowToSession({ ...ext, orders: order }, table.name);
|
|
1491
|
+
},
|
|
1492
|
+
async closeSession(sessionId) {
|
|
1493
|
+
requireWrites();
|
|
1494
|
+
const { data: order, error } = await db().from("orders").update({ status: "completed" }).eq("id", sessionId).select("id, status, notes, assignee_id").single();
|
|
1495
|
+
if (error) throw new Error(error.message);
|
|
1496
|
+
const { data: ext } = await db().from(ORDER_EXT).select("order_id, table_id, guests, seated_at, tenant_id").eq("order_id", sessionId).maybeSingle();
|
|
1497
|
+
return rowToSession({ ...ext ?? { order_id: sessionId }, orders: order });
|
|
1498
|
+
},
|
|
1499
|
+
async getActiveSessions() {
|
|
1500
|
+
const [rows, tables] = await Promise.all([openSeatings(), loadTables()]);
|
|
1501
|
+
const names = new Map(tables.map((t) => [t.id, t.name]));
|
|
1502
|
+
return rows.map((r) => rowToSession(r, names.get(r.table_id)));
|
|
1503
|
+
},
|
|
1504
|
+
async getSessionHistory(tableId) {
|
|
1505
|
+
let q = db().from(ORDER_EXT).select("order_id, table_id, guests, seated_at, tenant_id, orders(id, status, notes, assignee_id)").order("seated_at", { ascending: false }).limit(200);
|
|
1506
|
+
if (tableId) q = q.eq("table_id", tableId);
|
|
1507
|
+
const { data, error } = await q;
|
|
1508
|
+
if (error) throw new Error(error.message);
|
|
1509
|
+
const names = new Map((await loadTables()).map((t) => [t.id, t.name]));
|
|
1510
|
+
return (data ?? []).map((r) => rowToSession(r, names.get(r.table_id)));
|
|
1511
|
+
},
|
|
1512
|
+
async getZones() {
|
|
1513
|
+
const { data, error } = await db().from(ZONES).select("*").order("sort_order");
|
|
1514
|
+
if (error) throw new Error(error.message);
|
|
1515
|
+
return (data ?? []).map((z) => ({
|
|
1516
|
+
id: z.id,
|
|
1517
|
+
name: z.name,
|
|
1518
|
+
color: z.color ?? void 0,
|
|
1519
|
+
sortOrder: z.sort_order ?? 0,
|
|
1520
|
+
isActive: z.is_active !== false,
|
|
1521
|
+
tenantId: z.tenant_id,
|
|
1522
|
+
createdAt: z.created_at
|
|
1523
|
+
}));
|
|
1524
|
+
},
|
|
1525
|
+
async createZone(data) {
|
|
1526
|
+
const tenantId = tenant();
|
|
1527
|
+
const { data: row, error } = await db().from(ZONES).insert({
|
|
1528
|
+
tenant_id: tenantId,
|
|
1529
|
+
name: data.name ?? "Sal\xE3o",
|
|
1530
|
+
color: data.color ?? null,
|
|
1531
|
+
sort_order: data.sortOrder ?? 0
|
|
1532
|
+
}).select("*").single();
|
|
1533
|
+
if (error) throw new Error(error.message);
|
|
1534
|
+
return {
|
|
1535
|
+
id: row.id,
|
|
1536
|
+
name: row.name,
|
|
1537
|
+
color: row.color ?? void 0,
|
|
1538
|
+
sortOrder: row.sort_order ?? 0,
|
|
1539
|
+
isActive: true,
|
|
1540
|
+
tenantId: row.tenant_id,
|
|
1541
|
+
createdAt: row.created_at
|
|
1542
|
+
};
|
|
1543
|
+
},
|
|
1544
|
+
async updateZone(id, data) {
|
|
1545
|
+
requireWrites();
|
|
1546
|
+
const patch = {};
|
|
1547
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
1548
|
+
if (data.color !== void 0) patch.color = data.color;
|
|
1549
|
+
if (data.sortOrder !== void 0) patch.sort_order = data.sortOrder;
|
|
1550
|
+
if (data.isActive !== void 0) patch.is_active = data.isActive;
|
|
1551
|
+
const { data: row, error } = await db().from(ZONES).update(patch).eq("id", id).select("*").single();
|
|
1552
|
+
if (error) throw new Error(error.message);
|
|
1553
|
+
return {
|
|
1554
|
+
id: row.id,
|
|
1555
|
+
name: row.name,
|
|
1556
|
+
color: row.color ?? void 0,
|
|
1557
|
+
sortOrder: row.sort_order ?? 0,
|
|
1558
|
+
isActive: row.is_active !== false,
|
|
1559
|
+
tenantId: row.tenant_id,
|
|
1560
|
+
createdAt: row.created_at
|
|
1561
|
+
};
|
|
1562
|
+
},
|
|
1563
|
+
async deleteZone(id) {
|
|
1564
|
+
requireWrites();
|
|
1565
|
+
const { error } = await db().from(ZONES).delete().eq("id", id);
|
|
1566
|
+
if (error) throw new Error(error.message);
|
|
1567
|
+
},
|
|
1568
|
+
// -- The comanda ---------------------------------------------------------
|
|
1569
|
+
async getTableOrder(tableId) {
|
|
1570
|
+
const { data, error } = await db().from(ORDER_EXT).select("order_id, guests, seated_at, service_charge_percent, orders!inner(id, reference_number, status, notes, assignee_id, party_id, subtotal, discount, tax, total, currency)").eq("table_id", tableId).not("orders.status", "in", '("completed","cancelled")').order("seated_at", { ascending: false }).limit(1);
|
|
1571
|
+
if (error) throw new Error(error.message);
|
|
1572
|
+
const row = (data ?? [])[0];
|
|
1573
|
+
if (!row) return null;
|
|
1574
|
+
const order = Array.isArray(row.orders) ? row.orders[0] : row.orders;
|
|
1575
|
+
const [
|
|
1576
|
+
{ data: lines, error: linesError },
|
|
1577
|
+
{ data: people },
|
|
1578
|
+
{ data: guestRows },
|
|
1579
|
+
{ data: itemGuests },
|
|
1580
|
+
{ data: balance }
|
|
1581
|
+
] = await Promise.all([
|
|
1582
|
+
// `cancelled_at` e `cancel_reason` entram no select: a linha cancelada
|
|
1583
|
+
// CONTINUA na comanda, riscada. Filtrá-la aqui esconderia justamente o
|
|
1584
|
+
// que o gerente abre a comanda para ver.
|
|
1585
|
+
db().from(LINES).select("id, product_id, name, quantity, unit_price, discount, total, sort_order, metadata, cancelled_at, cancel_reason").eq("order_id", order.id).order("sort_order"),
|
|
1586
|
+
order.assignee_id || order.party_id ? db().from("people").select("id, name").in("id", [order.assignee_id, order.party_id].filter(Boolean)) : Promise.resolve({ data: [] }),
|
|
1587
|
+
db().from(GUESTS_VIEW).select("*").eq("order_id", order.id).order("seat_no"),
|
|
1588
|
+
db().from(ITEM_GUESTS).select("item_id, guest_id"),
|
|
1589
|
+
db().from(BALANCES).select("paid, balance").eq("invoice_id", order.id).maybeSingle()
|
|
1590
|
+
]);
|
|
1591
|
+
if (linesError) throw new Error(linesError.message);
|
|
1592
|
+
const nameById = new Map((people ?? []).map((p) => [p.id, p.name]));
|
|
1593
|
+
const guestOfItem = new Map(
|
|
1594
|
+
(itemGuests ?? []).map((r) => [r.item_id, r.guest_id])
|
|
1595
|
+
);
|
|
1596
|
+
return {
|
|
1597
|
+
id: order.id,
|
|
1598
|
+
reference: order.reference_number ?? shortRef(order.id),
|
|
1599
|
+
status: order.status,
|
|
1600
|
+
guests: row.guests ?? 0,
|
|
1601
|
+
seatedAt: row.seated_at,
|
|
1602
|
+
notes: order.notes ?? void 0,
|
|
1603
|
+
waiterId: order.assignee_id ?? void 0,
|
|
1604
|
+
waiterName: order.assignee_id ? nameById.get(order.assignee_id) : void 0,
|
|
1605
|
+
partyName: order.party_id ? nameById.get(order.party_id) : void 0,
|
|
1606
|
+
lines: (lines ?? []).map((l) => rowToLine({ ...l, guest_id: guestOfItem.get(l.id) ?? null })),
|
|
1607
|
+
participants: (guestRows ?? []).map(rowToGuest),
|
|
1608
|
+
serviceChargePercent: row.service_charge_percent == null ? void 0 : num(row.service_charge_percent),
|
|
1609
|
+
subtotal: num(order.subtotal),
|
|
1610
|
+
discount: num(order.discount),
|
|
1611
|
+
tax: num(order.tax),
|
|
1612
|
+
total: num(order.total),
|
|
1613
|
+
// Sem fatura levantada ainda, nada foi pago e tudo é devido. É a
|
|
1614
|
+
// resposta certa e não um buraco: a comanda aberta é, por definição,
|
|
1615
|
+
// dinheiro a receber.
|
|
1616
|
+
paid: num(balance?.paid),
|
|
1617
|
+
due: balance ? num(balance.balance) : num(order.total),
|
|
1618
|
+
currency: order.currency ?? "BRL"
|
|
1619
|
+
};
|
|
1620
|
+
},
|
|
1621
|
+
async getMenuCategories() {
|
|
1622
|
+
const sections = await db().from(SECTIONS).select("id, name, sort_order").eq("is_active", true).order("sort_order");
|
|
1623
|
+
if (!sections.error && (sections.data ?? []).length > 0) {
|
|
1624
|
+
return (sections.data ?? []).map((r) => ({ id: r.id, name: r.name }));
|
|
1625
|
+
}
|
|
1626
|
+
const { data, error } = await db().from("categories").select("id, name").order("name");
|
|
1627
|
+
if (error) throw new Error(error.message);
|
|
1628
|
+
return (data ?? []).map((c) => ({ id: c.id, name: c.name }));
|
|
1629
|
+
},
|
|
1630
|
+
async getMenuItems(search, categoryId) {
|
|
1631
|
+
let sectionFilter = null;
|
|
1632
|
+
if (categoryId) {
|
|
1633
|
+
const placed = await db().from(PLACEMENTS).select("item_id").eq("section_id", categoryId).eq("is_active", true);
|
|
1634
|
+
if (!placed.error) {
|
|
1635
|
+
const placedIds = (placed.data ?? []).map((r) => r.item_id);
|
|
1636
|
+
if (placedIds.length === 0) return [];
|
|
1637
|
+
sectionFilter = placedIds;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
let q = db().from(CATALOG).select("id, name, price, category_id, image_url").eq("is_active", true).order("name").limit(60);
|
|
1641
|
+
if (search) q = q.ilike("name", `%${search}%`);
|
|
1642
|
+
if (sectionFilter) q = q.in("id", sectionFilter);
|
|
1643
|
+
else if (categoryId) q = q.eq("category_id", categoryId);
|
|
1644
|
+
const { data, error } = await q;
|
|
1645
|
+
if (error) throw new Error(error.message);
|
|
1646
|
+
const rows = data ?? [];
|
|
1647
|
+
if (rows.length === 0) return [];
|
|
1648
|
+
const ids = rows.map((r) => r.id);
|
|
1649
|
+
const placements = await db().from(PLACEMENTS).select("item_id, section_id").in("item_id", ids).eq("is_active", true);
|
|
1650
|
+
const sectionByProduct = /* @__PURE__ */ new Map();
|
|
1651
|
+
if (!placements.error && (placements.data ?? []).length > 0) {
|
|
1652
|
+
const sectionIds = [...new Set((placements.data ?? []).map((r) => r.section_id).filter(Boolean))];
|
|
1653
|
+
const { data: sections } = await db().from(SECTIONS).select("id, name").in("id", sectionIds);
|
|
1654
|
+
const nameById = new Map((sections ?? []).map((r) => [r.id, r.name]));
|
|
1655
|
+
for (const row of placements.data ?? []) {
|
|
1656
|
+
const name = nameById.get(row.section_id);
|
|
1657
|
+
if (name) sectionByProduct.set(row.item_id, name);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const categoryIds = [...new Set(rows.map((r) => r.category_id).filter(Boolean))];
|
|
1661
|
+
const { data: categories } = categoryIds.length ? await db().from("categories").select("id, name").in("id", categoryIds) : { data: [] };
|
|
1662
|
+
const categoryById = new Map((categories ?? []).map((c) => [c.id, c.name]));
|
|
1663
|
+
return rows.map((r) => ({
|
|
1664
|
+
id: r.id,
|
|
1665
|
+
name: r.name,
|
|
1666
|
+
price: num(r.price),
|
|
1667
|
+
categoryId: r.category_id ?? void 0,
|
|
1668
|
+
// The section wins where there is one: it is the word the Cardápio
|
|
1669
|
+
// screen shows, and the two lists have to say the same thing.
|
|
1670
|
+
categoryName: sectionByProduct.get(r.id) ?? (r.category_id ? categoryById.get(r.category_id) : void 0),
|
|
1671
|
+
imageUrl: r.image_url ?? void 0
|
|
1672
|
+
}));
|
|
1673
|
+
},
|
|
1674
|
+
async addOrderLine(orderId, input) {
|
|
1675
|
+
const tenantId = tenant();
|
|
1676
|
+
const { count } = await db().from(LINES).select("id", { count: "exact", head: true }).eq("order_id", orderId);
|
|
1677
|
+
const { data, error } = await db().from(LINES).insert({
|
|
1678
|
+
tenant_id: tenantId,
|
|
1679
|
+
order_id: orderId,
|
|
1680
|
+
product_id: input.productId ?? null,
|
|
1681
|
+
name: input.name,
|
|
1682
|
+
quantity: input.quantity,
|
|
1683
|
+
unit_price: input.unitPrice,
|
|
1684
|
+
discount: 0,
|
|
1685
|
+
total: input.unitPrice * input.quantity,
|
|
1686
|
+
sort_order: count ?? 0,
|
|
1687
|
+
metadata: input.notes ? { kitchenNotes: input.notes } : {}
|
|
1688
|
+
}).select("id").single();
|
|
1689
|
+
if (error) throw new Error(error.message);
|
|
1690
|
+
if (input.guestId && data?.id) {
|
|
1691
|
+
const { error: linkError } = await db().from(ITEM_GUESTS).insert({
|
|
1692
|
+
item_id: data.id,
|
|
1693
|
+
guest_id: input.guestId,
|
|
1694
|
+
tenant_id: tenantId
|
|
1695
|
+
});
|
|
1696
|
+
if (linkError) throw new Error(linkError.message);
|
|
1697
|
+
}
|
|
1698
|
+
await recalc(orderId);
|
|
1699
|
+
},
|
|
1700
|
+
async setOrderLineQuantity(lineId, quantity) {
|
|
1701
|
+
requireWrites();
|
|
1702
|
+
const { data: line, error } = await db().from(LINES).select("id, order_id, unit_price, discount").eq("id", lineId).maybeSingle();
|
|
1703
|
+
if (error) throw new Error(error.message);
|
|
1704
|
+
if (!line) throw new Error("Item n\xE3o encontrado na comanda");
|
|
1705
|
+
if (quantity <= 0) {
|
|
1706
|
+
const { error: delError } = await db().from(LINES).delete().eq("id", lineId);
|
|
1707
|
+
if (delError) throw new Error(delError.message);
|
|
1708
|
+
} else {
|
|
1709
|
+
const { error: upError } = await db().from(LINES).update({
|
|
1710
|
+
quantity,
|
|
1711
|
+
total: num(line.unit_price) * quantity - num(line.discount)
|
|
1712
|
+
}).eq("id", lineId);
|
|
1713
|
+
if (upError) throw new Error(upError.message);
|
|
1714
|
+
}
|
|
1715
|
+
await recalc(line.order_id);
|
|
1716
|
+
},
|
|
1717
|
+
async removeOrderLine(lineId) {
|
|
1718
|
+
requireWrites();
|
|
1719
|
+
const { data: line, error } = await db().from(LINES).select("order_id").eq("id", lineId).maybeSingle();
|
|
1720
|
+
if (error) throw new Error(error.message);
|
|
1721
|
+
if (!line) return;
|
|
1722
|
+
const { error: delError } = await db().from(LINES).delete().eq("id", lineId);
|
|
1723
|
+
if (delError) throw new Error(delError.message);
|
|
1724
|
+
await recalc(line.order_id);
|
|
1725
|
+
},
|
|
1726
|
+
// -- A política da casa ---------------------------------------------------
|
|
1727
|
+
async getSettings() {
|
|
1728
|
+
const { data, error } = await db().from(SETTINGS).select("service_charge_percent, cancel_requires_pin, operational_day_start").maybeSingle();
|
|
1729
|
+
if (error) throw new Error(error.message);
|
|
1730
|
+
return {
|
|
1731
|
+
serviceChargePercent: num(data?.service_charge_percent),
|
|
1732
|
+
cancelRequiresPin: data?.cancel_requires_pin === true,
|
|
1733
|
+
operationalDayStart: data?.operational_day_start ?? "06:00"
|
|
1734
|
+
};
|
|
1735
|
+
},
|
|
1736
|
+
async saveSettings(patch) {
|
|
1737
|
+
const tenantId = tenant();
|
|
1738
|
+
const row = { tenant_id: tenantId };
|
|
1739
|
+
if (patch.serviceChargePercent != null) row.service_charge_percent = patch.serviceChargePercent;
|
|
1740
|
+
if (patch.cancelRequiresPin != null) row.cancel_requires_pin = patch.cancelRequiresPin;
|
|
1741
|
+
if (patch.operationalDayStart != null) row.operational_day_start = patch.operationalDayStart;
|
|
1742
|
+
const { error } = await db().from(SETTINGS).upsert(row, { onConflict: "tenant_id" });
|
|
1743
|
+
if (error) throw new Error(error.message);
|
|
1744
|
+
},
|
|
1745
|
+
// -- Participantes --------------------------------------------------------
|
|
1746
|
+
async addGuest(orderId, label) {
|
|
1747
|
+
const tenantId = tenant();
|
|
1748
|
+
const { data: taken } = await db().from(GUESTS).select("seat_no").eq("order_id", orderId).order("seat_no");
|
|
1749
|
+
const used = new Set((taken ?? []).map((r) => Number(r.seat_no)));
|
|
1750
|
+
let seat = 1;
|
|
1751
|
+
while (used.has(seat)) seat += 1;
|
|
1752
|
+
const { data, error } = await db().from(GUESTS).insert({
|
|
1753
|
+
tenant_id: tenantId,
|
|
1754
|
+
order_id: orderId,
|
|
1755
|
+
seat_no: seat,
|
|
1756
|
+
label: label ?? null
|
|
1757
|
+
}).select("id, seat_no, label").single();
|
|
1758
|
+
if (error) throw new Error(error.message);
|
|
1759
|
+
return {
|
|
1760
|
+
id: data.id,
|
|
1761
|
+
seatNo: Number(data.seat_no),
|
|
1762
|
+
label: data.label ?? void 0,
|
|
1763
|
+
serviceChargePercent: 0,
|
|
1764
|
+
subtotal: 0,
|
|
1765
|
+
serviceCharge: 0,
|
|
1766
|
+
cancelledTotal: 0,
|
|
1767
|
+
itemCount: 0
|
|
1768
|
+
};
|
|
1769
|
+
},
|
|
1770
|
+
async removeGuest(guestId) {
|
|
1771
|
+
requireWrites();
|
|
1772
|
+
const { data: guest } = await db().from(GUESTS).select("order_id").eq("id", guestId).maybeSingle();
|
|
1773
|
+
const { error } = await db().from(GUESTS).delete().eq("id", guestId);
|
|
1774
|
+
if (error) throw new Error(error.message);
|
|
1775
|
+
if (guest?.order_id) await recalc(guest.order_id);
|
|
1776
|
+
},
|
|
1777
|
+
async setGuestServiceCharge(guestId, percent) {
|
|
1778
|
+
requireWrites();
|
|
1779
|
+
const { data: guest } = await db().from(GUESTS).select("order_id").eq("id", guestId).maybeSingle();
|
|
1780
|
+
const { error } = await db().from(GUESTS).update({ service_charge_percent: percent }).eq("id", guestId);
|
|
1781
|
+
if (error) throw new Error(error.message);
|
|
1782
|
+
if (guest?.order_id) await recalc(guest.order_id);
|
|
1783
|
+
},
|
|
1784
|
+
async setOrderServiceCharge(orderId, percent) {
|
|
1785
|
+
requireWrites();
|
|
1786
|
+
const { error } = await db().from(ORDER_EXT).update({ service_charge_percent: percent }).eq("order_id", orderId);
|
|
1787
|
+
if (error) throw new Error(error.message);
|
|
1788
|
+
await recalc(orderId);
|
|
1789
|
+
},
|
|
1790
|
+
// -- Desconto -------------------------------------------------------------
|
|
1791
|
+
async discountLines(orderId, input) {
|
|
1792
|
+
requireWrites();
|
|
1793
|
+
if (input.lineIds.length === 0) return;
|
|
1794
|
+
const { data: lines, error } = await db().from(LINES).select("id, quantity, unit_price").in("id", input.lineIds);
|
|
1795
|
+
if (error) throw new Error(error.message);
|
|
1796
|
+
for (const line of lines ?? []) {
|
|
1797
|
+
const gross = num(line.unit_price) * num(line.quantity);
|
|
1798
|
+
const raw = input.mode === "percent" ? gross * (input.value / 100) : input.value;
|
|
1799
|
+
const discount = Math.min(Math.max(raw, 0), gross);
|
|
1800
|
+
const { error: upError } = await db().from(LINES).update({ discount, total: gross - discount }).eq("id", line.id);
|
|
1801
|
+
if (upError) throw new Error(upError.message);
|
|
1802
|
+
}
|
|
1803
|
+
await recalc(orderId);
|
|
1804
|
+
},
|
|
1805
|
+
// -- Cancelamento ---------------------------------------------------------
|
|
1806
|
+
async getCancelReasons() {
|
|
1807
|
+
const { data, error } = await db().from(CANCEL_REASONS).select("id, label, is_active, sort_order").order("sort_order");
|
|
1808
|
+
if (error) throw new Error(error.message);
|
|
1809
|
+
return (data ?? []).map((r) => ({
|
|
1810
|
+
id: r.id,
|
|
1811
|
+
label: r.label,
|
|
1812
|
+
isActive: r.is_active !== false,
|
|
1813
|
+
sortOrder: Number(r.sort_order) || 0
|
|
1814
|
+
}));
|
|
1815
|
+
},
|
|
1816
|
+
async addCancelReason(label) {
|
|
1817
|
+
const tenantId = tenant();
|
|
1818
|
+
const { count } = await db().from(CANCEL_REASONS).select("id", { count: "exact", head: true });
|
|
1819
|
+
const { data, error } = await db().from(CANCEL_REASONS).insert({ tenant_id: tenantId, label, sort_order: count ?? 0 }).select("id, label, is_active, sort_order").single();
|
|
1820
|
+
if (error) throw new Error(error.message);
|
|
1821
|
+
return { id: data.id, label: data.label, isActive: data.is_active !== false, sortOrder: Number(data.sort_order) || 0 };
|
|
1822
|
+
},
|
|
1823
|
+
async updateCancelReason(id, patch) {
|
|
1824
|
+
requireWrites();
|
|
1825
|
+
const row = {};
|
|
1826
|
+
if (patch.label != null) row.label = patch.label;
|
|
1827
|
+
if (patch.isActive != null) row.is_active = patch.isActive;
|
|
1828
|
+
if (patch.sortOrder != null) row.sort_order = patch.sortOrder;
|
|
1829
|
+
const { error } = await db().from(CANCEL_REASONS).update(row).eq("id", id);
|
|
1830
|
+
if (error) throw new Error(error.message);
|
|
1831
|
+
},
|
|
1832
|
+
async deleteCancelReason(id) {
|
|
1833
|
+
requireWrites();
|
|
1834
|
+
const { error } = await db().from(CANCEL_REASONS).delete().eq("id", id);
|
|
1835
|
+
if (error) throw new Error(error.message);
|
|
1836
|
+
},
|
|
1837
|
+
async cancelLines(orderId, input) {
|
|
1838
|
+
requireWrites();
|
|
1839
|
+
if (input.lineIds.length === 0) return;
|
|
1840
|
+
const { error } = await db().from(LINES).update({ cancelled_at: (/* @__PURE__ */ new Date()).toISOString(), cancel_reason: input.reason }).in("id", input.lineIds);
|
|
1841
|
+
if (error) throw new Error(error.message);
|
|
1842
|
+
await recalc(orderId);
|
|
1843
|
+
},
|
|
1844
|
+
// -- Transferência --------------------------------------------------------
|
|
1845
|
+
async transfer(orderId, input) {
|
|
1846
|
+
const tenantId = tenant();
|
|
1847
|
+
const audit = {
|
|
1848
|
+
tenant_id: tenantId,
|
|
1849
|
+
order_id: orderId,
|
|
1850
|
+
kind: input.kind,
|
|
1851
|
+
note: input.note ?? null,
|
|
1852
|
+
item_ids: input.lineIds ?? []
|
|
1853
|
+
};
|
|
1854
|
+
if (input.kind === "table") {
|
|
1855
|
+
if (!input.toTableId) throw new Error("Escolha a mesa de destino");
|
|
1856
|
+
const { data: current } = await db().from(ORDER_EXT).select("table_id").eq("order_id", orderId).maybeSingle();
|
|
1857
|
+
const { data: busy } = await db().from(ORDER_EXT).select("order_id, orders!inner(status)").eq("table_id", input.toTableId).not("orders.status", "in", '("completed","cancelled")').limit(1);
|
|
1858
|
+
if ((busy ?? []).length > 0) throw new Error("A mesa de destino j\xE1 tem comanda aberta");
|
|
1859
|
+
const { error } = await db().from(ORDER_EXT).update({ table_id: input.toTableId }).eq("order_id", orderId);
|
|
1860
|
+
if (error) throw new Error(error.message);
|
|
1861
|
+
audit.from_table_id = current?.table_id ?? null;
|
|
1862
|
+
audit.to_table_id = input.toTableId;
|
|
1863
|
+
} else if (input.kind === "guest") {
|
|
1864
|
+
if (!input.guestId || !input.toOrderId) throw new Error("Escolha o participante e a comanda de destino");
|
|
1865
|
+
const { error } = await db().from(GUESTS).update({ order_id: input.toOrderId }).eq("id", input.guestId);
|
|
1866
|
+
if (error) throw new Error(error.message);
|
|
1867
|
+
const { data: moved } = await db().from(ITEM_GUESTS).select("item_id").eq("guest_id", input.guestId);
|
|
1868
|
+
const ids = (moved ?? []).map((r) => r.item_id);
|
|
1869
|
+
if (ids.length > 0) {
|
|
1870
|
+
const { error: itemError } = await db().from(LINES).update({ order_id: input.toOrderId }).in("id", ids);
|
|
1871
|
+
if (itemError) throw new Error(itemError.message);
|
|
1872
|
+
}
|
|
1873
|
+
audit.guest_id = input.guestId;
|
|
1874
|
+
audit.target_order_id = input.toOrderId;
|
|
1875
|
+
audit.item_ids = ids;
|
|
1876
|
+
await recalc(input.toOrderId);
|
|
1877
|
+
} else {
|
|
1878
|
+
if (!input.lineIds || input.lineIds.length === 0) throw new Error("Escolha os itens");
|
|
1879
|
+
const { error: delError } = await db().from(ITEM_GUESTS).delete().in("item_id", input.lineIds);
|
|
1880
|
+
if (delError) throw new Error(delError.message);
|
|
1881
|
+
if (input.toGuestId) {
|
|
1882
|
+
const { error: insError } = await db().from(ITEM_GUESTS).insert(
|
|
1883
|
+
input.lineIds.map((id) => ({ item_id: id, guest_id: input.toGuestId, tenant_id: tenantId }))
|
|
1884
|
+
);
|
|
1885
|
+
if (insError) throw new Error(insError.message);
|
|
1886
|
+
}
|
|
1887
|
+
audit.guest_id = input.toGuestId ?? null;
|
|
1888
|
+
}
|
|
1889
|
+
const { error: auditError } = await db().from(TRANSFERS).insert(audit);
|
|
1890
|
+
if (auditError) throw new Error(auditError.message);
|
|
1891
|
+
await recalc(orderId);
|
|
1892
|
+
},
|
|
1893
|
+
async getTransfers(orderId) {
|
|
1894
|
+
const { data, error } = await db().from(TRANSFERS).select("id, kind, from_table_id, to_table_id, guest_id, item_ids, note, created_at").eq("order_id", orderId).order("created_at", { ascending: false });
|
|
1895
|
+
if (error) throw new Error(error.message);
|
|
1896
|
+
const rows = data ?? [];
|
|
1897
|
+
const tableIds = Array.from(new Set(
|
|
1898
|
+
rows.flatMap((r) => [r.from_table_id, r.to_table_id]).filter(Boolean)
|
|
1899
|
+
));
|
|
1900
|
+
const { data: tables } = tableIds.length ? await db().from(TABLES).select("id, number").in("id", tableIds) : { data: [] };
|
|
1901
|
+
const numberById = new Map((tables ?? []).map((t) => [t.id, t.number]));
|
|
1902
|
+
const named = (id) => id && numberById.has(id) ? `Mesa ${numberById.get(id)}` : void 0;
|
|
1903
|
+
return rows.map((r) => ({
|
|
1904
|
+
id: r.id,
|
|
1905
|
+
kind: r.kind,
|
|
1906
|
+
fromTableName: named(r.from_table_id),
|
|
1907
|
+
toTableName: named(r.to_table_id),
|
|
1908
|
+
itemCount: (r.item_ids ?? []).length,
|
|
1909
|
+
note: r.note ?? void 0,
|
|
1910
|
+
createdAt: r.created_at
|
|
1911
|
+
}));
|
|
1912
|
+
},
|
|
1913
|
+
async updateSessionGuests(orderId, guests) {
|
|
1914
|
+
requireWrites();
|
|
1915
|
+
const { error } = await db().from(ORDER_EXT).update({ guests }).eq("order_id", orderId);
|
|
1916
|
+
if (error) throw new Error(error.message);
|
|
1917
|
+
},
|
|
1918
|
+
async getSummary() {
|
|
1919
|
+
const tables = await loadTables();
|
|
1920
|
+
const count = (status) => tables.filter((t) => t.status === status).length;
|
|
1921
|
+
const elapsed = tables.map((t) => t.currentElapsedMinutes).filter((m) => typeof m === "number");
|
|
1922
|
+
return {
|
|
1923
|
+
totalTables: tables.length,
|
|
1924
|
+
availableCount: count("available"),
|
|
1925
|
+
occupiedCount: count("occupied"),
|
|
1926
|
+
reservedCount: count("reserved"),
|
|
1927
|
+
cleaningCount: count("cleaning"),
|
|
1928
|
+
totalSeats: tables.reduce((sum, t) => sum + t.seats, 0),
|
|
1929
|
+
occupiedSeats: tables.filter((t) => t.status === "occupied").reduce((sum, t) => sum + t.seats, 0),
|
|
1930
|
+
averageSessionMinutes: elapsed.length ? Math.round(elapsed.reduce((a, b) => a + b, 0) / elapsed.length) : 0
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
755
1935
|
|
|
756
1936
|
// src/index.ts
|
|
757
|
-
var TablesPage = React.lazy(() => import('./TablesPage-
|
|
1937
|
+
var TablesPage = React.lazy(() => import('./TablesPage-VVDNK3GN.js').then((m) => ({ default: m.TablesPage })));
|
|
1938
|
+
var ComandaSettingsView = React.lazy(() => import('./ComandaSettingsView-ZB5RK7TP.js').then((m) => ({ default: m.ComandaSettingsView })));
|
|
758
1939
|
var DEFAULT_LABELS = {
|
|
759
1940
|
pageTitle: "Tables",
|
|
760
1941
|
floorPlan: "Floor Plan",
|
|
@@ -773,10 +1954,12 @@ function resolveConfig(options) {
|
|
|
773
1954
|
{ name: "Outdoor", color: "#22c55e" },
|
|
774
1955
|
{ name: "Bar", color: "#f59e0b" }
|
|
775
1956
|
],
|
|
1957
|
+
serviceChargePercent: options?.serviceChargePercent ?? 0,
|
|
776
1958
|
onTableSeated: options?.onTableSeated,
|
|
777
1959
|
onTableClosed: options?.onTableClosed
|
|
778
1960
|
};
|
|
779
1961
|
}
|
|
1962
|
+
var FLOOR_PLAN_COMPONENT_ID = "tables.floor-plan";
|
|
780
1963
|
function createTablesPlugin(options) {
|
|
781
1964
|
const config = resolveConfig(options);
|
|
782
1965
|
const provider = options?.dataProvider ?? createMockTablesProvider();
|
|
@@ -786,6 +1969,9 @@ function createTablesPlugin(options) {
|
|
|
786
1969
|
{ fallback: null },
|
|
787
1970
|
React.createElement(TablesPage, { config, provider, store, registries: tablesRegistries })
|
|
788
1971
|
);
|
|
1972
|
+
registerComponent(FLOOR_PLAN_COMPONENT_ID, PageComponent);
|
|
1973
|
+
const ContextBoundary = ({ children }) => React.createElement(TablesContextProvider, { config, provider, store }, children);
|
|
1974
|
+
ContextBoundary.displayName = "TablesContextBoundary";
|
|
789
1975
|
return {
|
|
790
1976
|
id: "tables",
|
|
791
1977
|
defaultAgentRole: "operations",
|
|
@@ -869,12 +2055,39 @@ function createTablesPlugin(options) {
|
|
|
869
2055
|
})(),
|
|
870
2056
|
order: 21,
|
|
871
2057
|
permission: { feature: "tables", action: "read" }
|
|
2058
|
+
},
|
|
2059
|
+
// Duas abas, e cada uma responde uma pergunta diferente: a de cima é o
|
|
2060
|
+
// CADASTRO do salão (praças, mesas — trabalho de implantação), esta é a
|
|
2061
|
+
// POLÍTICA da comanda (taxa, cancelamento, dia operacional — decisões do
|
|
2062
|
+
// dono). Juntá-las daria uma tela em que quem vem mudar a taxa passa
|
|
2063
|
+
// primeiro pelas mesas.
|
|
2064
|
+
{
|
|
2065
|
+
id: "comanda",
|
|
2066
|
+
label: "Comanda",
|
|
2067
|
+
icon: "Receipt",
|
|
2068
|
+
component: (() => {
|
|
2069
|
+
const ComandaSettingsTab = () => React.createElement(
|
|
2070
|
+
ContextBoundary,
|
|
2071
|
+
null,
|
|
2072
|
+
React.createElement(
|
|
2073
|
+
React.Suspense,
|
|
2074
|
+
{ fallback: null },
|
|
2075
|
+
React.createElement(ComandaSettingsView)
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
ComandaSettingsTab.displayName = "ComandaSettingsTab";
|
|
2079
|
+
return ComandaSettingsTab;
|
|
2080
|
+
})(),
|
|
2081
|
+
order: 22,
|
|
2082
|
+
// A política é do dono, não do garçom: quem só atende mesa não decide a
|
|
2083
|
+
// taxa da casa nem apaga motivo de cancelamento.
|
|
2084
|
+
permission: { feature: "tables", action: "edit" }
|
|
872
2085
|
}
|
|
873
2086
|
],
|
|
874
2087
|
locales: tablesLocales
|
|
875
2088
|
};
|
|
876
2089
|
}
|
|
877
2090
|
|
|
878
|
-
export { createFayzTablesProvider, createTablesPlugin };
|
|
2091
|
+
export { FLOOR_PLAN_COMPONENT_ID, createCoreTablesProvider, createFayzTablesProvider, createTablesPlugin };
|
|
879
2092
|
//# sourceMappingURL=index.js.map
|
|
880
2093
|
//# sourceMappingURL=index.js.map
|