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
6 changes: 6 additions & 0 deletions app/api/display-config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 27 additions & 7 deletions app/api/events/display/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
541 changes: 266 additions & 275 deletions app/display/page.tsx

Large diffs are not rendered by default.

404 changes: 172 additions & 232 deletions app/manager/page.tsx

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions components/manager/StationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export function StationCard({
</div>
<div className="flex-1 overflow-y-auto px-4 pb-4">
<div className="flex gap-3 flex-wrap items-start place-content-start">
{confirmedOrders.map(order => (
<div key={`${order.id}-${stationId}`} className="min-w-max">
{[...confirmedOrders].sort((a, b) => a.ticketNumber - b.ticketNumber).map(order => (
<div key={`${order.id}-${stationId}`} className="min-w-28">
<OrderCard order={order} status="CONFIRMED" onNext={onConfirmedNext} />
</div>
))}
Expand All @@ -70,8 +70,8 @@ export function StationCard({
</div>
<div className="flex-1 overflow-y-auto px-4 pb-4">
<div className="flex gap-3 flex-wrap items-start place-content-start">
{completedOrders.map(order => (
<div key={`${order.id}-${stationId}`} className="min-w-max">
{[...completedOrders].sort((a, b) => a.ticketNumber - b.ticketNumber).map(order => (
<div key={`${order.id}-${stationId}`} className="min-w-20">
<OrderCard order={order} status="COMPLETED" onPrev={onCompletedPrev} onNext={onCompletedNext} />
</div>
))}
Expand Down
58 changes: 41 additions & 17 deletions components/manager/orders-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={cn("select-none h-full w-full rounded-xl outline-2 outline-secondary bg-card shadow-lg overflow-hidden flex flex-col", className)}>
<div className="flex-1 overflow-y-auto px-4 pb-4">
Expand All @@ -34,8 +36,8 @@ export default function OrdersGrid({ className, orders, title, status, stationId
}
<div className="flex gap-3 flex-wrap items-start place-content-start">
{
orders.map((order) => (
<div key={stationId ? `${order.id}-${stationId}` : order.id} className="min-w-max">
sortedOrders.map((order) => (
<div key={stationId ? `${order.id}-${stationId}` : order.id} className="w-40">
<OrderCard order={order} status={status} onPrev={onPrev} onNext={onNext} />
</div>
))
Expand All @@ -54,14 +56,38 @@ interface OrderCardProps {
}

export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
const [numberDisplay, setNumberDisplay] = useState<NumberDisplay>("displayCode");
const [numberDisplay, setNumberDisplay] = useState<NumberDisplay>(() => {
const stored = localStorage.getItem(NUMBER_DISPLAY_KEY) as NumberDisplay | null;
return (stored && ["displayCode", "ticketNumber"].includes(stored)) ? stored : "displayCode";
});
const [ticketNumberMax, setTicketNumberMax] = useState<number>(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() {
Expand All @@ -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);
Expand All @@ -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}
Expand All @@ -99,7 +125,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {

if (status === 'COMPLETED') {
return (
<div className="flex h-16 rounded-md overflow-hidden shadow-sm border border-input dark:border-input">
<div className="w-full flex h-16 rounded-md overflow-hidden shadow-sm border border-input dark:border-input">
<Button
variant="ghost"
className="select-none h-full w-12 shrink-0 rounded-none border-r border-input dark:border-input hover:bg-red-500 hover:text-white dark:hover:bg-red-600 transition-colors"
Expand All @@ -111,7 +137,7 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
<Button
variant="ghost"
onClick={addNext}
className="select-none h-full px-4 rounded-none text-3xl font-bold font-mono whitespace-nowrap bg-green-500 text-white hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 transition-colors"
className="min-w-28 select-none h-full px-4 rounded-none text-3xl font-bold font-mono whitespace-nowrap bg-green-500 text-white hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 transition-colors"
disabled={!onNext}
>
{orderTitle}
Expand All @@ -124,16 +150,14 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
return (
<div className="flex h-16 rounded-md overflow-hidden shadow-sm border border-input dark:border-input">
<Button
variant="ghost"
variant="outline"
className="select-none h-full w-12 shrink-0 rounded-none border-r border-input dark:border-input hover:bg-red-500 hover:text-white dark:hover:bg-red-600 transition-colors"
onClick={undoPrev}
disabled={!onPrev}
>
<Undo2 className="h-5 w-5" />
</Button>
<Button
variant="ghost"
className="select-none h-full px-4 rounded-none text-3xl font-bold font-mono whitespace-nowrap bg-green-500 text-white dark:bg-green-600 transition-colors opacity-70 cursor-not-allowed"
className="min-w-28 h-full px-4 rounded-none text-3xl font-bold font-mono whitespace-nowrap bg-blue-400 hover:bg-blue-500 text-white"
disabled
>
{orderTitle}
Expand All @@ -146,8 +170,8 @@ export function OrderCard({ order, status, onPrev, onNext }: OrderCardProps) {
<Card
key={order.id}
>
<CardContent className="p-4 flex items-center justify-center h-full">
<p className="text-8xl font-bold font-mono m-0">{orderTitle}</p>
<CardContent className="p-4 flex items-center justify-center h-16">
<p className="text-3xl font-bold font-mono m-0">{orderTitle}</p>
</CardContent>
</Card>
)
Expand Down
Loading
Loading