diff --git a/app/api/display-config/route.ts b/app/api/display-config/route.ts
index 29b22d5..8697cac 100644
--- a/app/api/display-config/route.ts
+++ b/app/api/display-config/route.ts
@@ -25,6 +25,12 @@ export async function PATCH(request: Request) {
if (typeof body.fullscreenAlertEnabled === "boolean") {
patch.fullscreenAlertEnabled = body.fullscreenAlertEnabled;
}
+ if (typeof body.autoScrollPagesEnabled === "boolean") {
+ patch.autoScrollPagesEnabled = body.autoScrollPagesEnabled;
+ }
+ if (typeof body.displayZoom === "number" && body.displayZoom >= 50 && body.displayZoom <= 200) {
+ patch.displayZoom = Math.round(body.displayZoom);
+ }
const updated = updateConfig(patch);
return Response.json(updated);
diff --git a/app/api/events/display/route.ts b/app/api/events/display/route.ts
index 6655fd8..ca7d24d 100644
--- a/app/api/events/display/route.ts
+++ b/app/api/events/display/route.ts
@@ -12,13 +12,33 @@ export async function GET(request: Request) {
const backendUrl = process.env.API_URL || "http://localhost:3000";
- const response = await fetch(`${backendUrl}/events/display`, {
- signal: request.signal,
- headers: {
- "Accept": "text/event-stream",
- "Cookie": token ? `mysagra_token=${token}` : "",
- }
- });
+ const lastEventId = request.headers.get("Last-Event-ID");
+
+ let response: Response;
+ try {
+ response = await fetch(`${backendUrl}/events/display`, {
+ signal: request.signal,
+ headers: {
+ "Accept": "text/event-stream",
+ "Cookie": token ? `mysagra_token=${token}` : "",
+ ...(lastEventId ? { "Last-Event-ID": lastEventId } : {}),
+ }
+ });
+ } catch {
+ // Return valid SSE stream that closes immediately — EventSource stays CONNECTING and retries
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode("retry: 3000\n\n"));
+ controller.close();
+ }
+ });
+ return new Response(stream, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache",
+ }
+ });
+ }
if (!response.ok) {
return new Response("Error connecting to event stream", { status: response.status });
diff --git a/app/display/page.tsx b/app/display/page.tsx
index b3a6010..71bd5cb 100644
--- a/app/display/page.tsx
+++ b/app/display/page.tsx
@@ -1,11 +1,12 @@
"use client";
import { Header } from "@/components/display/header";
-import { useEffect, useState, useRef, useCallback } from "react";
+import { useEffect, useState, useRef, useCallback, useMemo } from "react";
import { getWorkdayBounds, sortByDate } from "@/utils/utils";
import { type DisplayMode, DISPLAY_MODE_KEY } from "@/components/settings/DisplayModeSettingsCard";
import { EVENT_NAME_KEY } from "@/components/settings/GeneralSettingsCard";
import { NUMBER_DISPLAY_KEY, TICKET_NUMBER_MAX_KEY } from "@/components/settings/NumberDisplaySettingsCard";
+import { DISPLAY_ZOOM_KEY } from "@/components/settings/DisplayZoomSettingsCard";
import type { NumberDisplay } from "@/lib/display-config-store";
import { useTranslation } from "react-i18next";
@@ -119,9 +120,11 @@ interface DisplaySectionProps {
immediateRemoval?: boolean;
getOrderLabel: (order: ReadyOrder) => string;
bare?: boolean;
+ autoScrollEnabled?: boolean;
+ displayZoom?: number;
}
-function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, sectionId, immediateRemoval = false, getOrderLabel, bare = false }: DisplaySectionProps) {
+function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, sectionId, immediateRemoval = false, getOrderLabel, bare = false, autoScrollEnabled = true, displayZoom = 100 }: DisplaySectionProps) {
const staticCardsPerPage = cols * rows;
const [effectiveCardsPerPage, setEffectiveCardsPerPage] = useState(staticCardsPerPage);
const cardsPerPage = effectiveCardsPerPage;
@@ -141,8 +144,8 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
const { width, height } = el.getBoundingClientRect();
if (width < 10 || height < 10) return;
const gap = 12; // gap-3 = 12px
- const minColW = 100; // minmax(100px, 1fr)
- const minRowH = 70; // minmax(70px, 1fr)
+ const minColW = 100 * displayZoom / 100; // minmax(100px, 1fr) scaled by zoom
+ const minRowH = 70 * displayZoom / 100; // minmax(70px, 1fr) scaled by zoom
const c = Math.max(1, Math.floor((width + gap) / (minColW + gap)));
const r = Math.max(1, Math.floor((height + gap) / (minRowH + gap)));
setEffectiveCardsPerPage(c * r);
@@ -151,7 +154,7 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
const ro = new ResizeObserver(compute);
ro.observe(el);
return () => ro.disconnect();
- }, []);
+ }, [displayZoom]);
const realOrderCount = displayedOrders.filter((o): o is ReadyOrder => o !== null).length;
const totalPages = Math.max(1, Math.ceil(realOrderCount / cardsPerPage));
@@ -177,6 +180,7 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
}, [orders, cardsPerPage, immediateRemoval]);
useEffect(() => {
+ if (!autoScrollEnabled) { setCurrentPage(0); return; }
if (totalPages <= 1) { setCurrentPage(0); return; }
const timer = setTimeout(() => {
setCurrentPage(prev => {
@@ -187,7 +191,7 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
}, PAGE_INTERVAL);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentPage, totalPages]);
+ }, [currentPage, totalPages, autoScrollEnabled]);
const pageOrders = displayedOrders.slice(currentPage * cardsPerPage, (currentPage + 1) * cardsPerPage);
@@ -195,7 +199,7 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
<>
{title}
- {totalPages > 1 && (
+ {autoScrollEnabled && totalPages > 1 && (
{currentPage + 1}
/
@@ -203,13 +207,15 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
)}
-
- {totalPages > 1 && (
-
- )}
-
+ {autoScrollEnabled && (
+
+ {totalPages > 1 && (
+
+ )}
+
+ )}
-
+
{pageOrders.map((order, idx) => order ? (
) : (
@@ -243,6 +249,8 @@ interface SplitDisplaySectionProps {
topRows: number;
bottomRows: number;
getOrderLabel: (order: ReadyOrder) => string;
+ autoScrollEnabled?: boolean;
+ displayZoom?: number;
}
function SplitDisplaySection({
@@ -252,6 +260,8 @@ function SplitDisplaySection({
topHeaderClass, bottomHeaderClass,
topCardBgClass, bottomCardBgClass,
sectionId, cols, topRows, bottomRows, getOrderLabel,
+ autoScrollEnabled = true,
+ displayZoom = 100,
}: SplitDisplaySectionProps) {
return (
@@ -272,6 +282,8 @@ function SplitDisplaySection({
immediateRemoval
bare
getOrderLabel={getOrderLabel}
+ autoScrollEnabled={autoScrollEnabled}
+ displayZoom={displayZoom}
/>
{/* Bottom 2/3: ready */}
@@ -286,6 +298,8 @@ function SplitDisplaySection({
sectionId={`${sectionId}-bottom`}
bare
getOrderLabel={getOrderLabel}
+ autoScrollEnabled={true}
+ displayZoom={displayZoom}
/>
@@ -302,11 +316,9 @@ export default function Display() {
const [numberDisplay, setNumberDisplay] = useState
("displayCode");
const [ticketNumberMax, setTicketNumberMax] = useState(100);
const [stationsEnabled, setStationsEnabled] = useState(false);
+ const [autoScrollPagesEnabled, setAutoScrollPagesEnabled] = useState(true);
const [stations, setStations] = useState([]);
-
- // Per-station order maps (stations mode)
- const [stationConfirmed, setStationConfirmed] = useState>({});
- const [stationCompleted, setStationCompleted] = useState>({});
+ const [displayZoom, setDisplayZoom] = useState(100);
const TITLE_MAP: Record = {
ready: t("display.ordersReady"),
@@ -314,9 +326,50 @@ export default function Display() {
hybrid: t("display.orders"),
};
- // Normal mode order lists
- const [readyOrders, setReadyOrders] = useState([]);
- const [prepOrders, setPrepOrders] = useState([]);
+ // Single source of truth: all orders keyed by id
+ const [ordersMap, setOrdersMap] = useState
@@ -872,6 +861,8 @@ export default function Display() {
cardBgClass="bg-green-100"
sectionId="ready"
getOrderLabel={getOrderLabel}
+ autoScrollEnabled={true}
+ displayZoom={displayZoom}
/>
@@ -879,7 +870,7 @@ export default function Display() {
/* NORMAL — SINGLE MODE */
) : (
-
+
{pageOrders.map((order, idx) => order ? (
) : (
diff --git a/app/manager/page.tsx b/app/manager/page.tsx
index 1ac2438..e1bfccd 100644
--- a/app/manager/page.tsx
+++ b/app/manager/page.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState, useEffect, useCallback, useRef } from "react";
+import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { Header } from "@/components/manager/header";
import OrdersGrid from "@/components/manager/orders-grid";
import { PickedUpOrdersSheet } from "@/components/manager/picked-up-orders-sheet";
@@ -31,30 +31,80 @@ const toOrder = (o: Order): Order => ({
export default function Manager() {
const { t } = useTranslation();
- // --- Normal mode state ---
- const [confirmedOrders, setConfirmedOrders] = useState
([]);
- const [readyOrders, setReadyOrders] = useState([]);
- const [pickedUpOrders, setPickedUpOrders] = useState([]);
+ // Single source of truth: all orders keyed by id
+ const [ordersMap, setOrdersMap] = useState>(new Map());
- // --- Stations mode state ---
+ // Station config
const [stations, setStations] = useState([]);
const [stationsEnabled, setStationsEnabled] = useState(false);
const stationsEnabledRef = useRef(false);
- const [stationConfirmed, setStationConfirmed] = useState>({});
- const [stationCompleted, setStationCompleted] = useState>({});
- const [stationPickedUp, setStationPickedUp] = useState>({});
-
- // Keeps a ref to stations list so SSE closures can access it
- const stationsRef = useRef([]);
- useEffect(() => { stationsRef.current = stations; }, [stations]);
-
- // Refs to current station maps so SSE closures can look up orders by id
- const stationConfirmedRef = useRef>({});
- const stationCompletedRef = useRef>({});
- const pickedUpOrdersRef = useRef([]);
- useEffect(() => { stationConfirmedRef.current = stationConfirmed; }, [stationConfirmed]);
- useEffect(() => { stationCompletedRef.current = stationCompleted; }, [stationCompleted]);
- useEffect(() => { pickedUpOrdersRef.current = pickedUpOrders; }, [pickedUpOrders]);
+
+ // --- Derived lists: normal mode ---
+ const confirmedOrders = useMemo(() =>
+ Array.from(ordersMap.values()).filter(o =>
+ (o.orderStationStates ?? []).length > 0 &&
+ (o.status === 'CONFIRMED' || o.status === 'PARTIAL')
+ ),
+ [ordersMap]
+ );
+
+ const readyOrders = useMemo(() =>
+ Array.from(ordersMap.values()).filter(o =>
+ (o.orderStationStates ?? []).length > 0 && o.status === 'COMPLETED'
+ ),
+ [ordersMap]
+ );
+
+ const pickedUpOrders = useMemo(() =>
+ Array.from(ordersMap.values()).filter(o =>
+ (o.orderStationStates ?? []).length > 0 && o.status === 'PICKED_UP'
+ ),
+ [ordersMap]
+ );
+
+ // --- Derived maps: station mode ---
+ const stationConfirmed = useMemo(() => {
+ const map: Record = {};
+ for (const s of stations) map[s.id] = [];
+ for (const o of ordersMap.values())
+ for (const state of o.orderStationStates ?? [])
+ if (state.status === 'CONFIRMED' && state.stationId in map &&
+ !map[state.stationId].find(x => x.id === o.id))
+ map[state.stationId].push(o);
+ return map;
+ }, [ordersMap, stations]);
+
+ const stationCompleted = useMemo(() => {
+ const map: Record = {};
+ for (const s of stations) map[s.id] = [];
+ for (const o of ordersMap.values())
+ for (const state of o.orderStationStates ?? [])
+ if (state.status === 'COMPLETED' && state.stationId in map &&
+ !map[state.stationId].find(x => x.id === o.id))
+ map[state.stationId].push(o);
+ return map;
+ }, [ordersMap, stations]);
+
+ const stationPickedUp = useMemo(() => {
+ const map: Record = {};
+ for (const s of stations) map[s.id] = [];
+ for (const o of ordersMap.values())
+ for (const state of o.orderStationStates ?? [])
+ if (state.status === 'PICKED_UP' && state.stationId in map &&
+ !map[state.stationId].find(x => x.id === o.id))
+ map[state.stationId].push(o);
+ return map;
+ }, [ordersMap, stations]);
+
+ // Flat deduped picked-up list for the header in station mode
+ const stationModePickedUpOrders = useMemo(() => {
+ const seen = new Set();
+ const result: Order[] = [];
+ for (const list of Object.values(stationPickedUp))
+ for (const o of list)
+ if (!seen.has(o.id)) { seen.add(o.id); result.push(o); }
+ return result;
+ }, [stationPickedUp]);
const fetchAllPages = async (baseParams: string): Promise => {
let page = 1;
@@ -76,60 +126,8 @@ export default function Manager() {
try {
const { dateFrom, dateTo } = getWorkdayBounds();
const dateParams = `&dateFrom=${encodeURIComponent(dateFrom)}&dateTo=${encodeURIComponent(dateTo)}`;
-
- // Always fetch with station states included
const orders = await fetchAllPages(`${dateParams}&include=ordersStationsStates`);
-
- if (stationsEnabledRef.current) {
- const stList = stationsRef.current;
- const confirmedMap: Record = {};
- const completedMap: Record = {};
- const pickedUpMap: Record = {};
- for (const s of stList) { confirmedMap[s.id] = []; completedMap[s.id] = []; pickedUpMap[s.id] = []; }
-
- for (const o of orders) {
- // Distribute by station-level status (independent of order-level status)
- for (const state of o.orderStationStates ?? []) {
- const stId = state.stationId;
- if (state.status === 'CONFIRMED' && stId in confirmedMap && !confirmedMap[stId].find(x => x.id === o.id)) {
- confirmedMap[stId].push(o);
- } else if (state.status === 'COMPLETED' && stId in completedMap && !completedMap[stId].find(x => x.id === o.id)) {
- completedMap[stId].push(o);
- } else if (state.status === 'PICKED_UP' && stId in pickedUpMap && !pickedUpMap[stId].find(x => x.id === o.id)) {
- pickedUpMap[stId].push(o);
- }
- }
- }
-
- // Flatten pickedUpMap → flat pickedUpOrders for header (deduped)
- const pickedUpSeen = new Set();
- const pickedUp: Order[] = [];
- for (const list of Object.values(pickedUpMap)) {
- for (const o of list) {
- if (!pickedUpSeen.has(o.id)) { pickedUpSeen.add(o.id); pickedUp.push(o); }
- }
- }
-
- setStationConfirmed(confirmedMap);
- setStationCompleted(completedMap);
- setStationPickedUp(pickedUpMap);
- setPickedUpOrders(pickedUp);
- return;
- }
-
- // Normal mode: skip orders with no station assignments
- const confirmed: Order[] = [];
- const ready: Order[] = [];
- const pickedUp: Order[] = [];
- for (const o of orders) {
- if ((o.orderStationStates ?? []).length === 0) continue;
- if (o.status === 'CONFIRMED' || o.status === 'PARTIAL') confirmed.push(o);
- else if (o.status === 'COMPLETED') ready.push(o);
- else if (o.status === 'PICKED_UP') pickedUp.push(o);
- }
- setConfirmedOrders(confirmed);
- setReadyOrders(ready);
- setPickedUpOrders(pickedUp);
+ setOrdersMap(new Map(orders.map(o => [o.id, o])));
} catch (error) {
console.error("Failed to fetch orders:", error);
}
@@ -150,12 +148,7 @@ export default function Manager() {
.then(res => res && res.ok ? res.json() : null)
.then(data => {
if (Array.isArray(data)) {
- stationsRef.current = data;
setStations(data);
- const empty: Record = {};
- for (const s of data) empty[s.id] = [];
- setStationConfirmed({ ...empty });
- setStationCompleted({ ...empty });
fetchOrders();
}
})
@@ -167,41 +160,23 @@ export default function Manager() {
fetchOrders();
const eventSource = new EventSource('/api/events/display');
+ let isFirstOpen = true;
- const removeFromAllStations = (orderId: string) => {
- setStationConfirmed(prev => {
- const next = { ...prev };
- for (const k of Object.keys(next)) next[k] = next[k].filter(o => o.id !== orderId);
- return next;
- });
- setStationCompleted(prev => {
- const next = { ...prev };
- for (const k of Object.keys(next)) next[k] = next[k].filter(o => o.id !== orderId);
- return next;
- });
- };
+ // Refetch full state on every reconnect to resync after any missed events
+ eventSource.addEventListener('open', () => {
+ if (!isFirstOpen) fetchOrders();
+ isFirstOpen = false;
+ });
const handleConfirmedOrder = (event: MessageEvent) => {
try {
- const newOrder = toOrder(JSON.parse(event.data));
- // Ignore orders not assigned to any station
- if ((newOrder.ordersStations ?? []).length === 0) return;
- if (stationsEnabledRef.current) {
- setStationConfirmed(prev => {
- const next = { ...prev };
- for (const stId of newOrder.ordersStations ?? []) {
- if (stId in next && !next[stId].find(o => o.id === newOrder.id)) {
- next[stId] = [...next[stId], newOrder];
- }
- }
- return next;
- });
- return;
- }
- setConfirmedOrders(prev => {
- if (prev.find(o => String(o.id) === String(newOrder.id))) return prev;
- return [...prev, newOrder];
- });
+ const raw = toOrder(JSON.parse(event.data));
+ if ((raw.ordersStations ?? []).length === 0) return;
+ // Synthesize station states from ordersStations if not included in payload
+ const orderStationStates = (raw.orderStationStates ?? []).length > 0
+ ? raw.orderStationStates!
+ : (raw.ordersStations ?? []).map(stId => ({ stationId: stId, status: 'CONFIRMED' }));
+ setOrdersMap(prev => new Map(prev).set(raw.id, { ...raw, orderStationStates }));
} catch (err) {
console.error("Error parsing confirmed-order event:", err);
}
@@ -212,104 +187,52 @@ export default function Manager() {
eventSource.addEventListener('order-status-update', (event: MessageEvent) => {
try {
- const raw = JSON.parse(event.data);
- const { id, status } = raw;
- const sid = String(id);
-
- if (stationsEnabledRef.current) {
- // Find order in current maps before clearing (refs still point to current state)
- let order: Order | undefined;
- for (const stId of Object.keys(stationConfirmedRef.current)) {
- order = stationConfirmedRef.current[stId].find(o => String(o.id) === sid);
- if (order) break;
- }
- if (!order) {
- for (const stId of Object.keys(stationCompletedRef.current)) {
- order = stationCompletedRef.current[stId].find(o => String(o.id) === sid);
- if (order) break;
+ const { id, status, displayCode, ticketNumber } = JSON.parse(event.data);
+ setOrdersMap(prev => {
+ const existing = prev.get(String(id));
+ if (!existing) return prev;
+
+ let orderStationStates = existing.orderStationStates;
+ // In station mode, propagate order-level status to station states.
+ // This handles header undo ops that PATCH order-level (no stationId).
+ if (stationsEnabledRef.current) {
+ if (status === 'COMPLETED') {
+ orderStationStates = (orderStationStates ?? []).map(s =>
+ s.status === 'PICKED_UP' ? { ...s, status: 'COMPLETED' } : s
+ );
+ } else if (status === 'CONFIRMED') {
+ orderStationStates = (orderStationStates ?? []).map(s =>
+ s.status === 'COMPLETED' ? { ...s, status: 'CONFIRMED' } : s
+ );
}
}
- if (!order) {
- order = pickedUpOrdersRef.current.find(o => String(o.id) === sid);
- }
-
- removeFromAllStations(sid);
-
- if (status === 'CONFIRMED' && order) {
- // Undo: put back in confirmed for every station the order belongs to
- const stIds = Object.keys(stationCompletedRef.current).filter(k =>
- stationCompletedRef.current[k].some(o => String(o.id) === sid) ||
- stationConfirmedRef.current[k]?.some(o => String(o.id) === sid)
- );
- setStationConfirmed(prev => {
- const next = { ...prev };
- for (const stId of stIds) {
- if (stId in next && !next[stId].find(o => String(o.id) === sid)) next[stId] = [...next[stId], order!];
- }
- return next;
- });
- } else if (status === 'COMPLETED' && order) {
- const stIdsFromConfirmed = Object.keys(stationConfirmedRef.current).filter(k =>
- stationConfirmedRef.current[k].some(o => String(o.id) === sid)
- );
- // fallback: use order.ordersStations when coming from PICKED_UP
- const stIds = stIdsFromConfirmed.length > 0
- ? stIdsFromConfirmed
- : (order.ordersStations ?? []).filter(k => k in stationCompletedRef.current);
- setPickedUpOrders(prev => prev.filter(o => String(o.id) !== sid));
- setStationCompleted(prev => {
- const next = { ...prev };
- for (const stId of stIds) {
- if (stId in next && !next[stId].find(o => String(o.id) === sid)) next[stId] = [...next[stId], order!];
- }
- return next;
- });
- } else if (status === 'PICKED_UP' && order) {
- setPickedUpOrders(prev => prev.find(o => String(o.id) === sid) ? prev : [...prev, order!]);
- }
- return;
- }
- const updated = toOrder(raw);
- setConfirmedOrders(prev => prev.filter(o => String(o.id) !== sid));
- setReadyOrders(prev => prev.filter(o => String(o.id) !== sid));
- setPickedUpOrders(prev => prev.filter(o => String(o.id) !== sid));
- if (updated.status === 'CONFIRMED' || updated.status === 'PARTIAL') setConfirmedOrders(prev => [...prev, updated]);
- if (updated.status === 'COMPLETED') setReadyOrders(prev => [...prev, updated]);
- if (updated.status === 'PICKED_UP') setPickedUpOrders(prev => [...prev, updated]);
+ return new Map(prev).set(String(id), {
+ ...existing,
+ status,
+ displayCode,
+ ticketNumber,
+ orderStationStates,
+ });
+ });
} catch (err) {
console.error("Error parsing order-status-update event:", err);
}
});
eventSource.addEventListener('order-station-status-update', (event: MessageEvent) => {
- if (!stationsEnabledRef.current) return;
try {
const { orderId, stationId, status } = JSON.parse(event.data);
- const sid = String(orderId);
-
- if (status === 'COMPLETED') {
- const order = stationConfirmedRef.current[stationId]?.find(o => String(o.id) === sid)
- ?? pickedUpOrdersRef.current.find(o => String(o.id) === sid);
- setStationConfirmed(prev => ({ ...prev, [stationId]: (prev[stationId] ?? []).filter(o => String(o.id) !== sid) }));
- setStationPickedUp(prev => ({ ...prev, [stationId]: (prev[stationId] ?? []).filter(o => String(o.id) !== sid) }));
- setPickedUpOrders(prev => prev.filter(o => String(o.id) !== sid));
- if (order) setStationCompleted(prev => prev[stationId]?.find(o => String(o.id) === sid) ? prev : { ...prev, [stationId]: [...(prev[stationId] ?? []), order] });
- } else if (status === 'CONFIRMED') {
- const order = stationCompletedRef.current[stationId]?.find(o => String(o.id) === sid)
- ?? stationConfirmedRef.current[stationId]?.find(o => String(o.id) === sid);
- setStationCompleted(prev => ({ ...prev, [stationId]: (prev[stationId] ?? []).filter(o => String(o.id) !== sid) }));
- if (order) setStationConfirmed(prev => prev[stationId]?.find(o => String(o.id) === sid) ? prev : { ...prev, [stationId]: [...(prev[stationId] ?? []), order] });
- } else if (status === 'PICKED_UP') {
- const order = stationCompletedRef.current[stationId]?.find(o => String(o.id) === sid)
- ?? stationConfirmedRef.current[stationId]?.find(o => String(o.id) === sid);
- setStationConfirmed(prev => ({ ...prev, [stationId]: (prev[stationId] ?? []).filter(o => String(o.id) !== sid) }));
- setStationCompleted(prev => ({ ...prev, [stationId]: (prev[stationId] ?? []).filter(o => String(o.id) !== sid) }));
- if (order) {
- setStationPickedUp(prev => prev[stationId]?.find(o => String(o.id) === sid) ? prev : { ...prev, [stationId]: [...(prev[stationId] ?? []), order] });
- setPickedUpOrders(prev => prev.find(o => String(o.id) === sid) ? prev : [...prev, order]);
- }
- }
+ setOrdersMap(prev => {
+ const order = prev.get(String(orderId));
+ if (!order) return prev;
+ const existing = order.orderStationStates ?? [];
+ const hasState = existing.some(s => s.stationId === stationId);
+ const updatedStates = hasState
+ ? existing.map(s => s.stationId === stationId ? { ...s, status } : s)
+ : [...existing, { stationId, status }];
+ return new Map(prev).set(order.id, { ...order, orderStationStates: updatedStates });
+ });
} catch (err) {
console.error("Error parsing order-station-status-update event:", err);
}
@@ -319,14 +242,7 @@ export default function Manager() {
try {
const cancelled = JSON.parse(event.data);
const sid = String(cancelled.id);
- if (stationsEnabledRef.current) {
- removeFromAllStations(sid);
- setPickedUpOrders(prev => prev.filter(o => String(o.id) !== sid));
- } else {
- setConfirmedOrders(prev => prev.filter(o => String(o.id) !== sid));
- setReadyOrders(prev => prev.filter(o => String(o.id) !== sid));
- setPickedUpOrders(prev => prev.filter(o => String(o.id) !== sid));
- }
+ setOrdersMap(prev => { const next = new Map(prev); next.delete(sid); return next; });
toast.warning(t("manager.orderCancelled", { code: cancelled.displayCode }));
} catch (err) {
console.error("Error parsing order-cancelled event:", err);
@@ -354,61 +270,85 @@ export default function Manager() {
// --- Station mode handlers ---
const handleStationMarkDone = (order: Order, stationId: string) => {
- setStationConfirmed(prev => ({ ...prev, [stationId]: prev[stationId]?.filter(o => o.id !== order.id) ?? [] }));
- setStationCompleted(prev => ({ ...prev, [stationId]: [...(prev[stationId] ?? []), order] }));
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ const updatedStates = (current.orderStationStates ?? []).map(s =>
+ s.stationId === stationId ? { ...s, status: 'COMPLETED' } : s
+ );
+ return new Map(prev).set(order.id, { ...current, orderStationStates: updatedStates });
+ });
updateOrderStatus(order.id, 'COMPLETED', stationId);
};
const handleStationMarkUndo = (order: Order, stationId: string) => {
- setStationCompleted(prev => ({ ...prev, [stationId]: prev[stationId]?.filter(o => o.id !== order.id) ?? [] }));
- setStationConfirmed(prev => ({ ...prev, [stationId]: [...(prev[stationId] ?? []), order] }));
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ const updatedStates = (current.orderStationStates ?? []).map(s =>
+ s.stationId === stationId ? { ...s, status: 'CONFIRMED' } : s
+ );
+ return new Map(prev).set(order.id, { ...current, orderStationStates: updatedStates });
+ });
updateOrderStatus(order.id, 'CONFIRMED', stationId);
};
const handleStationMarkPickup = (order: Order, stationId: string) => {
- setStationCompleted(prev => ({ ...prev, [stationId]: prev[stationId]?.filter(o => o.id !== order.id) ?? [] }));
- setStationPickedUp(prev => ({
- ...prev,
- [stationId]: prev[stationId]?.find(o => o.id === order.id) ? prev[stationId] : [...(prev[stationId] ?? []), order],
- }));
- setPickedUpOrders(prev => prev.find(o => o.id === order.id) ? prev : [...prev, order]);
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ const updatedStates = (current.orderStationStates ?? []).map(s =>
+ s.stationId === stationId ? { ...s, status: 'PICKED_UP' } : s
+ );
+ return new Map(prev).set(order.id, { ...current, orderStationStates: updatedStates });
+ });
updateOrderStatus(order.id, 'PICKED_UP', stationId);
};
const handlePickupToCompleteStation = (order: Order, stationId?: string) => {
- setPickedUpOrders(prev => prev.filter(o => o.id !== order.id));
- if (stationId) {
- setStationPickedUp(prev => ({ ...prev, [stationId]: prev[stationId]?.filter(o => o.id !== order.id) ?? [] }));
- setStationCompleted(prev => ({
- ...prev,
- [stationId]: prev[stationId]?.find(o => o.id === order.id) ? prev[stationId] : [...(prev[stationId] ?? []), order],
- }));
- }
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ // With stationId: move that specific station PICKED_UP → COMPLETED
+ // Without stationId (header): move all PICKED_UP stations → COMPLETED
+ const updatedStates = stationId
+ ? (current.orderStationStates ?? []).map(s =>
+ s.stationId === stationId ? { ...s, status: 'COMPLETED' } : s
+ )
+ : (current.orderStationStates ?? []).map(s =>
+ s.status === 'PICKED_UP' ? { ...s, status: 'COMPLETED' } : s
+ );
+ return new Map(prev).set(order.id, { ...current, orderStationStates: updatedStates });
+ });
updateOrderStatus(order.id, 'COMPLETED', stationId);
};
// --- Normal mode handlers ---
const handleConfirmToComplete = (order: Order) => {
- setConfirmedOrders(prev => prev.filter(o => o.id !== order.id));
- setReadyOrders(prev => [...prev, order]);
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ return new Map(prev).set(order.id, { ...current, status: 'COMPLETED' });
+ });
updateOrderStatus(order.id, 'COMPLETED');
};
const handleCompleteToConfirm = (order: Order) => {
- setReadyOrders(prev => prev.filter(o => o.id !== order.id));
- setConfirmedOrders(prev => [...prev, order]);
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ return new Map(prev).set(order.id, { ...current, status: 'CONFIRMED' });
+ });
updateOrderStatus(order.id, 'CONFIRMED');
};
const handleCompleteToPickup = (order: Order) => {
- setReadyOrders(prev => prev.filter(o => o.id !== order.id));
- setPickedUpOrders(prev => [...prev, order]);
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ return new Map(prev).set(order.id, { ...current, status: 'PICKED_UP' });
+ });
updateOrderStatus(order.id, 'PICKED_UP');
};
const handlePickupToComplete = (order: Order) => {
- setPickedUpOrders(prev => prev.filter(o => o.id !== order.id));
- setReadyOrders(prev => [...prev, order]);
+ setOrdersMap(prev => {
+ const current = prev.get(order.id) ?? order;
+ return new Map(prev).set(order.id, { ...current, status: 'COMPLETED' });
+ });
updateOrderStatus(order.id, 'COMPLETED');
};
@@ -416,7 +356,7 @@ export default function Manager() {
if (stationsEnabled && stations.length > 0) {
return (
-
+
{stations.map(station => (
diff --git a/components/manager/StationCard.tsx b/components/manager/StationCard.tsx
index a556ddd..c7c9e14 100644
--- a/components/manager/StationCard.tsx
+++ b/components/manager/StationCard.tsx
@@ -48,8 +48,8 @@ export function StationCard({
- {confirmedOrders.map(order => (
-
+ {[...confirmedOrders].sort((a, b) => a.ticketNumber - b.ticketNumber).map(order => (
+
))}
@@ -70,8 +70,8 @@ export function StationCard({
- {completedOrders.map(order => (
-
+ {[...completedOrders].sort((a, b) => a.ticketNumber - b.ticketNumber).map(order => (
+
))}
diff --git a/components/manager/orders-grid.tsx b/components/manager/orders-grid.tsx
index bafbb38..1de23a8 100644
--- a/components/manager/orders-grid.tsx
+++ b/components/manager/orders-grid.tsx
@@ -18,6 +18,8 @@ interface OrdersGridProps {
}
export default function OrdersGrid({ className, orders, title, status, stationId, onPrev, onNext, children }: OrdersGridProps) {
+ const sortedOrders = [...orders].sort((a, b) => a.ticketNumber - b.ticketNumber);
+
return (
@@ -34,8 +36,8 @@ export default function OrdersGrid({ className, orders, title, status, stationId
}
{
- orders.map((order) => (
-
+ sortedOrders.map((order) => (
+
))
@@ -54,14 +56,38 @@ interface OrderCardProps {
}
export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
- const [numberDisplay, setNumberDisplay] = useState
("displayCode");
+ const [numberDisplay, setNumberDisplay] = useState(() => {
+ const stored = localStorage.getItem(NUMBER_DISPLAY_KEY) as NumberDisplay | null;
+ return (stored && ["displayCode", "ticketNumber"].includes(stored)) ? stored : "displayCode";
+ });
const [ticketNumberMax, setTicketNumberMax] = useState(0);
useEffect(() => {
- const nd = localStorage.getItem(NUMBER_DISPLAY_KEY) as NumberDisplay | null;
- if (nd && ["displayCode", "ticketNumber"].includes(nd)) setNumberDisplay(nd);
- const mx = localStorage.getItem(TICKET_NUMBER_MAX_KEY);
- if (mx !== null) { const n = parseInt(mx, 10); if (!isNaN(n) && n >= 0) setTicketNumberMax(n); }
+ fetch("/api/display-config")
+ .then(res => res.ok ? res.json() : null)
+ .then(cfg => {
+ if (!cfg) {
+ const nd = localStorage.getItem(NUMBER_DISPLAY_KEY) as NumberDisplay | null;
+ if (nd && ["displayCode", "ticketNumber"].includes(nd)) setNumberDisplay(nd);
+ const mx = localStorage.getItem(TICKET_NUMBER_MAX_KEY);
+ if (mx !== null) { const n = parseInt(mx, 10); if (!isNaN(n) && n >= 0) setTicketNumberMax(n); }
+ return;
+ }
+ if (cfg.numberDisplay && ["displayCode", "ticketNumber"].includes(cfg.numberDisplay)) {
+ setNumberDisplay(cfg.numberDisplay as NumberDisplay);
+ localStorage.setItem(NUMBER_DISPLAY_KEY, cfg.numberDisplay);
+ }
+ if (typeof cfg.ticketNumberMax === "number" && cfg.ticketNumberMax >= 0) {
+ setTicketNumberMax(cfg.ticketNumberMax);
+ localStorage.setItem(TICKET_NUMBER_MAX_KEY, String(cfg.ticketNumberMax));
+ }
+ })
+ .catch(() => {
+ const nd = localStorage.getItem(NUMBER_DISPLAY_KEY) as NumberDisplay | null;
+ if (nd && ["displayCode", "ticketNumber"].includes(nd)) setNumberDisplay(nd);
+ const mx = localStorage.getItem(TICKET_NUMBER_MAX_KEY);
+ if (mx !== null) { const n = parseInt(mx, 10); if (!isNaN(n) && n >= 0) setTicketNumberMax(n); }
+ });
}, []);
function addNext() {
@@ -77,7 +103,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
}
const orderTitle = (() => {
- if (numberDisplay !== "ticketNumber") return order.displayCode;
+ if (numberDisplay == "displayCode") return order.displayCode;
// 0 = no max, show raw ticketNumber
if (!ticketNumberMax) return String(order.ticketNumber);
return String(order.ticketNumber % ticketNumberMax);
@@ -89,7 +115,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
variant="outline"
size="lg"
onClick={addNext}
- className="select-none h-16 px-4 text-3xl font-bold font-mono whitespace-nowrap hover:bg-primary hover:text-primary-foreground transition-colors"
+ className="w-full select-none h-16 px-4 text-3xl font-bold font-mono whitespace-nowrap hover:bg-primary transition-colors"
disabled={!onNext}
>
{orderTitle}
@@ -99,7 +125,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
if (status === 'COMPLETED') {
return (
-
+