feat(menu): per-item print station (cold bar / kitchen / barista)
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m28s

Each menu item can now pick its own print station, overriding the category's —
so a category can fan out to different printers (e.g. a drink → cold bar, a
food → kitchen). Adds MenuItem.KitchenStationId (+ migration, FK SetNull), wires
create/update/DTO, and updates kitchen-ticket routing to group by the item's
station ?? the category's station ?? the branch kitchen printer. Deleting a
station now also clears item assignments. Menu item editor gains a "Print
station" dropdown (default = "same as category"). fa/en/ar added.

Backend built clean via the Nexus mirror; migration applies on deploy (MigrateAsync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-25 10:08:07 +03:30
parent aede5bfd97
commit 27b3ac60c7
14 changed files with 3693 additions and 70 deletions
+2 -1
View File
@@ -917,7 +917,8 @@
"deleteItemSuccess": "تم حذف الصنف",
"deleteCategoryConfirmTitle": "حذف الفئة",
"deleteCategoryConfirmDesc": "هل أنت متأكد من حذف الفئة «{name}»؟",
"deleteCategorySuccess": "تم حذف الفئة"
"deleteCategorySuccess": "تم حذف الفئة",
"printStationInherit": "نفس الفئة"
},
"branchMenu": {
"title": "قائمة الفرع",
+2 -1
View File
@@ -951,7 +951,8 @@
"deleteItemSuccess": "Item deleted",
"deleteCategoryConfirmTitle": "Delete category",
"deleteCategoryConfirmDesc": "Are you sure you want to delete the “{name}” category?",
"deleteCategorySuccess": "Category deleted"
"deleteCategorySuccess": "Category deleted",
"printStationInherit": "Same as category"
},
"branchMenu": {
"title": "Branch Menu",
+2 -1
View File
@@ -951,7 +951,8 @@
"deleteItemSuccess": "آیتم حذف شد",
"deleteCategoryConfirmTitle": "حذف دسته‌بندی",
"deleteCategoryConfirmDesc": "آیا از حذف دسته «{name}» مطمئن هستید؟",
"deleteCategorySuccess": "دسته حذف شد"
"deleteCategorySuccess": "دسته حذف شد",
"printStationInherit": "مثل دستهٔ منو"
},
"branchMenu": {
"title": "منوی شعبه",
@@ -73,6 +73,7 @@ interface MenuItem {
videoUrl?: string;
model3dUrl?: string;
isAvailable: boolean;
kitchenStationId?: string | null;
}
interface ItemForm {
@@ -84,6 +85,7 @@ interface ItemForm {
imageUrl: string;
videoUrl: string;
model3dUrl: string;
kitchenStationId: string;
}
interface CatForm {
@@ -114,6 +116,7 @@ const defaultItemForm: ItemForm = {
imageUrl: "",
videoUrl: "",
model3dUrl: "",
kitchenStationId: "",
};
const defaultCatForm: CatForm = {
@@ -296,6 +299,7 @@ export function MenuAdminScreen() {
imageUrl: itemForm.imageUrl || null,
videoUrl: itemForm.videoUrl || null,
model3dUrl: itemForm.model3dUrl || null,
kitchenStationId: itemForm.kitchenStationId || null,
}),
onSuccess: () => {
setItemModalOpen(false);
@@ -314,6 +318,7 @@ export function MenuAdminScreen() {
imageUrl: mediaField(itemForm.imageUrl),
videoUrl: mediaField(itemForm.videoUrl),
model3dUrl: mediaField(itemForm.model3dUrl),
kitchenStationId: itemForm.kitchenStationId || null,
}),
onSuccess: () => {
setItemModalOpen(false);
@@ -411,6 +416,7 @@ export function MenuAdminScreen() {
imageUrl: item.imageUrl ?? "",
videoUrl: item.videoUrl ?? "",
model3dUrl: item.model3dUrl ?? "",
kitchenStationId: item.kitchenStationId ?? "",
});
setItemModalOpen(true);
};
@@ -956,6 +962,26 @@ export function MenuAdminScreen() {
) : null}
</LabeledField>
{stations.length > 0 ? (
<LabeledField label={t("printStation")} htmlFor="modal-item-station">
<select
id="modal-item-station"
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
value={itemForm.kitchenStationId}
onChange={(e) =>
setItemForm((f) => ({ ...f, kitchenStationId: e.target.value }))
}
>
<option value="">{t("printStationInherit")}</option>
{stations.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</LabeledField>
) : null}
{/* Actions */}
<div className="flex items-center justify-between gap-2 border-t border-border pt-4">
{editingItem ? (