Skip to content

Commit 10d5183

Browse files
authored
feat(admin): show floor totals and check badges (#25) (#31)
1 parent ba73241 commit 10d5183

17 files changed

Lines changed: 102 additions & 17 deletions

File tree

backend/src/__tests__/staff.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,12 +530,21 @@ describe('/admin/floor payload (#15)', () => {
530530

531531
const res = await testRequest('/admin/floor', { headers: await adminHeaders(), env: adminEnv(db) });
532532
expect(res.status).toBe(200);
533-
const body = (await res.json()) as { areas: Array<{ id: string }>; tables: Array<{ id: string; active: boolean; sessionId: string | null; oldestSubmittedAt: number | null }> };
533+
const body = (await res.json()) as { areas: Array<{ id: string }>; tables: Array<{ id: string; active: boolean; sessionId: string | null; oldestSubmittedAt: number | null; provisionalTotal: number }> };
534534
expect(body.areas).toEqual([{ id: 'area-1', name: 'Sala', sortOrder: 0 }]);
535535
const row = body.tables.find((t) => t.id === 'table-1')!;
536536
expect(row.sessionId).toBe(sessionId);
537537
expect(row.oldestSubmittedAt).toEqual(expect.any(Number));
538538
expect(row.active).toBe(true);
539+
expect(row.provisionalTotal).toBe(750);
540+
541+
const checkRes = await testRequest(`/admin/sessions/${sessionId}/check`, { method: 'POST', headers: await adminHeaders(), env: adminEnv(db) });
542+
expect(checkRes.status).toBe(201);
543+
const checkedRes = await testRequest('/admin/floor', { headers: await adminHeaders(), env: adminEnv(db) });
544+
const checkedBody = (await checkedRes.json()) as { tables: Array<{ id: string; checkStatus: string | null; checkTotal: number | null }> };
545+
const checkedRow = checkedBody.tables.find((t) => t.id === 'table-1')!;
546+
expect(checkedRow.checkStatus).toBe('open');
547+
expect(checkedRow.checkTotal).toBe(750);
539548
});
540549

541550
it('includes inactive tables (admin) while /staff/floor excludes them', async () => {

backend/src/lib/floor.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,51 @@ export async function buildFloorState(db: DbInstance, includeInactive: boolean)
3434
const sessionIds = openSessions.map((s) => s.id);
3535
const orderRows = sessionIds.length > 0
3636
? await db
37-
.select({ tableSessionId: schema.orders.tableSessionId, status: schema.orders.status, createdAt: schema.orders.createdAt })
37+
.select({ id: schema.orders.id, tableSessionId: schema.orders.tableSessionId, status: schema.orders.status, createdAt: schema.orders.createdAt })
3838
.from(schema.orders)
3939
.where(inArray(schema.orders.tableSessionId, sessionIds))
4040
: [];
4141

42-
const counts = new Map<string, { orderCount: number; readyCount: number; oldestSubmittedAt: number | null }>();
42+
const orderIds = orderRows.map((o) => o.id);
43+
const itemRows = includeInactive && orderIds.length > 0
44+
? await db
45+
.select({ orderId: schema.orderItems.orderId, price: schema.orderItems.price, quantity: schema.orderItems.quantity })
46+
.from(schema.orderItems)
47+
.where(inArray(schema.orderItems.orderId, orderIds))
48+
: [];
49+
const orderTotal = new Map<string, number>();
50+
for (const item of itemRows) orderTotal.set(item.orderId, (orderTotal.get(item.orderId) ?? 0) + item.price * item.quantity);
51+
52+
const checks = includeInactive && sessionIds.length > 0
53+
? await db
54+
.select()
55+
.from(schema.checks)
56+
.where(inArray(schema.checks.tableSessionId, sessionIds))
57+
: [];
58+
const checkBySession = new Map<string, typeof checks[number]>();
59+
for (const check of checks) {
60+
const prev = checkBySession.get(check.tableSessionId);
61+
if (!prev || check.createdAt > prev.createdAt) checkBySession.set(check.tableSessionId, check);
62+
}
63+
64+
const checkTotal = (check: typeof checks[number]): number => {
65+
const lines = check.lines ?? [];
66+
const adjustments = check.adjustments ?? [];
67+
const subtotal = lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0);
68+
const discount = check.discount
69+
? check.discount.type === 'percent' ? Math.round(subtotal * check.discount.value / 100) : check.discount.value
70+
: 0;
71+
const adjusted = subtotal - discount + adjustments.reduce((sum, a) => sum + a.amount, 0);
72+
return Math.max(0, adjusted);
73+
};
74+
75+
const counts = new Map<string, { orderCount: number; readyCount: number; oldestSubmittedAt: number | null; provisionalTotal: number }>();
4376
for (const o of orderRows) {
4477
if (!o.tableSessionId) continue;
45-
const c = counts.get(o.tableSessionId) ?? { orderCount: 0, readyCount: 0, oldestSubmittedAt: null };
78+
const c = counts.get(o.tableSessionId) ?? { orderCount: 0, readyCount: 0, oldestSubmittedAt: null, provisionalTotal: 0 };
4679
// "Open orders" = anything not served/rejected; "ready" = ready to serve.
4780
if (o.status === 'submitted' || o.status === 'ready') c.orderCount++;
81+
if (o.status !== 'rejected') c.provisionalTotal += orderTotal.get(o.id) ?? 0;
4882
if (o.status === 'ready') c.readyCount++;
4983
if (o.status === 'submitted' && (c.oldestSubmittedAt === null || o.createdAt < c.oldestSubmittedAt)) {
5084
c.oldestSubmittedAt = o.createdAt;
@@ -69,6 +103,11 @@ export async function buildFloorState(db: DbInstance, includeInactive: boolean)
69103
x: t.x,
70104
y: t.y,
71105
shape: t.shape,
106+
...(includeInactive ? {
107+
provisionalTotal: count?.provisionalTotal ?? 0,
108+
checkStatus: session ? checkBySession.get(session.id)?.status ?? null : null,
109+
checkTotal: session && checkBySession.has(session.id) ? checkTotal(checkBySession.get(session.id)!) : null,
110+
} : {}),
72111
oldestSubmittedAt: count?.oldestSubmittedAt ?? null,
73112
};
74113
}),

packages/schemas/src/staff.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export interface FloorTable {
111111
/** Admin floor state: a FloorTable that also carries the active flag (inactive tables show dimmed). */
112112
export interface AdminFloorTable extends FloorTable {
113113
active: boolean;
114+
provisionalTotal: number;
115+
checkStatus: 'open' | 'settled' | 'voided' | null;
116+
checkTotal: number | null;
114117
}
115118

116119
export interface StaffOrderItem {

web/e2e/admin/admin-checks-real.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ test.describe.serial("Checks (conto) — real backend", () => {
4242
await page.goto("/admin/tables/");
4343
const tile = page.getByTestId(`table-${SEEDED_TABLE.id}`);
4444
await expect(tile).toBeVisible({ timeout: 10000 });
45+
await expect(page.getByTestId(`table-total-${SEEDED_TABLE.id}`)).toHaveText(/15,00/);
4546
await tile.click();
4647
await expect(page).toHaveURL(new RegExp(`/admin/tables/detail/?\\?tableId=${SEEDED_TABLE.id}`));
4748

@@ -55,6 +56,12 @@ test.describe.serial("Checks (conto) — real backend", () => {
5556
await expect(page.getByTestId("check-card")).toBeVisible();
5657
await expect(page.getByTestId("add-items-blocked")).toContainText(/paga o annulla|settle or void/i);
5758
await expect(page.getByTestId("add-order")).toHaveCount(0);
59+
60+
await page.goto("/admin/tables/");
61+
await expect(page.getByTestId(`table-check-${SEEDED_TABLE.id}`)).toHaveText(/conto|check/i);
62+
await expect(page.getByTestId(`table-total-${SEEDED_TABLE.id}`)).toHaveText(/15,00/);
63+
await page.getByTestId(`table-${SEEDED_TABLE.id}`).click();
64+
await expect(page).toHaveURL(new RegExp(`/admin/tables/detail/?\\?tableId=${SEEDED_TABLE.id}`));
5865
await expect(page.getByTestId("check-total")).toHaveText(/15,00/);
5966

6067
// Apply a 10% discount; total updates to 13,50.
@@ -72,7 +79,7 @@ test.describe.serial("Checks (conto) — real backend", () => {
7279
await settleRes;
7380

7481
await expect(page.getByTestId("table-free")).toBeVisible();
75-
await expect(page.getByText("Pagato").first()).toBeVisible();
82+
await expect(page.getByText(/Pagato|Paid/).first()).toBeVisible();
7683
});
7784
test("admin adds searchable items from the table page", async ({ page, request }) => {
7885
await seedSessionWithOrder(request);

web/messages/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Alle verfügbaren Artikel suchen",
662662
"tableDetail.noAvailableItems": "Keine verfügbaren Artikel gefunden.",
663663
"tableDetail.submitItems": "Artikel hinzufügen",
664-
"tableDetail.checkOpenAddItemsBlocked": "Begleiche oder storniere die offene Rechnung, bevor du weitere Artikel hinzufügst."
664+
"tableDetail.checkOpenAddItemsBlocked": "Begleiche oder storniere die offene Rechnung, bevor du weitere Artikel hinzufügst.",
665+
"tables.checkOpen": "Rechnung"
665666
},
666667
"outOfStock": "AUSVERKAUFT",
667668
"staff": {

web/messages/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Search all available items",
662662
"tableDetail.noAvailableItems": "No available items found.",
663663
"tableDetail.submitItems": "Add items",
664-
"tableDetail.checkOpenAddItemsBlocked": "Settle or void the open check before adding more items."
664+
"tableDetail.checkOpenAddItemsBlocked": "Settle or void the open check before adding more items.",
665+
"tables.checkOpen": "Check"
665666
},
666667
"outOfStock": "OUT OF STOCK",
667668
"staff": {

web/messages/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Buscar artículos disponibles",
662662
"tableDetail.noAvailableItems": "No se encontraron artículos disponibles.",
663663
"tableDetail.submitItems": "Añadir artículos",
664-
"tableDetail.checkOpenAddItemsBlocked": "Cobra o anula la cuenta abierta antes de añadir más artículos."
664+
"tableDetail.checkOpenAddItemsBlocked": "Cobra o anula la cuenta abierta antes de añadir más artículos.",
665+
"tables.checkOpen": "Cuenta"
665666
},
666667
"outOfStock": "AGOTADO",
667668
"staff": {

web/messages/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Rechercher les articles disponibles",
662662
"tableDetail.noAvailableItems": "Aucun article disponible trouvé.",
663663
"tableDetail.submitItems": "Ajouter des articles",
664-
"tableDetail.checkOpenAddItemsBlocked": "Encaissez ou annulez l’addition ouverte avant d’ajouter d’autres articles."
664+
"tableDetail.checkOpenAddItemsBlocked": "Encaissez ou annulez l’addition ouverte avant d’ajouter d’autres articles.",
665+
"tables.checkOpen": "Note"
665666
},
666667
"outOfStock": "ÉPUISÉ",
667668
"staff": {

web/messages/hu.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Elérhető tételek keresése",
662662
"tableDetail.noAvailableItems": "Nem található elérhető tétel.",
663663
"tableDetail.submitItems": "Tételek hozzáadása",
664-
"tableDetail.checkOpenAddItemsBlocked": "Rendezze vagy érvénytelenítse a nyitott számlát, mielőtt új tételeket ad hozzá."
664+
"tableDetail.checkOpenAddItemsBlocked": "Rendezze vagy érvénytelenítse a nyitott számlát, mielőtt új tételeket ad hozzá.",
665+
"tables.checkOpen": "Számla"
665666
},
666667
"outOfStock": "OUT OF STOCK",
667668
"staff": {

web/messages/it.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@
661661
"tableDetail.searchItems": "Cerca tutti gli articoli disponibili",
662662
"tableDetail.noAvailableItems": "Nessun articolo disponibile trovato.",
663663
"tableDetail.submitItems": "Aggiungi articoli",
664-
"tableDetail.checkOpenAddItemsBlocked": "Paga o annulla il conto aperto prima di aggiungere altri articoli."
664+
"tableDetail.checkOpenAddItemsBlocked": "Paga o annulla il conto aperto prima di aggiungere altri articoli.",
665+
"tables.checkOpen": "Conto"
665666
},
666667
"outOfStock": "ESAURITO",
667668
"staff": {

0 commit comments

Comments
 (0)