Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion backend/src/__tests__/staff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,12 +530,21 @@ describe('/admin/floor payload (#15)', () => {

const res = await testRequest('/admin/floor', { headers: await adminHeaders(), env: adminEnv(db) });
expect(res.status).toBe(200);
const body = (await res.json()) as { areas: Array<{ id: string }>; tables: Array<{ id: string; active: boolean; sessionId: string | null; oldestSubmittedAt: number | null }> };
const body = (await res.json()) as { areas: Array<{ id: string }>; tables: Array<{ id: string; active: boolean; sessionId: string | null; oldestSubmittedAt: number | null; provisionalTotal: number }> };
expect(body.areas).toEqual([{ id: 'area-1', name: 'Sala', sortOrder: 0 }]);
const row = body.tables.find((t) => t.id === 'table-1')!;
expect(row.sessionId).toBe(sessionId);
expect(row.oldestSubmittedAt).toEqual(expect.any(Number));
expect(row.active).toBe(true);
expect(row.provisionalTotal).toBe(750);

const checkRes = await testRequest(`/admin/sessions/${sessionId}/check`, { method: 'POST', headers: await adminHeaders(), env: adminEnv(db) });
expect(checkRes.status).toBe(201);
const checkedRes = await testRequest('/admin/floor', { headers: await adminHeaders(), env: adminEnv(db) });
const checkedBody = (await checkedRes.json()) as { tables: Array<{ id: string; checkStatus: string | null; checkTotal: number | null }> };
const checkedRow = checkedBody.tables.find((t) => t.id === 'table-1')!;
expect(checkedRow.checkStatus).toBe('open');
expect(checkedRow.checkTotal).toBe(750);
});

it('includes inactive tables (admin) while /staff/floor excludes them', async () => {
Expand Down
45 changes: 42 additions & 3 deletions backend/src/lib/floor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,51 @@ export async function buildFloorState(db: DbInstance, includeInactive: boolean)
const sessionIds = openSessions.map((s) => s.id);
const orderRows = sessionIds.length > 0
? await db
.select({ tableSessionId: schema.orders.tableSessionId, status: schema.orders.status, createdAt: schema.orders.createdAt })
.select({ id: schema.orders.id, tableSessionId: schema.orders.tableSessionId, status: schema.orders.status, createdAt: schema.orders.createdAt })
.from(schema.orders)
.where(inArray(schema.orders.tableSessionId, sessionIds))
: [];

const counts = new Map<string, { orderCount: number; readyCount: number; oldestSubmittedAt: number | null }>();
const orderIds = orderRows.map((o) => o.id);
const itemRows = includeInactive && orderIds.length > 0
? await db
.select({ orderId: schema.orderItems.orderId, price: schema.orderItems.price, quantity: schema.orderItems.quantity })
.from(schema.orderItems)
.where(inArray(schema.orderItems.orderId, orderIds))
: [];
const orderTotal = new Map<string, number>();
for (const item of itemRows) orderTotal.set(item.orderId, (orderTotal.get(item.orderId) ?? 0) + item.price * item.quantity);

const checks = includeInactive && sessionIds.length > 0
? await db
.select()
.from(schema.checks)
.where(inArray(schema.checks.tableSessionId, sessionIds))
: [];
const checkBySession = new Map<string, typeof checks[number]>();
for (const check of checks) {
const prev = checkBySession.get(check.tableSessionId);
if (!prev || check.createdAt > prev.createdAt) checkBySession.set(check.tableSessionId, check);
}

const checkTotal = (check: typeof checks[number]): number => {
const lines = check.lines ?? [];
const adjustments = check.adjustments ?? [];
const subtotal = lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0);
const discount = check.discount
? check.discount.type === 'percent' ? Math.round(subtotal * check.discount.value / 100) : check.discount.value
: 0;
const adjusted = subtotal - discount + adjustments.reduce((sum, a) => sum + a.amount, 0);
return Math.max(0, adjusted);
};

const counts = new Map<string, { orderCount: number; readyCount: number; oldestSubmittedAt: number | null; provisionalTotal: number }>();
for (const o of orderRows) {
if (!o.tableSessionId) continue;
const c = counts.get(o.tableSessionId) ?? { orderCount: 0, readyCount: 0, oldestSubmittedAt: null };
const c = counts.get(o.tableSessionId) ?? { orderCount: 0, readyCount: 0, oldestSubmittedAt: null, provisionalTotal: 0 };
// "Open orders" = anything not served/rejected; "ready" = ready to serve.
if (o.status === 'submitted' || o.status === 'ready') c.orderCount++;
if (o.status !== 'rejected') c.provisionalTotal += orderTotal.get(o.id) ?? 0;
if (o.status === 'ready') c.readyCount++;
if (o.status === 'submitted' && (c.oldestSubmittedAt === null || o.createdAt < c.oldestSubmittedAt)) {
c.oldestSubmittedAt = o.createdAt;
Expand All @@ -69,6 +103,11 @@ export async function buildFloorState(db: DbInstance, includeInactive: boolean)
x: t.x,
y: t.y,
shape: t.shape,
...(includeInactive ? {
provisionalTotal: count?.provisionalTotal ?? 0,
checkStatus: session ? checkBySession.get(session.id)?.status ?? null : null,
checkTotal: session && checkBySession.has(session.id) ? checkTotal(checkBySession.get(session.id)!) : null,
} : {}),
oldestSubmittedAt: count?.oldestSubmittedAt ?? null,
};
}),
Expand Down
3 changes: 3 additions & 0 deletions packages/schemas/src/staff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export interface FloorTable {
/** Admin floor state: a FloorTable that also carries the active flag (inactive tables show dimmed). */
export interface AdminFloorTable extends FloorTable {
active: boolean;
provisionalTotal: number;
checkStatus: 'open' | 'settled' | 'voided' | null;
checkTotal: number | null;
}

export interface StaffOrderItem {
Expand Down
9 changes: 8 additions & 1 deletion web/e2e/admin/admin-checks-real.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ test.describe.serial("Checks (conto) — real backend", () => {
await page.goto("/admin/tables/");
const tile = page.getByTestId(`table-${SEEDED_TABLE.id}`);
await expect(tile).toBeVisible({ timeout: 10000 });
await expect(page.getByTestId(`table-total-${SEEDED_TABLE.id}`)).toHaveText(/15,00/);
await tile.click();
await expect(page).toHaveURL(new RegExp(`/admin/tables/detail/?\\?tableId=${SEEDED_TABLE.id}`));

Expand All @@ -55,6 +56,12 @@ test.describe.serial("Checks (conto) — real backend", () => {
await expect(page.getByTestId("check-card")).toBeVisible();
await expect(page.getByTestId("add-items-blocked")).toContainText(/paga o annulla|settle or void/i);
await expect(page.getByTestId("add-order")).toHaveCount(0);

await page.goto("/admin/tables/");
await expect(page.getByTestId(`table-check-${SEEDED_TABLE.id}`)).toHaveText(/conto|check/i);
await expect(page.getByTestId(`table-total-${SEEDED_TABLE.id}`)).toHaveText(/15,00/);
await page.getByTestId(`table-${SEEDED_TABLE.id}`).click();
await expect(page).toHaveURL(new RegExp(`/admin/tables/detail/?\\?tableId=${SEEDED_TABLE.id}`));
await expect(page.getByTestId("check-total")).toHaveText(/15,00/);

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

await expect(page.getByTestId("table-free")).toBeVisible();
await expect(page.getByText("Pagato").first()).toBeVisible();
await expect(page.getByText(/Pagato|Paid/).first()).toBeVisible();
});
test("admin adds searchable items from the table page", async ({ page, request }) => {
await seedSessionWithOrder(request);
Expand Down
3 changes: 2 additions & 1 deletion web/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Alle verfügbaren Artikel suchen",
"tableDetail.noAvailableItems": "Keine verfügbaren Artikel gefunden.",
"tableDetail.submitItems": "Artikel hinzufügen",
"tableDetail.checkOpenAddItemsBlocked": "Begleiche oder storniere die offene Rechnung, bevor du weitere Artikel hinzufügst."
"tableDetail.checkOpenAddItemsBlocked": "Begleiche oder storniere die offene Rechnung, bevor du weitere Artikel hinzufügst.",
"tables.checkOpen": "Rechnung"
},
"outOfStock": "AUSVERKAUFT",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Search all available items",
"tableDetail.noAvailableItems": "No available items found.",
"tableDetail.submitItems": "Add items",
"tableDetail.checkOpenAddItemsBlocked": "Settle or void the open check before adding more items."
"tableDetail.checkOpenAddItemsBlocked": "Settle or void the open check before adding more items.",
"tables.checkOpen": "Check"
},
"outOfStock": "OUT OF STOCK",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Buscar artículos disponibles",
"tableDetail.noAvailableItems": "No se encontraron artículos disponibles.",
"tableDetail.submitItems": "Añadir artículos",
"tableDetail.checkOpenAddItemsBlocked": "Cobra o anula la cuenta abierta antes de añadir más artículos."
"tableDetail.checkOpenAddItemsBlocked": "Cobra o anula la cuenta abierta antes de añadir más artículos.",
"tables.checkOpen": "Cuenta"
},
"outOfStock": "AGOTADO",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Rechercher les articles disponibles",
"tableDetail.noAvailableItems": "Aucun article disponible trouvé.",
"tableDetail.submitItems": "Ajouter des articles",
"tableDetail.checkOpenAddItemsBlocked": "Encaissez ou annulez l’addition ouverte avant d’ajouter d’autres articles."
"tableDetail.checkOpenAddItemsBlocked": "Encaissez ou annulez l’addition ouverte avant d’ajouter d’autres articles.",
"tables.checkOpen": "Note"
},
"outOfStock": "ÉPUISÉ",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Elérhető tételek keresése",
"tableDetail.noAvailableItems": "Nem található elérhető tétel.",
"tableDetail.submitItems": "Tételek hozzáadása",
"tableDetail.checkOpenAddItemsBlocked": "Rendezze vagy érvénytelenítse a nyitott számlát, mielőtt új tételeket ad hozzá."
"tableDetail.checkOpenAddItemsBlocked": "Rendezze vagy érvénytelenítse a nyitott számlát, mielőtt új tételeket ad hozzá.",
"tables.checkOpen": "Számla"
},
"outOfStock": "OUT OF STOCK",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Cerca tutti gli articoli disponibili",
"tableDetail.noAvailableItems": "Nessun articolo disponibile trovato.",
"tableDetail.submitItems": "Aggiungi articoli",
"tableDetail.checkOpenAddItemsBlocked": "Paga o annulla il conto aperto prima di aggiungere altri articoli."
"tableDetail.checkOpenAddItemsBlocked": "Paga o annulla il conto aperto prima di aggiungere altri articoli.",
"tables.checkOpen": "Conto"
},
"outOfStock": "ESAURITO",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Zoek beschikbare items",
"tableDetail.noAvailableItems": "Geen beschikbare items gevonden.",
"tableDetail.submitItems": "Items toevoegen",
"tableDetail.checkOpenAddItemsBlocked": "Reken de open bon af of annuleer deze voordat je meer items toevoegt."
"tableDetail.checkOpenAddItemsBlocked": "Reken de open bon af of annuleer deze voordat je meer items toevoegt.",
"tables.checkOpen": "Rekening"
},
"outOfStock": "UITVERKOCHT",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Pesquisar itens disponíveis",
"tableDetail.noAvailableItems": "Nenhum item disponível encontrado.",
"tableDetail.submitItems": "Adicionar itens",
"tableDetail.checkOpenAddItemsBlocked": "Liquide ou anule a conta aberta antes de adicionar mais itens."
"tableDetail.checkOpenAddItemsBlocked": "Liquide ou anule a conta aberta antes de adicionar mais itens.",
"tables.checkOpen": "Conta"
},
"outOfStock": "ESGOTADO",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Поиск доступных позиций",
"tableDetail.noAvailableItems": "Доступные позиции не найдены.",
"tableDetail.submitItems": "Добавить позиции",
"tableDetail.checkOpenAddItemsBlocked": "Оплатите или аннулируйте открытый счёт перед добавлением новых позиций."
"tableDetail.checkOpenAddItemsBlocked": "Оплатите или аннулируйте открытый счёт перед добавлением новых позиций.",
"tables.checkOpen": "Счет"
},
"outOfStock": "НЕТ В НАЛИЧИИ",
"staff": {
Expand Down
3 changes: 2 additions & 1 deletion web/messages/vec.json
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@
"tableDetail.searchItems": "Serca tute łe voci disponìbiłi",
"tableDetail.noAvailableItems": "Nisuna vose disponìbiłe catada.",
"tableDetail.submitItems": "Zonta voci",
"tableDetail.checkOpenAddItemsBlocked": "Paga o anuła el conto verto prima de zontar altre voci."
"tableDetail.checkOpenAddItemsBlocked": "Paga o anuła el conto verto prima de zontar altre voci.",
"tables.checkOpen": "Conto"
},
"outOfStock": "FINIO",
"staff": {
Expand Down
13 changes: 11 additions & 2 deletions web/src/components/admin/pages/TablesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const areas = [
const base = { openedAt: null, orderCount: 0, readyCount: 0 };
const tables = [
// t1: green (open, no submitted); t2 lives in another area.
{ id: "t1", name: "1", active: true, areaId: "a1", x: 50, y: 50, shape: "rect" as const, sessionId: "s1", oldestSubmittedAt: null, ...base },
{ id: "t2", name: "2", active: true, areaId: "a2", x: 50, y: 50, shape: "circle" as const, sessionId: null, oldestSubmittedAt: null, ...base },
{ id: "t1", name: "1", active: true, areaId: "a1", x: 50, y: 50, shape: "rect" as const, sessionId: "s1", oldestSubmittedAt: null, provisionalTotal: 1500, checkStatus: null, checkTotal: null, ...base },
{ id: "t2", name: "2", active: true, areaId: "a2", x: 50, y: 50, shape: "circle" as const, sessionId: null, oldestSubmittedAt: null, provisionalTotal: 0, checkStatus: null, checkTotal: null, ...base },
];

beforeEach(() => {
Expand All @@ -55,6 +55,7 @@ describe("TablesPage", () => {
expect(tile).toBeInTheDocument();
expect(tile.className).toContain("bg-green-100");
expect(screen.queryByTestId("table-t2")).toBeNull();
expect(screen.getByTestId("table-total-t1")).toHaveTextContent("15,00");
});

it("switches the visible tables when another area tab is clicked", async () => {
Expand All @@ -72,6 +73,14 @@ describe("TablesPage", () => {
expect(routerPush).toHaveBeenCalledWith("/admin/tables/detail?tableId=t1");
});

it("shows an open-check badge and check total on admin floor tiles", async () => {
apiMocks.fetchAdminFloor.mockResolvedValue({ areas, tables: [{ ...tables[0], checkStatus: "open", checkTotal: 1350 }] });
render(<TablesPage />);
await screen.findByTestId("area-tab-a1");
expect(screen.getByTestId("table-total-t1")).toHaveTextContent("13,50");
expect(screen.getByTestId("table-check-t1")).toHaveTextContent("tables.checkOpen");
});

it("hides the edit UI by default and reveals it via the toggle", async () => {
render(<TablesPage />);
await screen.findByTestId("area-tab-a1");
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/admin/pages/TablesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FloorCanvas, type FloorTile } from "@/components/floor/FloorCanvas";
import type { TableShape } from "@menu/schemas";

const POLL_MS = 10_000;
const money = (cents: number) => (cents / 100).toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 });

/**
* Admin tables (#15): canvas-first, colour-coded like the staff floor. Default mode
Expand Down Expand Up @@ -101,6 +102,7 @@ export default function TablesPage() {
setTables((prev) => [...prev, {
id: res.id, name: trimmed, active: true, areaId: activeArea, x: 25, y: 25, shape: tableShape,
sessionId: null, openedAt: null, orderCount: 0, readyCount: 0, oldestSubmittedAt: null,
provisionalTotal: 0, checkStatus: null, checkTotal: null,
}]);
setTableName("");
} catch (err) {
Expand Down Expand Up @@ -167,6 +169,8 @@ export default function TablesPage() {
const tiles: FloorTile[] = areaTables.map((tb) => ({
id: tb.id, name: tb.name, x: tb.x, y: tb.y, shape: tb.shape, active: tb.active,
sessionId: tb.sessionId, oldestSubmittedAt: tb.oldestSubmittedAt,
totalLabel: tb.checkTotal != null ? money(tb.checkTotal) : tb.provisionalTotal > 0 ? `≈${money(tb.provisionalTotal)}` : null,
checkLabel: tb.checkStatus === "open" ? t("tables.checkOpen") : null,
}));
const selected = areaTables.find((tb) => tb.id === selectedId) ?? null;

Expand Down
4 changes: 4 additions & 0 deletions web/src/components/floor/FloorCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type FloorTile = {
active?: boolean; // admin: dim inactive tables
sessionId?: string | null; // staff: open session state
oldestSubmittedAt?: number | null; // staff: oldest submitted order timestamp
totalLabel?: string | null;
checkLabel?: string | null;
};

type Visual = { color: "gray" | "green" | "amber" | "red"; minutes: number | null };
Expand Down Expand Up @@ -142,6 +144,8 @@ export function FloorCanvas({ tiles, editable, now = Date.now(), busyId, pannabl
{visual?.minutes != null && (
<span data-testid={`table-mins-${tile.id}`} className="text-xs font-semibold">{visual.minutes}m</span>
)}
{tile.totalLabel && <span data-testid={`table-total-${tile.id}`} className="text-[11px] font-semibold">{tile.totalLabel}</span>}
{tile.checkLabel && <span data-testid={`table-check-${tile.id}`} className="text-[10px] rounded-full bg-white/70 px-1.5 font-semibold">{tile.checkLabel}</span>}
</button>
);
})}
Expand Down
Loading