diff --git a/src/Meezi.API/Models/Orders/OrderDtos.cs b/src/Meezi.API/Models/Orders/OrderDtos.cs index 2422f5f..8219aeb 100644 --- a/src/Meezi.API/Models/Orders/OrderDtos.cs +++ b/src/Meezi.API/Models/Orders/OrderDtos.cs @@ -10,7 +10,11 @@ public record OrderItemDto( decimal UnitPrice, string? Notes, bool IsVoided = false, - DateTime? VoidedAt = null); + DateTime? VoidedAt = null, + // Prep station the item routes to (Kitchen/Bar). Populated on the live/KDS + // path only; null elsewhere (= the branch kitchen / no station). + string? StationId = null, + string? StationName = null); public record TransferTableRequest(string TargetTableId); diff --git a/src/Meezi.API/Services/OrderService.cs b/src/Meezi.API/Services/OrderService.cs index 601ab44..1ed91a0 100644 --- a/src/Meezi.API/Services/OrderService.cs +++ b/src/Meezi.API/Services/OrderService.cs @@ -172,6 +172,8 @@ public class OrderService : IOrderService var orders = await _db.Orders .Include(o => o.Items) .ThenInclude(i => i.MenuItem) + .ThenInclude(m => m.Category) + .ThenInclude(c => c.KitchenStation) .Include(o => o.Table) .Where(o => o.CafeId == cafeId && LiveStatuses.Contains(o.Status)) .OrderBy(o => o.CreatedAt) @@ -1345,6 +1347,8 @@ public class OrderService : IOrderService i.UnitPrice, i.Notes, i.IsVoided, - i.VoidedAt)).ToList(), + i.VoidedAt, + i.MenuItem?.Category?.KitchenStationId, + i.MenuItem?.Category?.KitchenStation?.Name)).ToList(), o.Source); } diff --git a/web/dashboard/messages/ar.json b/web/dashboard/messages/ar.json index b80c392..221dae1 100644 --- a/web/dashboard/messages/ar.json +++ b/web/dashboard/messages/ar.json @@ -716,6 +716,8 @@ "loading": "جاري التحميل...", "live": "مباشر", "polling": "تحديث دوري", + "allStations": "الكل", + "defaultStation": "المطبخ", "advance": "المرحلة التالية", "status": { "Pending": "قيد الانتظار", diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index aac6b77..301c1f5 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -750,6 +750,8 @@ "loading": "Loading...", "live": "Live", "polling": "Polling", + "allStations": "All", + "defaultStation": "Kitchen", "advance": "Next step", "status": { "Pending": "Pending", diff --git a/web/dashboard/messages/fa.json b/web/dashboard/messages/fa.json index 724a771..118d563 100644 --- a/web/dashboard/messages/fa.json +++ b/web/dashboard/messages/fa.json @@ -750,6 +750,8 @@ "loading": "در حال بارگذاری...", "live": "زنده", "polling": "به‌روزرسانی دوره‌ای", + "allStations": "همه", + "defaultStation": "آشپزخانه", "advance": "مرحله بعد", "status": { "Pending": "در انتظار", diff --git a/web/dashboard/src/components/kds/kds-screen.tsx b/web/dashboard/src/components/kds/kds-screen.tsx index aae1f4b..e558d9e 100644 --- a/web/dashboard/src/components/kds/kds-screen.tsx +++ b/web/dashboard/src/components/kds/kds-screen.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslations, useLocale } from "next-intl"; import * as signalR from "@microsoft/signalr"; @@ -13,6 +13,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { cn } from "@/lib/utils"; +const DEFAULT_STATION = "__default__"; + const STATUS_FLOW: Record = { Pending: "Confirmed", Confirmed: "Preparing", @@ -70,6 +72,7 @@ export function KdsScreen() { const cafeId = useAuthStore((s) => s.user?.cafeId); const queryClient = useQueryClient(); const [connected, setConnected] = useState(false); + const [station, setStation] = useState("all"); const { data: orders = [], isLoading } = useQuery({ queryKey: ["orders-live", cafeId], @@ -120,6 +123,25 @@ export function KdsScreen() { onSuccess: () => refresh(), }); + // Group live items by their prep station (Kitchen/Bar/…). Items with no station + // bucket under a default "Kitchen" tab. Tabs only appear once ≥2 stations are + // actually in play, so single-station cafés see the board unchanged. + const stationOptions = useMemo(() => { + const map = new Map(); + for (const o of orders) + for (const it of o.items) { + const id = it.stationId ?? DEFAULT_STATION; + if (!map.has(id)) map.set(id, it.stationName ?? t("defaultStation")); + } + return Array.from(map, ([id, name]) => ({ id, name })); + }, [orders, t]); + const showStationTabs = stationOptions.length > 1; + + const itemsForStation = (items: LiveOrder["items"]) => + station === "all" || !showStationTabs + ? items + : items.filter((it) => (it.stationId ?? DEFAULT_STATION) === station); + if (!cafeId) return null; const columns = [ @@ -137,6 +159,26 @@ export function KdsScreen() { + {showStationTabs ? ( +
+ {[{ id: "all", name: t("allStations") }, ...stationOptions].map((s) => ( + + ))} +
+ ) : null} + {isLoading ? (

{t("loading")}

) : orders.length === 0 ? ( @@ -148,6 +190,7 @@ export function KdsScreen() {

{col.label}

{orders .filter((o) => col.statuses.includes(o.status)) + .filter((o) => itemsForStation(o.items).length > 0) .map((order) => { const nextStatus = STATUS_FLOW[order.status]; return ( @@ -174,7 +217,7 @@ export function KdsScreen() {
    - {order.items.map((item) => ( + {itemsForStation(order.items).map((item) => (
  • {formatNumber(item.quantity, numberLocale)}×{" "} {item.menuItemName} diff --git a/web/dashboard/src/lib/api/types.ts b/web/dashboard/src/lib/api/types.ts index a0c6061..b29414c 100644 --- a/web/dashboard/src/lib/api/types.ts +++ b/web/dashboard/src/lib/api/types.ts @@ -82,6 +82,9 @@ export interface OrderItemLine { notes?: string; isVoided?: boolean; voidedAt?: string | null; + /** Prep station (Kitchen/Bar) the item routes to; only present on live/KDS data. */ + stationId?: string | null; + stationName?: string | null; } export interface PaymentLine {