diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/CafeJourney.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/CafeJourney.razor new file mode 100644 index 00000000..ff48d0ec --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/CafeJourney.razor @@ -0,0 +1,253 @@ +@* + EN: Cafe Journey — End-to-end café workflow tracker: Đặt món → Thanh toán → Pha chế → Phục vụ → Hoàn tất. + VI: Hành trình Café — Theo dõi quy trình từ đầu đến cuối: Đặt món → Thanh toán → Pha chế → Phục vụ → Hoàn tất. +*@ +@page "/pos/cafe/cafe-journey" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + Hành trình Café + + Đơn #CF-0027 +
+ + @* ═══ STEP TRACKER / THANH BƯỚC ═══ *@ +
+
+ @for (int i = 0; i < _steps.Count; i++) + { + var step = _steps[i]; + var stepIdx = i; + var isActive = stepIdx == _currentStep; + var isCompleted = stepIdx < _currentStep; + var bgColor = isCompleted ? "var(--pos-success)" : isActive ? "var(--pos-orange-primary)" : "var(--pos-bg-interactive)"; + var fgColor = isCompleted || isActive ? "#FFF" : "var(--pos-text-tertiary)"; + + @* EN: Step circle / VI: Vòng tròn bước *@ +
+
+ @if (isCompleted) + { + + } + else + { + + } +
+
+ @step.Label +
+
+ + @* EN: Connector line / VI: Đường nối *@ + @if (stepIdx < _steps.Count - 1) + { +
+ } + } +
+
+ + @* ═══ STEP CONTENT / NỘI DUNG BƯỚC ═══ *@ +
+ @switch (_currentStep) + { + case 0: + @* ═══ ĐẶT MÓN / ORDER STEP ═══ *@ +
+
+ Đặt món +
+
+ @foreach (var item in _orderItems) + { +
+
+
@item.Name
+
x@item.Qty
+
+ @FormatPrice(item.Price * item.Qty) +
+ } +
+
+ Tổng (@_orderItems.Sum(i => i.Qty) món) + @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty)) +
+
+ break; + + case 1: + @* ═══ THANH TOÁN / PAYMENT STEP ═══ *@ +
+
+ Thanh toán +
+
+
+ Phương thức + Tiền mặt +
+
+ Tổng tiền + @FormatPrice(125_000) +
+
+ Khách đưa + @FormatPrice(150_000) +
+
+ Tiền thừa + @FormatPrice(25_000) +
+
+
+ break; + + case 2: + @* ═══ PHA CHẾ / BARISTA STEP ═══ *@ +
+
+ Pha chế +
+
+
+ Barista + Trần Minh Tú +
+
+ Thời gian ước tính + 3 phút +
+
+ Trạng thái + Đang pha chế +
+
+
+
+ + Đang pha 3 món... +
+
+
+ break; + + case 3: + @* ═══ PHỤC VỤ / SERVING STEP ═══ *@ +
+
+ Phục vụ +
+
+ #027 +
+
Số thứ tự
+
Vui lòng chờ gọi số tại quầy
+
+ break; + + case 4: + @* ═══ HOÀN TẤT / COMPLETE STEP ═══ *@ +
+
+ +
+
Hoàn tất!
+
+ Đơn hàng #CF-0027 đã hoàn thành +
+
+ 3 món · Tổng: @FormatPrice(125_000) · Tiền mặt +
+
+ + +
+
+ break; + } +
+ + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ @if (_currentStep > 0) + { + + } + + @if (_currentStep < _steps.Count - 1) + { + + } + else + { + + } +
+
+ +@* EN: Pulse animation / VI: Hiệu ứng nhấp nháy *@ + + +@code { + private int _currentStep = 0; + + // EN: Journey steps / VI: Các bước hành trình + private readonly List _steps = new() + { + new("Đặt món", "clipboard-list"), + new("Thanh toán", "credit-card"), + new("Pha chế", "coffee"), + new("Phục vụ", "bell"), + new("Hoàn tất", "check-circle"), + }; + + // EN: Demo order items / VI: Các món trong đơn mẫu + private readonly List _orderItems = new() + { + new("Cà phê sữa đá", 35_000, 2), + new("Bánh mì bơ tỏi", 25_000, 1), + new("Trà đào cam sả", 30_000, 1), + }; + + private record StepInfo(string Label, string Icon); + private record OrderItem(string Name, decimal Price, int Qty); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/MilkFoamOptions.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/MilkFoamOptions.razor new file mode 100644 index 00000000..3935f8dd --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/Workflow/MilkFoamOptions.razor @@ -0,0 +1,188 @@ +@* + EN: Milk Foam Options — Milk type, foam level, temperature, extras for drink customization. + VI: Tùy chọn sữa & foam — Loại sữa, mức foam, nhiệt độ, thêm topping cho đồ uống. +*@ +@page "/pos/cafe/milk-foam-options" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ CUSTOMIZATION PANEL / PANEL TÙY CHỈNH ═══ *@ +
+
+ @* EN: Product header / VI: Tiêu đề sản phẩm *@ +
+ +
+ +
+
+
@_productName
+
@FormatPrice(_basePrice + _extraPrice)
+
+
+ + @* ═══ MILK TYPE / LOẠI SỮA ═══ *@ +
+
+ Loại sữa +
+
+ @foreach (var milk in _milkTypes) + { + + } +
+
+ + @* ═══ FOAM LEVEL / MỨC FOAM ═══ *@ +
+
+ Mức foam +
+
+ @foreach (var foam in _foamLevels) + { + + } +
+
+ + @* ═══ TEMPERATURE / NHIỆT ĐỘ ═══ *@ +
+
+ Nhiệt độ +
+
+ @foreach (var temp in _temperatures) + { + + } +
+
+ + @* ═══ EXTRAS / THÊM ═══ *@ +
+
+ Thêm +
+
+ @foreach (var extra in _extras) + { + var isSelected = _selectedExtras.Contains(extra.Name); + + } +
+
+
+
+ + @* ═══ SUMMARY PANEL / PANEL TÓM TẮT ═══ *@ +
+
Tóm tắt
+
+
Sữa: @_selectedMilk
+
Foam: @_selectedFoam
+
Nhiệt độ: @_selectedTemp
+
Thêm: @(_selectedExtras.Any() ? string.Join(", ", _selectedExtras) : "Không")
+
+
+
+ Tổng + @FormatPrice(_basePrice + _extraPrice) +
+ +
+
+
+ +@code { + private string _productName = "Latte"; + private decimal _basePrice = 45_000; + private decimal _extraPrice = 0; + private string _selectedMilk = "Sữa tươi"; + private string _selectedFoam = "Vừa"; + private string _selectedTemp = "Nóng"; + private readonly HashSet _selectedExtras = new(); + + // EN: Milk type options / VI: Các loại sữa + private readonly List _milkTypes = new() + { + new("Sữa tươi", 0), + new("Sữa đặc", 0), + new("Sữa yến mạch", 15_000), + new("Sữa hạnh nhân", 15_000), + new("Sữa dừa", 10_000), + }; + + private readonly string[] _foamLevels = { "Nhiều foam", "Vừa", "Ít foam", "Không foam" }; + + // EN: Temperature options / VI: Các mức nhiệt độ + private readonly List _temperatures = new() + { + new("Nóng", "🔥"), + new("Lạnh", "🧊"), + new("Ấm", "☕"), + }; + + // EN: Extra options / VI: Tùy chọn thêm + private readonly List _extras = new() + { + new("Whipped cream", 10_000), + new("Caramel drizzle", 5_000), + new("Chocolate sauce", 5_000), + new("Cinnamon", 3_000), + }; + + private void SelectMilk(MilkOption milk) + { + _selectedMilk = milk.Name; + RecalcExtra(); + } + + private void ToggleExtra(ExtraOption extra) + { + if (!_selectedExtras.Remove(extra.Name)) + _selectedExtras.Add(extra.Name); + RecalcExtra(); + } + + private void RecalcExtra() + { + var milkExtra = _milkTypes.First(m => m.Name == _selectedMilk).Extra; + var extrasTotal = _extras.Where(e => _selectedExtras.Contains(e.Name)).Sum(e => e.Price); + _extraPrice = milkExtra + extrasTotal; + } + + private void Confirm() => NavigateTo("cafe"); + + private record MilkOption(string Name, decimal Extra); + private record TempOption(string Label, string Emoji); + private record ExtraOption(string Name, decimal Price); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/AllergenWarning.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/AllergenWarning.razor new file mode 100644 index 00000000..e9400a4d --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/AllergenWarning.razor @@ -0,0 +1,166 @@ +@* + EN: Allergen Warning — Allergen alert display with toggles, severity levels, customer profile. + VI: Cảnh báo dị ứng — Hiển thị cảnh báo dị ứng với toggle, mức độ nghiêm trọng, hồ sơ khách. +*@ +@page "/pos/restaurant/allergen-warning" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Cảnh báo dị ứng + + + + 2 chất gây dị ứng + +
+ + @* ═══ CURRENT ITEM / MÓN HIỆN TẠI ═══ *@ +
+
+
+ +
+
+
@_currentItem
+
@FormatPrice(65_000)
+
+
+ @foreach (var tag in _itemAllergens) + { + @tag + } +
+
+
+ +
+ @* ═══ ALLERGEN GRID / LƯỚI CHẤT GÂY DỊ ỨNG ═══ *@ +
+
Chất gây dị ứng phổ biến
+
+ @foreach (var allergen in _allergens) + { + var isActive = _activeAllergens.Contains(allergen.Name); +
+
@allergen.Icon
+
@allergen.Name
+ @if (isActive) + { +
+ @SeverityLabel(allergen.Severity) +
+ } +
+ } +
+
+ + @* ═══ SEVERITY LEGEND / CHÚ THÍCH MỨC ĐỘ ═══ *@ +
+
Mức độ nghiêm trọng
+
+
+
Cao
+
Nguy hiểm
+
+
+
Trung bình
+
Cẩn thận
+
+
+
Thấp
+
Lưu ý
+
+
+
+ + @* ═══ CUSTOMER PROFILE / HỒ SƠ KHÁCH ═══ *@ +
+
+ Hồ sơ dị ứng khách hàng +
+
+
+ B +
+
+
Trần Thị B
+
Dị ứng: Hải sản, Đậu phộng
+
+ Cao +
+
+
+ + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ + +
+
+ +@code { + private string _currentItem = "Gỏi cuốn tôm"; + private readonly string[] _itemAllergens = { "Hải sản", "Đậu phộng" }; + private readonly HashSet _activeAllergens = new() { "Hải sản", "Đậu phộng" }; + + // EN: Common allergens / VI: Chất gây dị ứng phổ biến + private readonly List _allergens = new() + { + new("Đậu phộng", "🥜", "high"), + new("Hải sản", "🦐", "high"), + new("Sữa", "🥛", "medium"), + new("Trứng", "🥚", "medium"), + new("Lúa mì", "🌾", "low"), + new("Đậu nành", "🫘", "low"), + new("Hạt cây", "🌰", "medium"), + new("Cá", "🐟", "high"), + }; + + private void ToggleAllergen(string name) + { + if (!_activeAllergens.Remove(name)) + _activeAllergens.Add(name); + } + + private void ConfirmAllergens() => NavigateTo("restaurant"); + + private static string SeverityColor(string s) => s switch + { + "high" => "var(--pos-danger)", "medium" => "var(--pos-orange-primary)", + "low" => "var(--pos-warning)", _ => "var(--pos-text-tertiary)" + }; + + private static string SeverityBg(string s) => s switch + { + "high" => "rgba(239,68,68,.1)", "medium" => "rgba(255,92,0,.1)", + "low" => "rgba(245,158,11,.1)", _ => "var(--pos-bg-interactive)" + }; + + private static string SeverityLabel(string s) => s switch + { + "high" => "Cao", "medium" => "Trung bình", "low" => "Thấp", _ => s + }; + + private record AllergenInfo(string Name, string Icon, string Severity); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/CourseTiming.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/CourseTiming.razor new file mode 100644 index 00000000..4fb6514c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/CourseTiming.razor @@ -0,0 +1,153 @@ +@* + EN: Course Timing — Multi-course meal timing management with fire buttons and timeline. + VI: Quản lý thời gian course — Quản lý thời gian tiệc nhiều món với nút fire và timeline. +*@ +@page "/pos/restaurant/course-timing" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Bàn 7 — Tiệc 5 món + + + + Bắt đầu: 18:30 + +
+ + @* ═══ AUTO-FIRE RULE / QUY TẮC TỰ ĐỘNG ═══ *@ +
+
+ + Tự động: Phục vụ mỗi món cách 15 phút +
+
+ + @* ═══ COURSE LIST / DANH SÁCH COURSE ═══ *@ +
+
+ @foreach (var course in _courses) + { + var statusColor = CourseStatusColor(course.Status); + var statusLabel = CourseStatusLabel(course.Status); + +
+ @* EN: Course header / VI: Tiêu đề course *@ +
+
+ +
+
+
@course.Name
+
@course.Items
+
+
+ + @statusLabel + +
+
+ + @* EN: Timeline bar / VI: Thanh thời gian *@ +
+
+ @course.EstTime phút + @course.Progress% +
+
+
+
+
+ + @* EN: Fire button / VI: Nút kích hoạt *@ + @if (course.Status == "queued") + { + + } + else if (course.Status == "cooking") + { + + } +
+ } +
+
+ + @* ═══ FOOTER STATS / THỐNG KÊ ═══ *@ +
+ Đã phục vụ: @_courses.Count(c => c.Status == "served") + Đang nấu: @_courses.Count(c => c.Status == "cooking") + Chờ: @_courses.Count(c => c.Status == "queued") + Tổng: @_courses.Count course +
+
+ +@code { + // EN: Course list / VI: Danh sách course + private readonly List _courses = new() + { + new("Khai vị", "Gỏi cuốn tôm, Chả giò chiên", "salad", 15, "served", 100), + new("Soup", "Súp cua thập cẩm", "soup", 10, "cooking", 60), + new("Món chính", "Cá kho tộ, Gà nướng mật ong", "beef", 25, "queued", 0), + new("Phụ", "Cơm chiên dương châu, Rau muống xào", "carrot", 15, "queued", 0), + new("Tráng miệng", "Chè thái, Bánh flan", "ice-cream-cone", 10, "queued", 0), + }; + + private void FireCourse(CourseInfo course) + { + course.Status = "cooking"; + course.Progress = 20; + } + + private void ServeCourse(CourseInfo course) + { + course.Status = "served"; + course.Progress = 100; + } + + private static string CourseStatusColor(string s) => s switch + { + "served" => "var(--pos-success)", "cooking" => "var(--pos-warning)", + "queued" => "var(--pos-text-tertiary)", _ => "var(--pos-text-tertiary)" + }; + + private static string CourseStatusBg(string s) => s switch + { + "served" => "rgba(34,197,94,.15)", "cooking" => "rgba(245,158,11,.15)", + "queued" => "var(--pos-bg-interactive)", _ => "var(--pos-bg-interactive)" + }; + + private static string CourseStatusLabel(string s) => s switch + { + "served" => "Đã phục vụ", "cooking" => "Đang nấu", "queued" => "Chờ", _ => s + }; + + private class CourseInfo(string name, string items, string icon, int estTime, string status, int progress) + { + public string Name { get; set; } = name; + public string Items { get; set; } = items; + public string Icon { get; set; } = icon; + public int EstTime { get; set; } = estTime; + public string Status { get; set; } = status; + public int Progress { get; set; } = progress; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderNote.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderNote.razor new file mode 100644 index 00000000..b98a5998 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderNote.razor @@ -0,0 +1,151 @@ +@* + EN: Order Note — Special notes interface with quick chips, custom textarea, kitchen priority. + VI: Ghi chú đơn hàng — Giao diện ghi chú đặc biệt với chip nhanh, textarea tùy chỉnh, ưu tiên bếp. +*@ +@page "/pos/restaurant/order-note" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Ghi chú đặc biệt + +
+ +
+
+ @* ═══ CURRENT ITEM / MÓN HIỆN TẠI ═══ *@ +
+
+ +
+
+
@_currentItem
+
@FormatPrice(75_000)
+
+
+ + @* ═══ QUICK NOTE CHIPS / CHIP GHI CHÚ NHANH ═══ *@ +
+
Ghi chú nhanh
+
+ @foreach (var chip in _quickChips) + { + var isSelected = _selectedChips.Contains(chip); + + } +
+
+ + @* ═══ CUSTOM NOTE / GHI CHÚ TÙY CHỈNH ═══ *@ +
+
Ghi chú tùy chỉnh
+ +
+ + @* ═══ ALLERGEN TAGS / THẺ DỊ ỨNG ═══ *@ +
+
+ Cảnh báo dị ứng +
+
+ @foreach (var tag in _allergenTags) + { + + @tag + + } +
+
+ + @* ═══ KITCHEN PRIORITY / ƯU TIÊN BẾP ═══ *@ +
+
Ưu tiên bếp
+
+ @foreach (var priority in _priorities) + { + + } +
+
+ + @* ═══ APPLY ALL CHECKBOX / ÁP DỤNG CHO TẤT CẢ ═══ *@ +
+
+ @if (_applyToAll) + { + + } +
+ Áp dụng cho tất cả các món +
+
+
+ + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ + +
+
+ +@code { + private string _currentItem = "Phở bò tái"; + private string _customNote = string.Empty; + private string _selectedPriority = "normal"; + private bool _applyToAll = false; + private readonly HashSet _selectedChips = new(); + + private readonly string[] _quickChips = { "Ít hành", "Không rau", "Thêm nước mắm", "Cay nhiều", "Không MSG", "Chín kỹ" }; + private readonly string[] _allergenTags = { "Không" }; + + // EN: Priority options / VI: Các mức ưu tiên + private readonly List _priorities = new() + { + new("normal", "Thường", "clock", "var(--pos-text-tertiary)", "var(--pos-bg-interactive)"), + new("urgent", "Gấp", "zap", "var(--pos-warning)", "rgba(245,158,11,.1)"), + new("vip", "VIP", "crown", "var(--pos-orange-primary)", "rgba(255,92,0,.1)"), + }; + + private void ToggleChip(string chip) + { + if (!_selectedChips.Remove(chip)) + _selectedChips.Add(chip); + } + + private void SaveNote() => NavigateTo("restaurant"); + + private record PriorityOption(string Key, string Label, string Icon, string Color, string Bg); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantJourney.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantJourney.razor new file mode 100644 index 00000000..8a7ae771 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantJourney.razor @@ -0,0 +1,332 @@ +@* + EN: Restaurant Journey — End-to-end restaurant workflow: Đón khách → Chọn bàn → Đặt món → Bếp → Tiếp tục → Thanh toán → Hoàn tất. + VI: Hành trình nhà hàng — Quy trình từ đầu đến cuối: Đón khách → Chọn bàn → Đặt món → Bếp → Tiếp tục → Thanh toán → Hoàn tất. +*@ +@page "/pos/restaurant/restaurant-journey" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + Hành trình nhà hàng + + Khách: Nguyễn Văn A · 4 người · Bàn 7 +
+ + @* ═══ STEP TRACKER / THANH BƯỚC ═══ *@ +
+
+ @for (int i = 0; i < _steps.Count; i++) + { + var step = _steps[i]; + var stepIdx = i; + var isActive = stepIdx == _currentStep; + var isCompleted = stepIdx < _currentStep; + var bgColor = isCompleted ? "var(--pos-success)" : isActive ? "var(--pos-orange-primary)" : "var(--pos-bg-interactive)"; + var fgColor = isCompleted || isActive ? "#FFF" : "var(--pos-text-tertiary)"; + + @* EN: Step circle / VI: Vòng tròn bước *@ +
+
+ @if (isCompleted) + { + + } + else + { + + } +
+
+ @step.Label +
+
+ + @* EN: Connector line / VI: Đường nối *@ + @if (stepIdx < _steps.Count - 1) + { +
+ } + } +
+
+ + @* ═══ GUEST INFO CARD / THẺ KHÁCH ═══ *@ +
+
+
+ A +
+
+
Nguyễn Văn A
+
4 người · Bàn 7 · Khu VIP
+
+
+
Mã đơn
+
#NH-0047
+
+
+
+ + @* ═══ STEP CONTENT / NỘI DUNG BƯỚC ═══ *@ +
+ @switch (_currentStep) + { + case 0: + @* ═══ ĐÓN KHÁCH / WELCOME STEP ═══ *@ +
+
+ Đón khách +
+
+
+ Tên khách + Nguyễn Văn A +
+
+ Số người + 4 người +
+
+ Giờ đến + 18:30 +
+
+ Ghi chú + Sinh nhật +
+
+
+ break; + + case 1: + @* ═══ CHỌN BÀN / TABLE SELECT STEP ═══ *@ +
+
+ Chọn bàn +
+
+ @foreach (var table in _availableTables) + { + var isSelected = table.Name == "Bàn 7"; +
+
@table.Name
+
@table.Seats chỗ · @table.Section
+
+ } +
+
+ break; + + case 2: + @* ═══ ĐẶT MÓN / ORDER STEP ═══ *@ +
+
+ Đặt món +
+ @foreach (var item in _orderItems) + { +
+
+
@item.Name
+
x@item.Qty
+
+ @FormatPrice(item.Price * item.Qty) +
+ } +
+ Tổng + @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty)) +
+
+ break; + + case 3: + @* ═══ BẾP / KITCHEN STEP ═══ *@ +
+
+ Bếp đang chuẩn bị +
+ @foreach (var item in _orderItems) + { +
+
+
@item.Name
+
+ + @(item.Status == "done" ? "Xong" : item.Status == "cooking" ? "Đang nấu" : "Chờ") + +
+ } +
+ break; + + case 4: + @* ═══ TIẾP TỤC / CONTINUE STEP ═══ *@ +
+
+ Phục vụ bàn +
+
+
+ Trạng thái + Đã phục vụ xong +
+
+ Gọi thêm? + Có thể gọi thêm món +
+
+ Thời gian ngồi + 45 phút +
+
+
+ break; + + case 5: + @* ═══ THANH TOÁN / PAYMENT STEP ═══ *@ +
+
+ Thanh toán +
+
+ @foreach (var item in _orderItems) + { +
+ @item.Qty x @item.Name + @FormatPrice(item.Price * item.Qty) +
+ } +
+
+ Tạm tính + @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty)) +
+
+ VAT (8%) + @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 0.08m) +
+
+ Tổng thanh toán + @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 1.08m) +
+
+
+
+ break; + + case 6: + @* ═══ HOÀN TẤT / COMPLETE STEP ═══ *@ +
+
+ +
+
Hoàn tất!
+
+ Cảm ơn quý khách Nguyễn Văn A +
+
+ Bàn 7 · 4 người · Tổng: @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 1.08m) +
+
+ + +
+
+ break; + } +
+ + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ @if (_currentStep > 0) + { + + } + + @if (_currentStep < _steps.Count - 1) + { + + } + else + { + + } +
+
+ +@code { + private int _currentStep = 0; + + // EN: Journey steps / VI: Các bước hành trình + private readonly List _steps = new() + { + new("Đón khách", "hand"), + new("Chọn bàn", "layout-grid"), + new("Đặt món", "utensils"), + new("Bếp", "chef-hat"), + new("Tiếp tục", "utensils-crossed"), + new("Thanh toán", "credit-card"), + new("Hoàn tất", "check-circle"), + }; + + // EN: Available tables / VI: Bàn trống + private readonly List _availableTables = new() + { + new("Bàn 5", 4, "Trong nhà"), new("Bàn 7", 6, "VIP"), new("Bàn 8", 2, "Ngoài trời"), + new("Bàn 11", 4, "Trong nhà"), new("Bàn 12", 8, "VIP"), new("Bàn 6", 4, "Ngoài trời"), + }; + + // EN: Demo order items / VI: Các món trong đơn mẫu + private readonly List _orderItems = new() + { + new("Phở bò tái", 75_000, 2, "done"), + new("Gỏi cuốn", 45_000, 1, "cooking"), + new("Lẩu thái", 250_000, 1, "pending"), + new("Trà đá", 10_000, 4, "done"), + }; + + private record StepInfo(string Label, string Icon); + private record TableInfo(string Name, int Seats, string Section); + private class OrderItem(string name, decimal price, int qty, string status) + { + public string Name { get; set; } = name; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = qty; + public string Status { get; set; } = status; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor new file mode 100644 index 00000000..369ef120 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor @@ -0,0 +1,150 @@ +@* + EN: Restaurant Menu Management — Category tabs, item grid, edit mode, quick actions, stats. + VI: Quản lý thực đơn nhà hàng — Tab danh mục, lưới món, chế độ chỉnh sửa, thao tác nhanh, thống kê. +*@ +@page "/pos/restaurant/menu-management" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Quản lý thực đơn + + + +
+ + @* ═══ CATEGORY TABS / TAB DANH MỤC ═══ *@ +
+ @foreach (var cat in _categories) + { + + } +
+ + @* ═══ QUICK ACTIONS / THAO TÁC NHANH ═══ *@ + @if (_editMode) + { +
+ + + +
+ } + + @* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@ +
+
+ @foreach (var item in FilteredMenu) + { +
+ @* EN: Photo placeholder / VI: Ảnh giữ chỗ *@ +
+ + @if (!item.Available) + { +
86
+ } +
+
+
@item.Name
+
+ @FormatPrice(item.Price) + @if (_editMode) + { + + } +
+ @if (!string.IsNullOrEmpty(item.Note)) + { +
@item.Note
+ } +
+
+ } +
+
+ + @* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@ +
+ Tổng: @_menuItems.Count món + Có sẵn: @_menuItems.Count(i => i.Available) + Hết (86): @_menuItems.Count(i => !i.Available) + @_activeCategory +
+
+ +@code { + private string _activeCategory = "Khai vị"; + private bool _editMode = false; + private string? _selectedItem; + private readonly string[] _categories = { "Khai vị", "Món chính", "Lẩu", "Nước", "Tráng miệng" }; + + // EN: Menu items / VI: Các món trong thực đơn + private readonly List _menuItems = new() + { + new("Gỏi cuốn tôm", 45_000, "Khai vị", "salad", true, ""), + new("Chả giò chiên", 40_000, "Khai vị", "flame", true, ""), + new("Súp cua", 55_000, "Khai vị", "soup", true, "Đặc biệt"), + new("Phở bò tái", 75_000, "Món chính", "beef", true, ""), + new("Cơm tấm sườn", 65_000, "Món chính", "utensils", true, ""), + new("Cá kho tộ", 120_000, "Món chính", "fish", false, "Hết nguyên liệu"), + new("Gà nướng mật ong", 180_000, "Món chính", "drumstick", true, ""), + new("Lẩu thái", 250_000, "Lẩu", "soup", true, "Best seller"), + new("Lẩu nấm", 200_000, "Lẩu", "leaf", true, ""), + new("Trà đá", 10_000, "Nước", "glass-water", true, ""), + new("Cà phê sữa", 29_000, "Nước", "coffee", true, ""), + new("Chè thái", 30_000, "Tráng miệng", "ice-cream-cone", true, ""), + }; + + private IEnumerable FilteredMenu => + _menuItems.Where(m => m.Category == _activeCategory); + + private void MarkSoldOut() + { + if (_selectedItem is not null) + { + var item = _menuItems.FirstOrDefault(m => m.Name == _selectedItem); + if (item is not null) item.Available = false; + } + } + + private class RestMenuItem(string name, decimal price, string category, string icon, bool available, string note) + { + public string Name { get; set; } = name; + public decimal Price { get; set; } = price; + public string Category { get; set; } = category; + public string Icon { get; set; } = icon; + public bool Available { get; set; } = available; + public string Note { get; set; } = note; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor new file mode 100644 index 00000000..86c8fbf4 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor @@ -0,0 +1,209 @@ +@* + EN: Table Merge/Split — Merge multiple tables or split a table with active order. + VI: Ghép/Tách bàn — Ghép nhiều bàn lại hoặc tách bàn có đơn đang hoạt động. +*@ +@page "/pos/restaurant/table-merge-split" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Ghép / Tách bàn + +
+ + @* ═══ MODE TABS / TAB CHẾ ĐỘ ═══ *@ +
+ + +
+ +
+ @if (_mode == "merge") + { + @* ═══ MERGE MODE / CHẾ ĐỘ GHÉP ═══ *@ +
+
+ Chọn bàn để ghép (2+ bàn) +
+
+ @foreach (var table in _mergeTables) + { + var isSelected = _mergeSelected.Contains(table.Id); +
+ @if (isSelected) + { +
+ +
+ } +
@table.Name
+
@table.Seats chỗ
+
+ @(table.Status == "available" ? "Trống" : "Đang dùng") +
+
+ } +
+
+ + @* EN: Merge preview / VI: Xem trước ghép *@ + @if (_mergeSelected.Count >= 2) + { +
+
+ Xem trước +
+
+ @foreach (var id in _mergeSelected) + { + var t = _mergeTables.First(x => x.Id == id); + @t.Name + @if (id != _mergeSelected.Last()) + { + + + } + } + + + @MergedName (@MergedCapacity chỗ) + +
+
+ } + } + else + { + @* ═══ SPLIT MODE / CHẾ ĐỘ TÁCH ═══ *@ +
+
+ Chọn bàn để tách +
+
+ @foreach (var table in _splitTables) + { + var isSelected = _splitSelected == table.Id; +
+
@table.Name
+
@table.Seats chỗ
+
+ @table.OrderCount món +
+
+ } +
+
+ + @* EN: Split preview / VI: Xem trước tách *@ + @if (_splitSelected is not null) + { + var selected = _splitTables.First(t => t.Id == _splitSelected); +
+
+ Xem trước tách +
+
+ @selected.Name (@selected.Seats chỗ) + + + @(selected.Name)A (@(selected.Seats / 2) chỗ) + + + + + @(selected.Name)B (@(selected.Seats - selected.Seats / 2) chỗ) + +
+
+ @selected.OrderCount món sẽ được chia giữa 2 bàn +
+
+ } + } +
+ + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ + +
+
+ +@code { + private string _mode = "merge"; + private readonly HashSet _mergeSelected = new(); + private string? _splitSelected; + + // EN: Tables for merge / VI: Bàn để ghép + private readonly List _mergeTables = new() + { + new("T03", "Bàn 3", 4, "available"), + new("T04", "Bàn 4", 6, "available"), + new("T05", "Bàn 5", 4, "occupied"), + new("T06", "Bàn 6", 4, "available"), + new("T08", "Bàn 8", 2, "available"), + new("T11", "Bàn 11", 4, "available"), + }; + + // EN: Tables for split / VI: Bàn để tách + private readonly List _splitTables = new() + { + new("T07", "Bàn 7", 6, 5), + new("T03", "Bàn 3", 4, 3), + new("T10", "Bàn 10", 8, 7), + }; + + private void SwitchMode(string mode) + { + _mode = mode; + _mergeSelected.Clear(); + _splitSelected = null; + } + + private void ToggleMerge(string id) + { + if (!_mergeSelected.Remove(id)) + _mergeSelected.Add(id); + } + + private bool CanConfirm => _mode == "merge" ? _mergeSelected.Count >= 2 : _splitSelected is not null; + + private string MergedName => string.Join("-", _mergeSelected.Select(id => + _mergeTables.First(t => t.Id == id).Name.Replace("Bàn ", ""))); + + private int MergedCapacity => _mergeSelected.Sum(id => + _mergeTables.First(t => t.Id == id).Seats); + + private void ConfirmAction() => NavigateTo("restaurant"); + + private record MergeTable(string Id, string Name, int Seats, string Status); + private record SplitTable(string Id, string Name, int Seats, int OrderCount); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor new file mode 100644 index 00000000..0ee69893 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor @@ -0,0 +1,155 @@ +@* + EN: Table Select — Quick table selection for seating guests with capacity matching. + VI: Chọn bàn nhanh — Chọn bàn nhanh cho khách với kiểm tra sức chứa. +*@ +@page "/pos/restaurant/table-select" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ + + Chọn bàn + +
+ + @* ═══ GUEST COUNT + SECTION FILTER / SỐ KHÁCH + LỌC KHU VỰC ═══ *@ +
+ @* EN: Guest count input / VI: Nhập số khách *@ +
+ + Số khách: +
+ + @_guestCount + +
+
+ + @* EN: Section filter / VI: Lọc khu vực *@ +
+ @foreach (var sec in _sections) + { + + } +
+
+ + @* ═══ TABLE GRID / LƯỚI BÀN ═══ *@ +
+
+ @foreach (var table in FilteredTables) + { + var matchColor = CapacityColor(table.Seats, _guestCount); + var isSelected = _selectedTable == table.Id; + +
+ @* EN: Capacity indicator / VI: Chỉ báo sức chứa *@ +
+
@table.Name
+
+ @table.Seats chỗ +
+
@table.Section
+ + @if (isSelected) + { +
+ +
+ } +
+ } +
+ + @if (!FilteredTables.Any()) + { +
+ +
Không tìm thấy bàn trống phù hợp
+
+ } +
+ + @* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@ + @if (_selectedTable is not null) + { + var sel = _availableTables.First(t => t.Id == _selectedTable); +
+
+ @sel.Name + @sel.Seats chỗ · @sel.Section +
+ +
+ + @CapacityLabel(sel.Seats, _guestCount) + +
+ } + + @* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@ +
+ + +
+
+ +@code { + private int _guestCount = 4; + private string _activeSection = "Tất cả"; + private string? _selectedTable; + private readonly string[] _sections = { "Tất cả", "Tầng 1", "Tầng 2", "Sân vườn", "VIP" }; + + // EN: Available tables / VI: Bàn trống + private readonly List _availableTables = new() + { + new("T01", "Bàn 1", 4, "Tầng 1"), + new("T05", "Bàn 5", 8, "VIP"), + new("T06", "Bàn 6", 4, "Sân vườn"), + new("T08", "Bàn 8", 2, "Sân vườn"), + new("T11", "Bàn 11", 4, "Tầng 1"), + new("T12", "Bàn 12", 2, "Tầng 2"), + new("T13", "Bàn 13", 6, "VIP"), + new("T14", "Bàn 14", 10, "Tầng 2"), + new("T15", "Bàn 15", 4, "Sân vườn"), + }; + + private IEnumerable FilteredTables => + _activeSection == "Tất cả" ? _availableTables : _availableTables.Where(t => t.Section == _activeSection); + + private static string CapacityColor(int seats, int guests) => + seats >= guests + 2 ? "var(--pos-success)" + : seats >= guests ? "#F59E0B" + : "var(--pos-text-tertiary)"; + + private static string CapacityLabel(int seats, int guests) => + seats >= guests + 2 ? "Rộng rãi" + : seats >= guests ? "Vừa" + : "Quá nhỏ"; + + private void OpenTable() => NavigateTo("restaurant"); + + private record AvailableTable(string Id, string Name, int Seats, string Section); +}