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
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:
@@ -10,7 +10,11 @@ public record OrderItemDto(
|
|||||||
decimal UnitPrice,
|
decimal UnitPrice,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
bool IsVoided = false,
|
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);
|
public record TransferTableRequest(string TargetTableId);
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ public class OrderService : IOrderService
|
|||||||
var orders = await _db.Orders
|
var orders = await _db.Orders
|
||||||
.Include(o => o.Items)
|
.Include(o => o.Items)
|
||||||
.ThenInclude(i => i.MenuItem)
|
.ThenInclude(i => i.MenuItem)
|
||||||
|
.ThenInclude(m => m.Category)
|
||||||
|
.ThenInclude(c => c.KitchenStation)
|
||||||
.Include(o => o.Table)
|
.Include(o => o.Table)
|
||||||
.Where(o => o.CafeId == cafeId && LiveStatuses.Contains(o.Status))
|
.Where(o => o.CafeId == cafeId && LiveStatuses.Contains(o.Status))
|
||||||
.OrderBy(o => o.CreatedAt)
|
.OrderBy(o => o.CreatedAt)
|
||||||
@@ -1345,6 +1347,8 @@ public class OrderService : IOrderService
|
|||||||
i.UnitPrice,
|
i.UnitPrice,
|
||||||
i.Notes,
|
i.Notes,
|
||||||
i.IsVoided,
|
i.IsVoided,
|
||||||
i.VoidedAt)).ToList(),
|
i.VoidedAt,
|
||||||
|
i.MenuItem?.Category?.KitchenStationId,
|
||||||
|
i.MenuItem?.Category?.KitchenStation?.Name)).ToList(),
|
||||||
o.Source);
|
o.Source);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -716,6 +716,8 @@
|
|||||||
"loading": "جاري التحميل...",
|
"loading": "جاري التحميل...",
|
||||||
"live": "مباشر",
|
"live": "مباشر",
|
||||||
"polling": "تحديث دوري",
|
"polling": "تحديث دوري",
|
||||||
|
"allStations": "الكل",
|
||||||
|
"defaultStation": "المطبخ",
|
||||||
"advance": "المرحلة التالية",
|
"advance": "المرحلة التالية",
|
||||||
"status": {
|
"status": {
|
||||||
"Pending": "قيد الانتظار",
|
"Pending": "قيد الانتظار",
|
||||||
|
|||||||
@@ -750,6 +750,8 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"polling": "Polling",
|
"polling": "Polling",
|
||||||
|
"allStations": "All",
|
||||||
|
"defaultStation": "Kitchen",
|
||||||
"advance": "Next step",
|
"advance": "Next step",
|
||||||
"status": {
|
"status": {
|
||||||
"Pending": "Pending",
|
"Pending": "Pending",
|
||||||
|
|||||||
@@ -750,6 +750,8 @@
|
|||||||
"loading": "در حال بارگذاری...",
|
"loading": "در حال بارگذاری...",
|
||||||
"live": "زنده",
|
"live": "زنده",
|
||||||
"polling": "بهروزرسانی دورهای",
|
"polling": "بهروزرسانی دورهای",
|
||||||
|
"allStations": "همه",
|
||||||
|
"defaultStation": "آشپزخانه",
|
||||||
"advance": "مرحله بعد",
|
"advance": "مرحله بعد",
|
||||||
"status": {
|
"status": {
|
||||||
"Pending": "در انتظار",
|
"Pending": "در انتظار",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslations, useLocale } from "next-intl";
|
import { useTranslations, useLocale } from "next-intl";
|
||||||
import * as signalR from "@microsoft/signalr";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const DEFAULT_STATION = "__default__";
|
||||||
|
|
||||||
const STATUS_FLOW: Record<string, string> = {
|
const STATUS_FLOW: Record<string, string> = {
|
||||||
Pending: "Confirmed",
|
Pending: "Confirmed",
|
||||||
Confirmed: "Preparing",
|
Confirmed: "Preparing",
|
||||||
@@ -70,6 +72,7 @@ export function KdsScreen() {
|
|||||||
const cafeId = useAuthStore((s) => s.user?.cafeId);
|
const cafeId = useAuthStore((s) => s.user?.cafeId);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [station, setStation] = useState<string>("all");
|
||||||
|
|
||||||
const { data: orders = [], isLoading } = useQuery({
|
const { data: orders = [], isLoading } = useQuery({
|
||||||
queryKey: ["orders-live", cafeId],
|
queryKey: ["orders-live", cafeId],
|
||||||
@@ -120,6 +123,25 @@ export function KdsScreen() {
|
|||||||
onSuccess: () => refresh(),
|
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;
|
if (!cafeId) return null;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -137,6 +159,26 @@ export function KdsScreen() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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 ? (
|
{isLoading ? (
|
||||||
<p className="text-muted-foreground">{t("loading")}</p>
|
<p className="text-muted-foreground">{t("loading")}</p>
|
||||||
) : orders.length === 0 ? (
|
) : orders.length === 0 ? (
|
||||||
@@ -148,6 +190,7 @@ export function KdsScreen() {
|
|||||||
<h3 className="font-semibold">{col.label}</h3>
|
<h3 className="font-semibold">{col.label}</h3>
|
||||||
{orders
|
{orders
|
||||||
.filter((o) => col.statuses.includes(o.status))
|
.filter((o) => col.statuses.includes(o.status))
|
||||||
|
.filter((o) => itemsForStation(o.items).length > 0)
|
||||||
.map((order) => {
|
.map((order) => {
|
||||||
const nextStatus = STATUS_FLOW[order.status];
|
const nextStatus = STATUS_FLOW[order.status];
|
||||||
return (
|
return (
|
||||||
@@ -174,7 +217,7 @@ export function KdsScreen() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<ul className="text-sm">
|
<ul className="text-sm">
|
||||||
{order.items.map((item) => (
|
{itemsForStation(order.items).map((item) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
{formatNumber(item.quantity, numberLocale)}×{" "}
|
{formatNumber(item.quantity, numberLocale)}×{" "}
|
||||||
{item.menuItemName}
|
{item.menuItemName}
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export interface OrderItemLine {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
isVoided?: boolean;
|
isVoided?: boolean;
|
||||||
voidedAt?: string | null;
|
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 {
|
export interface PaymentLine {
|
||||||
|
|||||||
Reference in New Issue
Block a user