feat(kds): filter the kitchen display by station (kitchen / bar)
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Has been cancelled
CI/CD / CI · Admin Web (tsc) (push) Has been cancelled
CI/CD / CI · Website (tsc) (push) Has been cancelled
CI/CD / CI · Koja (tsc) (push) Has been cancelled
CI/CD / Deploy · all services (push) Has been cancelled

Complements the separate kitchen/bar printers with an on-screen split. The live
order DTO now carries each item's prep station (MenuItem → Category →
KitchenStation), and the KDS shows station tabs (All / Kitchen / Bar / …) that
appear only once ≥2 stations are in play. Selecting a station shows just the
tickets — and just the items within each ticket — for that station, so bar staff
see drinks and kitchen staff see food. Single-station cafés see the board
unchanged. fa/en/ar strings added.

Note: order status is still per-order (one advance button); the split is for
viewing/printing, not per-item status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 10:27:40 +03:30
parent 8703e9cf87
commit 958addf734
7 changed files with 64 additions and 4 deletions
+5 -1
View File
@@ -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);
+5 -1
View File
@@ -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);
}
+2
View File
@@ -716,6 +716,8 @@
"loading": "جاري التحميل...",
"live": "مباشر",
"polling": "تحديث دوري",
"allStations": "الكل",
"defaultStation": "المطبخ",
"advance": "المرحلة التالية",
"status": {
"Pending": "قيد الانتظار",
+2
View File
@@ -750,6 +750,8 @@
"loading": "Loading...",
"live": "Live",
"polling": "Polling",
"allStations": "All",
"defaultStation": "Kitchen",
"advance": "Next step",
"status": {
"Pending": "Pending",
+2
View File
@@ -750,6 +750,8 @@
"loading": "در حال بارگذاری...",
"live": "زنده",
"polling": "به‌روزرسانی دوره‌ای",
"allStations": "همه",
"defaultStation": "آشپزخانه",
"advance": "مرحله بعد",
"status": {
"Pending": "در انتظار",
@@ -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<string, string> = {
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<string>("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<string, string>();
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() {
</span>
</div>
{showStationTabs ? (
<div className="flex flex-wrap gap-1.5">
{[{ id: "all", name: t("allStations") }, ...stationOptions].map((s) => (
<button
key={s.id}
type="button"
onClick={() => setStation(s.id)}
className={cn(
"rounded-lg border px-3 py-1.5 text-sm font-medium transition",
station === s.id
? "border-[#0F6E56] bg-[#E1F5EE] text-[#0F6E56]"
: "border-border bg-card text-muted-foreground hover:text-foreground"
)}
>
{s.name}
</button>
))}
</div>
) : null}
{isLoading ? (
<p className="text-muted-foreground">{t("loading")}</p>
) : orders.length === 0 ? (
@@ -148,6 +190,7 @@ export function KdsScreen() {
<h3 className="font-semibold">{col.label}</h3>
{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() {
</CardHeader>
<CardContent className="space-y-2">
<ul className="text-sm">
{order.items.map((item) => (
{itemsForStation(order.items).map((item) => (
<li key={item.id}>
{formatNumber(item.quantity, numberLocale)}×{" "}
{item.menuItemName}
+3
View File
@@ -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 {