From 32887e0d9bab2f6efdceb52488ce416689405e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Spampatti?= Date: Fri, 8 May 2026 22:55:09 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=E2=96=B6=EF=B8=8F=20added=20flag=20for=20p?= =?UTF-8?q?rocess=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/display-config/route.ts | 3 + app/display/page.tsx | 167 +++++++++++------- .../settings/DisplayModeSettingsCard.tsx | 24 ++- lib/display-config-store.ts | 5 +- lib/i18n/locales/en.ts | 2 + lib/i18n/locales/it.ts | 2 + 6 files changed, 136 insertions(+), 67 deletions(-) diff --git a/app/api/display-config/route.ts b/app/api/display-config/route.ts index 29b22d5..73cd7ab 100644 --- a/app/api/display-config/route.ts +++ b/app/api/display-config/route.ts @@ -25,6 +25,9 @@ export async function PATCH(request: Request) { if (typeof body.fullscreenAlertEnabled === "boolean") { patch.fullscreenAlertEnabled = body.fullscreenAlertEnabled; } + if (typeof body.autoScrollPagesEnabled === "boolean") { + patch.autoScrollPagesEnabled = body.autoScrollPagesEnabled; + } const updated = updateConfig(patch); return Response.json(updated); diff --git a/app/display/page.tsx b/app/display/page.tsx index b3a6010..a520605 100644 --- a/app/display/page.tsx +++ b/app/display/page.tsx @@ -119,9 +119,10 @@ interface DisplaySectionProps { immediateRemoval?: boolean; getOrderLabel: (order: ReadyOrder) => string; bare?: boolean; + autoScrollEnabled?: boolean; } -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 }: DisplaySectionProps) { const staticCardsPerPage = cols * rows; const [effectiveCardsPerPage, setEffectiveCardsPerPage] = useState(staticCardsPerPage); const cardsPerPage = effectiveCardsPerPage; @@ -177,6 +178,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 +189,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 +197,7 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s <>

{title}

- {totalPages > 1 && ( + {autoScrollEnabled && totalPages > 1 && (
{currentPage + 1} / @@ -203,11 +205,13 @@ function DisplaySection({ orders, cols, rows, title, headerClass, cardBgClass, s
)}
-
- {totalPages > 1 && ( -
- )} -
+ {autoScrollEnabled && ( +
+ {totalPages > 1 && ( +
+ )} +
+ )}
{pageOrders.map((order, idx) => order ? ( @@ -243,6 +247,7 @@ interface SplitDisplaySectionProps { topRows: number; bottomRows: number; getOrderLabel: (order: ReadyOrder) => string; + autoScrollEnabled?: boolean; } function SplitDisplaySection({ @@ -252,6 +257,7 @@ function SplitDisplaySection({ topHeaderClass, bottomHeaderClass, topCardBgClass, bottomCardBgClass, sectionId, cols, topRows, bottomRows, getOrderLabel, + autoScrollEnabled = true, }: SplitDisplaySectionProps) { return (
@@ -272,6 +278,7 @@ function SplitDisplaySection({ immediateRemoval bare getOrderLabel={getOrderLabel} + autoScrollEnabled={autoScrollEnabled} />
{/* Bottom 2/3: ready */} @@ -286,6 +293,7 @@ function SplitDisplaySection({ sectionId={`${sectionId}-bottom`} bare getOrderLabel={getOrderLabel} + autoScrollEnabled={autoScrollEnabled} />
@@ -302,6 +310,7 @@ 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) @@ -328,6 +337,7 @@ export default function Display() { const [eventName, setEventName] = useState(""); const stationsEnabledRef = useRef(false); + const autoScrollPagesEnabledRef = useRef(true); // Refs to current station maps so SSE closures can look up orders by id const stationConfirmedRef = useRef>({}); @@ -335,6 +345,7 @@ export default function Display() { const pickedUpOrdersRef = useRef>({}); useEffect(() => { stationConfirmedRef.current = stationConfirmed; }, [stationConfirmed]); useEffect(() => { stationCompletedRef.current = stationCompleted; }, [stationCompleted]); + useEffect(() => { autoScrollPagesEnabledRef.current = autoScrollPagesEnabled; }, [autoScrollPagesEnabled]); // Full-screen overlay const [fullscreenAlertEnabled, setFullscreenAlertEnabled] = useState(true); @@ -386,6 +397,7 @@ export default function Display() { if (cfg.numberDisplay && ["displayCode", "ticketNumber"].includes(cfg.numberDisplay)) setNumberDisplay(cfg.numberDisplay as NumberDisplay); if (typeof cfg.ticketNumberMax === "number" && cfg.ticketNumberMax >= 0) setTicketNumberMax(cfg.ticketNumberMax); if (typeof cfg.fullscreenAlertEnabled === "boolean") { fullscreenAlertEnabledRef.current = cfg.fullscreenAlertEnabled; setFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); } + if (typeof cfg.autoScrollPagesEnabled === "boolean") { autoScrollPagesEnabledRef.current = cfg.autoScrollPagesEnabled; setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); } if (cfg.stationsEnabled) { stationsEnabledRef.current = true; setStationsEnabled(true); @@ -434,6 +446,7 @@ export default function Display() { if (cfg.numberDisplay && ["displayCode", "ticketNumber"].includes(cfg.numberDisplay)) setNumberDisplay(cfg.numberDisplay as NumberDisplay); if (typeof cfg.ticketNumberMax === "number" && cfg.ticketNumberMax >= 0) setTicketNumberMax(cfg.ticketNumberMax); if (typeof cfg.fullscreenAlertEnabled === "boolean") { fullscreenAlertEnabledRef.current = cfg.fullscreenAlertEnabled; setFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); } + if (typeof cfg.autoScrollPagesEnabled === "boolean") { autoScrollPagesEnabledRef.current = cfg.autoScrollPagesEnabled; setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); } if (typeof cfg.stationsEnabled === "boolean") { stationsEnabledRef.current = cfg.stationsEnabled; setStationsEnabled(cfg.stationsEnabled); @@ -725,18 +738,20 @@ export default function Display() { // ------------------------------------------------------------------ useEffect(() => { if (displayMode === "hybrid" || stationsEnabled) return; - setDisplayedOrders(activeOrders); + const ordered = [...activeOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); + setDisplayedOrders(ordered); setCurrentPage(0); }, [displayMode]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (displayMode === "hybrid" || stationsEnabled) return; + const orderedActive = [...activeOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); setDisplayedOrders(prev => { const prevItems = prev.filter((o): o is ReadyOrder => o !== null); - if (prevItems.length <= CARDS_PER_PAGE) return activeOrders; - const currentIds = new Set(activeOrders.map(o => o.id)); + if (prevItems.length <= CARDS_PER_PAGE) return orderedActive; + const currentIds = new Set(orderedActive.map(o => o.id)); const prevIds = new Set(prevItems.map(o => o.id)); - const newOrders = activeOrders.filter(o => !prevIds.has(o.id)); + const newOrders = orderedActive.filter(o => !prevIds.has(o.id)); const base = prev.map(o => (o === null || currentIds.has(o.id)) ? o : null); if (newOrders.length === 0 && base.every((o, i) => o === prev[i])) return prev; return [...base, ...newOrders]; @@ -746,6 +761,7 @@ export default function Display() { useEffect(() => { if (displayMode === "hybrid" || stationsEnabled) return; + if (!autoScrollPagesEnabled) { setCurrentPage(0); return; } if (totalPages <= 1) { setCurrentPage(0); return; } const timer = setTimeout(() => { setCurrentPage(prev => { @@ -756,7 +772,7 @@ export default function Display() { }, PAGE_INTERVAL); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage, totalPages, displayMode]); + }, [currentPage, totalPages, displayMode, autoScrollPagesEnabled]); // ------------------------------------------------------------------ // Derived: effective mode when stations active @@ -766,6 +782,10 @@ export default function Display() { const effectivePreparing = stationsEnabled && (displayMode === "preparing" || (displayMode === "hybrid" && !hybridAllowed)); const effectiveReady = stationsEnabled && displayMode === "ready"; + // Order by ticketNumber (ascending) + const orderedPrepOrders = [...prepOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); + const orderedReadyOrders = [...readyOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); + // ------------------------------------------------------------------ // Render // ------------------------------------------------------------------ @@ -785,65 +805,82 @@ export default function Display() { {/* STATIONS — HYBRID (≤3 stations): one card per station split top=prep/bottom=ready */} {effectiveHybrid ? (
- {stations.map((station, idx) => ( -
- -
- ))} + {stations.map((station, idx) => { + const topOrders = stationConfirmed[station.id] ?? []; + const bottomOrders = stationCompleted[station.id] ?? []; + const sortedTop = [...topOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); + const sortedBottom = [...bottomOrders].sort((a, b) => a.ticketNumber - b.ticketNumber); + return ( +
+ +
+ ); + })}
/* STATIONS — PREPARING (or hybrid degraded with >3 stations) */ ) : effectivePreparing ? (
- {stations.map((station, idx) => ( -
- -
- ))} + {stations.map((station, idx) => { + const orders = stationConfirmed[station.id] ?? []; + const sorted = [...orders].sort((a, b) => a.ticketNumber - b.ticketNumber); + return ( +
+ +
+ ); + })}
/* STATIONS — READY */ ) : effectiveReady ? (
- {stations.map((station, idx) => ( -
- -
- ))} + {stations.map((station, idx) => { + const orders = stationCompleted[station.id] ?? []; + const sorted = [...orders].sort((a, b) => a.ticketNumber - b.ticketNumber); + return ( +
+ +
+ ); + })}
/* NORMAL — HYBRID */ @@ -851,7 +888,7 @@ export default function Display() {
diff --git a/components/settings/DisplayModeSettingsCard.tsx b/components/settings/DisplayModeSettingsCard.tsx index 962fccf..3ae015d 100644 --- a/components/settings/DisplayModeSettingsCard.tsx +++ b/components/settings/DisplayModeSettingsCard.tsx @@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"; export type DisplayMode = "ready" | "preparing" | "hybrid"; export const DISPLAY_MODE_KEY = "display-mode"; +export const AUTO_SCROLL_PAGES_KEY = "auto-scroll-pages"; export function DisplayModeSettingsCard() { const { t } = useTranslation(); @@ -23,6 +24,8 @@ export function DisplayModeSettingsCard() { const [savedStationsEnabled, setSavedStationsEnabled] = useState(false); const [fullscreenAlertEnabled, setFullscreenAlertEnabled] = useState(true); const [savedFullscreenAlertEnabled, setSavedFullscreenAlertEnabled] = useState(true); + const [autoScrollPagesEnabled, setAutoScrollPagesEnabled] = useState(true); + const [savedAutoScrollPagesEnabled, setSavedAutoScrollPagesEnabled] = useState(true); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -68,6 +71,10 @@ export function DisplayModeSettingsCard() { setFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); setSavedFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); } + if (typeof cfg?.autoScrollPagesEnabled === "boolean") { + setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); + setSavedAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); + } }) .catch(() => { const stored = localStorage.getItem(DISPLAY_MODE_KEY) as DisplayMode | null; @@ -85,11 +92,12 @@ export function DisplayModeSettingsCard() { await fetch("/api/display-config", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ displayMode: mode, stationsEnabled, fullscreenAlertEnabled }), + body: JSON.stringify({ displayMode: mode, stationsEnabled, fullscreenAlertEnabled, autoScrollPagesEnabled }), }); setSavedMode(mode); setSavedStationsEnabled(stationsEnabled); setSavedFullscreenAlertEnabled(fullscreenAlertEnabled); + setSavedAutoScrollPagesEnabled(autoScrollPagesEnabled); localStorage.setItem(DISPLAY_MODE_KEY, mode); toast.success(t("settings.displayModeSaved")); } catch { @@ -102,7 +110,8 @@ export function DisplayModeSettingsCard() { const hasChanges = mode !== savedMode || stationsEnabled !== savedStationsEnabled || - fullscreenAlertEnabled !== savedFullscreenAlertEnabled; + fullscreenAlertEnabled !== savedFullscreenAlertEnabled || + autoScrollPagesEnabled !== savedAutoScrollPagesEnabled; return ( @@ -167,6 +176,17 @@ export function DisplayModeSettingsCard() { disabled={isLoading} />
+
+ + +
)}
-
+
{pageOrders.map((order, idx) => order ? ( ) : ( @@ -248,6 +250,7 @@ interface SplitDisplaySectionProps { bottomRows: number; getOrderLabel: (order: ReadyOrder) => string; autoScrollEnabled?: boolean; + displayZoom?: number; } function SplitDisplaySection({ @@ -258,6 +261,7 @@ function SplitDisplaySection({ topCardBgClass, bottomCardBgClass, sectionId, cols, topRows, bottomRows, getOrderLabel, autoScrollEnabled = true, + displayZoom = 100, }: SplitDisplaySectionProps) { return (
@@ -279,6 +283,7 @@ function SplitDisplaySection({ bare getOrderLabel={getOrderLabel} autoScrollEnabled={autoScrollEnabled} + displayZoom={displayZoom} />
{/* Bottom 2/3: ready */} @@ -293,7 +298,8 @@ function SplitDisplaySection({ sectionId={`${sectionId}-bottom`} bare getOrderLabel={getOrderLabel} - autoScrollEnabled={autoScrollEnabled} + autoScrollEnabled={true} + displayZoom={displayZoom} />
@@ -312,6 +318,7 @@ export default function Display() { const [stationsEnabled, setStationsEnabled] = useState(false); const [autoScrollPagesEnabled, setAutoScrollPagesEnabled] = useState(true); const [stations, setStations] = useState([]); + const [displayZoom, setDisplayZoom] = useState(100); // Per-station order maps (stations mode) const [stationConfirmed, setStationConfirmed] = useState>({}); @@ -398,6 +405,7 @@ export default function Display() { if (typeof cfg.ticketNumberMax === "number" && cfg.ticketNumberMax >= 0) setTicketNumberMax(cfg.ticketNumberMax); if (typeof cfg.fullscreenAlertEnabled === "boolean") { fullscreenAlertEnabledRef.current = cfg.fullscreenAlertEnabled; setFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); } if (typeof cfg.autoScrollPagesEnabled === "boolean") { autoScrollPagesEnabledRef.current = cfg.autoScrollPagesEnabled; setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); } + if (typeof cfg.displayZoom === "number" && cfg.displayZoom >= 50 && cfg.displayZoom <= 200) { setDisplayZoom(cfg.displayZoom); localStorage.setItem(DISPLAY_ZOOM_KEY, String(cfg.displayZoom)); } if (cfg.stationsEnabled) { stationsEnabledRef.current = true; setStationsEnabled(true); @@ -427,6 +435,8 @@ export default function Display() { if (storedND && ["displayCode", "ticketNumber"].includes(storedND)) setNumberDisplay(storedND); const storedMax = localStorage.getItem(TICKET_NUMBER_MAX_KEY); if (storedMax) { const n = parseInt(storedMax, 10); if (!isNaN(n) && n >= 1) setTicketNumberMax(n); } + const storedZoom = localStorage.getItem(DISPLAY_ZOOM_KEY); + if (storedZoom) { const z = parseInt(storedZoom, 10); if (!isNaN(z) && z >= 50 && z <= 200) setDisplayZoom(z); } fetchOrders(); }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -447,6 +457,7 @@ export default function Display() { if (typeof cfg.ticketNumberMax === "number" && cfg.ticketNumberMax >= 0) setTicketNumberMax(cfg.ticketNumberMax); if (typeof cfg.fullscreenAlertEnabled === "boolean") { fullscreenAlertEnabledRef.current = cfg.fullscreenAlertEnabled; setFullscreenAlertEnabled(cfg.fullscreenAlertEnabled); } if (typeof cfg.autoScrollPagesEnabled === "boolean") { autoScrollPagesEnabledRef.current = cfg.autoScrollPagesEnabled; setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); } + if (typeof cfg.displayZoom === "number" && cfg.displayZoom >= 50 && cfg.displayZoom <= 200) { setDisplayZoom(cfg.displayZoom); localStorage.setItem(DISPLAY_ZOOM_KEY, String(cfg.displayZoom)); } if (typeof cfg.stationsEnabled === "boolean") { stationsEnabledRef.current = cfg.stationsEnabled; setStationsEnabled(cfg.stationsEnabled); @@ -828,6 +839,7 @@ export default function Display() { bottomRows={STATION_HYBRID_ROWS} getOrderLabel={getOrderLabel} autoScrollEnabled={autoScrollPagesEnabled} + displayZoom={displayZoom} />
); @@ -853,6 +865,7 @@ export default function Display() { immediateRemoval getOrderLabel={getOrderLabel} autoScrollEnabled={autoScrollPagesEnabled} + displayZoom={displayZoom} />
); @@ -876,7 +889,8 @@ export default function Display() { cardBgClass="bg-green-100" sectionId={`st-ready-${idx}`} getOrderLabel={getOrderLabel} - autoScrollEnabled={autoScrollPagesEnabled} + autoScrollEnabled={true} + displayZoom={displayZoom} /> ); @@ -898,6 +912,7 @@ export default function Display() { immediateRemoval getOrderLabel={getOrderLabel} autoScrollEnabled={autoScrollPagesEnabled} + displayZoom={displayZoom} />
@@ -910,7 +925,8 @@ export default function Display() { cardBgClass="bg-green-100" sectionId="ready" getOrderLabel={getOrderLabel} - autoScrollEnabled={autoScrollPagesEnabled} + autoScrollEnabled={true} + displayZoom={displayZoom} />
@@ -918,7 +934,7 @@ export default function Display() { /* NORMAL — SINGLE MODE */ ) : (
-
+
{pageOrders.map((order, idx) => order ? ( ) : ( diff --git a/components/settings/DisplayModeSettingsCard.tsx b/components/settings/DisplayModeSettingsCard.tsx index 3ae015d..ebd853a 100644 --- a/components/settings/DisplayModeSettingsCard.tsx +++ b/components/settings/DisplayModeSettingsCard.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; -import { Monitor } from "lucide-react"; +import { Monitor, AlertCircle, Play, Grid2x2, ZoomIn, ZoomOut } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -15,6 +15,7 @@ export type DisplayMode = "ready" | "preparing" | "hybrid"; export const DISPLAY_MODE_KEY = "display-mode"; export const AUTO_SCROLL_PAGES_KEY = "auto-scroll-pages"; +export const DISPLAY_ZOOM_KEY = "display-zoom"; export function DisplayModeSettingsCard() { const { t } = useTranslation(); @@ -26,6 +27,8 @@ export function DisplayModeSettingsCard() { const [savedFullscreenAlertEnabled, setSavedFullscreenAlertEnabled] = useState(true); const [autoScrollPagesEnabled, setAutoScrollPagesEnabled] = useState(true); const [savedAutoScrollPagesEnabled, setSavedAutoScrollPagesEnabled] = useState(true); + const [displayZoom, setDisplayZoom] = useState(100); + const [savedDisplayZoom, setSavedDisplayZoom] = useState(100); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -75,6 +78,11 @@ export function DisplayModeSettingsCard() { setAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); setSavedAutoScrollPagesEnabled(cfg.autoScrollPagesEnabled); } + if (typeof cfg?.displayZoom === "number" && cfg.displayZoom >= 50 && cfg.displayZoom <= 200) { + setDisplayZoom(cfg.displayZoom); + setSavedDisplayZoom(cfg.displayZoom); + localStorage.setItem(DISPLAY_ZOOM_KEY, String(cfg.displayZoom)); + } }) .catch(() => { const stored = localStorage.getItem(DISPLAY_MODE_KEY) as DisplayMode | null; @@ -92,13 +100,15 @@ export function DisplayModeSettingsCard() { await fetch("/api/display-config", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ displayMode: mode, stationsEnabled, fullscreenAlertEnabled, autoScrollPagesEnabled }), + body: JSON.stringify({ displayMode: mode, stationsEnabled, fullscreenAlertEnabled, autoScrollPagesEnabled, displayZoom }), }); setSavedMode(mode); setSavedStationsEnabled(stationsEnabled); setSavedFullscreenAlertEnabled(fullscreenAlertEnabled); setSavedAutoScrollPagesEnabled(autoScrollPagesEnabled); + setSavedDisplayZoom(displayZoom); localStorage.setItem(DISPLAY_MODE_KEY, mode); + localStorage.setItem(DISPLAY_ZOOM_KEY, String(displayZoom)); toast.success(t("settings.displayModeSaved")); } catch { toast.error(t("settings.saveError")); @@ -111,7 +121,8 @@ export function DisplayModeSettingsCard() { mode !== savedMode || stationsEnabled !== savedStationsEnabled || fullscreenAlertEnabled !== savedFullscreenAlertEnabled || - autoScrollPagesEnabled !== savedAutoScrollPagesEnabled; + autoScrollPagesEnabled !== savedAutoScrollPagesEnabled || + displayZoom !== savedDisplayZoom; return ( @@ -164,39 +175,117 @@ export function DisplayModeSettingsCard() {
)} -
-
- - -
-
- - +
+ {/* Zoom Section */} +
+ + {isLoading ? ( + + ) : ( +
+
+ + setDisplayZoom(parseInt(e.target.value, 10))} + disabled={isLoading} + className="flex-1 h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer dark:bg-blue-800" + style={{ + background: `linear-gradient(to right, rgb(96, 165, 250) 0%, rgb(96, 165, 250) ${((displayZoom - 50) / 150) * 100}%, rgb(229, 231, 235) ${((displayZoom - 50) / 150) * 100}%, rgb(229, 231, 235) 100%)` + }} + /> + + + {displayZoom}% + +
+
+ + + +
+
+ )}
-
- - + + {/* Flags Grid 2x2 */} +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
diff --git a/components/settings/DisplayZoomSettingsCard.tsx b/components/settings/DisplayZoomSettingsCard.tsx new file mode 100644 index 0000000..1c64494 --- /dev/null +++ b/components/settings/DisplayZoomSettingsCard.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Label } from "@/components/ui/label"; +import { Zap, ZoomIn, ZoomOut } from "lucide-react"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; + +export const DISPLAY_ZOOM_KEY = "display-zoom"; + +export function DisplayZoomSettingsCard() { + const { t } = useTranslation(); + const [zoom, setZoom] = useState(100); + const [savedZoom, setSavedZoom] = useState(100); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + fetch("/api/display-config") + .then((res) => res.ok ? res.json() : null) + .then((cfg) => { + if (typeof cfg?.displayZoom === "number" && cfg.displayZoom >= 50 && cfg.displayZoom <= 200) { + setZoom(cfg.displayZoom); + setSavedZoom(cfg.displayZoom); + localStorage.setItem(DISPLAY_ZOOM_KEY, String(cfg.displayZoom)); + } else { + const stored = localStorage.getItem(DISPLAY_ZOOM_KEY); + if (stored) { + const parsed = parseInt(stored, 10); + if (!isNaN(parsed) && parsed >= 50 && parsed <= 200) { + setZoom(parsed); + setSavedZoom(parsed); + } + } + } + }) + .catch(() => { + const stored = localStorage.getItem(DISPLAY_ZOOM_KEY); + if (stored) { + const parsed = parseInt(stored, 10); + if (!isNaN(parsed) && parsed >= 50 && parsed <= 200) { + setZoom(parsed); + setSavedZoom(parsed); + } + } + }) + .finally(() => setIsLoading(false)); + }, []); + + const handleSave = async () => { + setIsSaving(true); + try { + await fetch("/api/display-config", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ displayZoom: zoom }), + }); + setSavedZoom(zoom); + localStorage.setItem(DISPLAY_ZOOM_KEY, String(zoom)); + toast.success(t("settings.zoomSaved")); + } catch { + toast.error(t("settings.saveError")); + } finally { + setIsSaving(false); + } + }; + + const hasChanges = zoom !== savedZoom; + + return ( + + +
+ + {t("settings.displayZoom")} +
+ + {t("settings.displayZoomDesc")} + +
+ + {isLoading ? ( + + ) : ( +
+
+ +
+ setZoom(parseInt(e.target.value, 10))} + disabled={isLoading} + className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer dark:bg-blue-800" + style={{ + background: `linear-gradient(to right, rgb(96, 165, 250) 0%, rgb(96, 165, 250) ${((zoom - 50) / 150) * 100}%, rgb(229, 231, 235) ${((zoom - 50) / 150) * 100}%, rgb(229, 231, 235) 100%)` + }} + /> +
+ + + {zoom}% + +
+
+ + + +
+
+ )} + +
+ +
+
+
+ ); +} diff --git a/lib/display-config-store.ts b/lib/display-config-store.ts index 0799ce6..1a5c87b 100644 --- a/lib/display-config-store.ts +++ b/lib/display-config-store.ts @@ -16,6 +16,7 @@ export interface DisplayConfig { stationsEnabled: boolean; fullscreenAlertEnabled: boolean; autoScrollPagesEnabled: boolean; + displayZoom: number; } type Subscriber = (text: string) => void; @@ -32,6 +33,7 @@ const DEFAULT_CONFIG: DisplayConfig = { stationsEnabled: false, fullscreenAlertEnabled: true, autoScrollPagesEnabled: true, + displayZoom: 100, }; // In-memory cache — populated lazily on first read/write @@ -86,7 +88,7 @@ export function updateConfig(patch: Partial): DisplayConfig { if (patch.displayMode !== undefined || patch.eventName !== undefined || patch.numberDisplay !== undefined || patch.ticketNumberMax !== undefined || patch.stationsEnabled !== undefined || patch.fullscreenAlertEnabled !== undefined || - patch.autoScrollPagesEnabled !== undefined) { + patch.autoScrollPagesEnabled !== undefined || patch.displayZoom !== undefined) { configSubscribers.forEach((fn) => { try { fn({ ...cache }); } catch { /* subscriber gone */ } }); diff --git a/lib/i18n/locales/en.ts b/lib/i18n/locales/en.ts index aac027b..dfcd275 100644 --- a/lib/i18n/locales/en.ts +++ b/lib/i18n/locales/en.ts @@ -63,6 +63,7 @@ export const en = { displayModeHybridDesc: "¾ of the page for preparing orders and ¼ for ready orders", display: "Display", displayDesc: "Edit the display page", + displayOptions: "Display options", operativeMode: "Operative mode", operativeModeDesc: "Select what to show on the public display page", saving: "Saving...", @@ -96,6 +97,9 @@ export const en = { fullscreenAlertEnabledDesc: "Show a full-screen overlay when an order is ready", autoScrollPagesEnabled: "Auto-scroll pages", autoScrollPagesEnabledDesc: "Automatically scroll through order pages every 10 seconds", + displayZoom: "Display zoom", + displayZoomDesc: "Zoom in or out to see more or fewer orders", + zoomSaved: "Display zoom saved", allOrders: "All orders" }, session: { diff --git a/lib/i18n/locales/it.ts b/lib/i18n/locales/it.ts index c7863eb..785c1c5 100644 --- a/lib/i18n/locales/it.ts +++ b/lib/i18n/locales/it.ts @@ -63,6 +63,7 @@ export const it = { displayModeHybridDesc: "¾ della pagina per gli ordini in preparazione e ¼ per gli ordini pronti", display: "Display", displayDesc: "Modifica la pagina del display", + displayOptions: "Opzioni display", operativeMode: "Modalità operativa", operativeModeDesc: "Seleziona cosa mostrare nella pagina display pubblica", saving: "Salvataggio...", @@ -96,6 +97,9 @@ export const it = { fullscreenAlertEnabledDesc: "Mostra un overlay a tutto schermo quando un ordine è pronto", autoScrollPagesEnabled: "Scorrimento automatico pagine", autoScrollPagesEnabledDesc: "Scorre automaticamente le pagine di ordini ogni 10 secondi", + displayZoom: "Zoom del display", + displayZoomDesc: "Zoom in o out per vedere più o meno ordini", + zoomSaved: "Zoom del display salvato", allOrders: "Tutti gli ordini" }, session: { From 30ff08ba29518902d636f8237a73a7f288b5f822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Spampatti?= Date: Sat, 9 May 2026 00:35:03 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20UI=20graphi?= =?UTF-8?q?c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/manager/StationCard.tsx | 8 ++++---- components/manager/orders-grid.tsx | 29 ++++++++++++++++------------- 2 files changed, 20 insertions(+), 17 deletions(-) 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..9c0e6f6 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,7 +56,10 @@ 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(() => { @@ -77,7 +82,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 +94,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 +104,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) { if (status === 'COMPLETED') { return ( -
+