Add 9 missing Blazor Razor workflow files for Cafe and Restaurant POS verticals
Cafe (2 files): - CafeJourney.razor: 5-step café workflow tracker - MilkFoamOptions.razor: Milk foam/drink customization sub-options Restaurant (7 files): - RestaurantJourney.razor: 7-step restaurant workflow tracker - AllergenWarning.razor: Allergen alert display with severity levels - CourseTiming.razor: Multi-course meal timing management - RestaurantMenuManagement.razor: Restaurant menu editor with quick actions - OrderNote.razor: Order/item special notes with quick chips - TableMergeSplit.razor: Table merge and split operations - TableSelect.razor: Quick table selection with capacity matching All files follow existing POS patterns: @layout PosLayout, @inherits PosBase, bilingual EN/VI comments, section markers, CSS vars, FormatPrice, NavigateTo, Lucide icons, hardcoded Vietnamese demo data with VND prices. Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
|
||||
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
|
||||
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("cafe"))">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Hành trình Café</span>
|
||||
<span style="flex:1;"></span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">Đơn #CF-0027</span>
|
||||
</div>
|
||||
|
||||
@* ═══ STEP TRACKER / THANH BƯỚC ═══ *@
|
||||
<div style="padding:20px 16px;flex-shrink:0;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:0;">
|
||||
@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 *@
|
||||
<div style="display:flex;flex-direction:column;align-items:center;z-index:1;">
|
||||
<div style="width:40px;height:40px;border-radius:50%;background:@bgColor;
|
||||
display:flex;align-items:center;justify-content:center;color:@fgColor;
|
||||
font-size:14px;font-weight:700;cursor:pointer;"
|
||||
@onclick="() => _currentStep = stepIdx">
|
||||
@if (isCompleted)
|
||||
{
|
||||
<i data-lucide="check" style="width:18px;height:18px;"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i data-lucide="@step.Icon" style="width:18px;height:18px;"></i>
|
||||
}
|
||||
</div>
|
||||
<div style="font-size:11px;margin-top:6px;font-weight:@(isActive ? "700" : "500");
|
||||
color:@(isActive ? "var(--pos-orange-primary)" : isCompleted ? "var(--pos-success)" : "var(--pos-text-tertiary)");">
|
||||
@step.Label
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Connector line / VI: Đường nối *@
|
||||
@if (stepIdx < _steps.Count - 1)
|
||||
{
|
||||
<div style="flex:1;height:2px;max-width:80px;margin:0 8px;margin-bottom:20px;
|
||||
background:@(stepIdx < _currentStep ? "var(--pos-success)" : "var(--pos-border-default)");"></div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ STEP CONTENT / NỘI DUNG BƯỚC ═══ *@
|
||||
<div style="flex:1;overflow-y:auto;padding:0 16px 16px;">
|
||||
@switch (_currentStep)
|
||||
{
|
||||
case 0:
|
||||
@* ═══ ĐẶT MÓN / ORDER STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="clipboard-list" style="width:18px;height:18px;display:inline;"></i> Đặt món
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var item in _orderItems)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px;
|
||||
background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;">@item.Name</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);">x@item.Qty</div>
|
||||
</div>
|
||||
<span style="font-size:14px;font-weight:600;">@FormatPrice(item.Price * item.Qty)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:12px;padding-top:12px;
|
||||
border-top:1px solid var(--pos-border-subtle);font-size:15px;font-weight:700;">
|
||||
<span>Tổng (@_orderItems.Sum(i => i.Qty) món)</span>
|
||||
<span style="color:var(--pos-orange-primary);">@FormatPrice(_orderItems.Sum(i => i.Price * i.Qty))</span>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 1:
|
||||
@* ═══ THANH TOÁN / PAYMENT STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="credit-card" style="width:18px;height:18px;display:inline;"></i> Thanh toán
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Phương thức</span>
|
||||
<span style="font-weight:600;">Tiền mặt</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Tổng tiền</span>
|
||||
<span style="font-weight:600;color:var(--pos-orange-primary);">@FormatPrice(125_000)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Khách đưa</span>
|
||||
<span style="font-weight:600;">@FormatPrice(150_000)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Tiền thừa</span>
|
||||
<span style="font-weight:600;color:var(--pos-success);">@FormatPrice(25_000)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 2:
|
||||
@* ═══ PHA CHẾ / BARISTA STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="coffee" style="width:18px;height:18px;display:inline;"></i> Pha chế
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Barista</span>
|
||||
<span style="font-weight:600;">Trần Minh Tú</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Thời gian ước tính</span>
|
||||
<span style="font-weight:600;color:var(--pos-warning);">3 phút</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Trạng thái</span>
|
||||
<span style="font-weight:600;color:var(--pos-orange-primary);">Đang pha chế</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:16px;padding:12px;background:rgba(245,158,11,.1);border-radius:8px;border:1px solid rgba(245,158,11,.3);">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:var(--pos-warning);animation:pulse 1.5s infinite;"></span>
|
||||
<span style="font-size:12px;color:var(--pos-warning);font-weight:600;">Đang pha 3 món...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 3:
|
||||
@* ═══ PHỤC VỤ / SERVING STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;text-align:center;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="bell" style="width:18px;height:18px;display:inline;"></i> Phục vụ
|
||||
</div>
|
||||
<div style="width:100px;height:100px;border-radius:50%;background:rgba(255,92,0,.15);
|
||||
display:flex;align-items:center;justify-content:center;margin:20px auto;">
|
||||
<span style="font-size:36px;font-weight:800;color:var(--pos-orange-primary);">#027</span>
|
||||
</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-secondary);margin-bottom:8px;">Số thứ tự</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Vui lòng chờ gọi số tại quầy</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 4:
|
||||
@* ═══ HOÀN TẤT / COMPLETE STEP ═══ *@
|
||||
<div style="text-align:center;padding:40px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:50%;background:rgba(34,197,94,.15);
|
||||
display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<i data-lucide="check-circle" style="width:40px;height:40px;color:#22C55E;"></i>
|
||||
</div>
|
||||
<div style="font-size:20px;font-weight:700;color:var(--pos-success);margin-bottom:8px;">Hoàn tất!</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-secondary);margin-bottom:4px;">
|
||||
Đơn hàng #CF-0027 đã hoàn thành
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--pos-text-tertiary);margin-bottom:20px;">
|
||||
3 món · Tổng: @FormatPrice(125_000) · Tiền mặt
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;">
|
||||
<button style="padding:12px 24px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;">
|
||||
<i data-lucide="printer" style="width:14px;height:14px;display:inline;"></i> In hóa đơn
|
||||
</button>
|
||||
<button style="padding:12px 24px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;">
|
||||
<i data-lucide="star" style="width:14px;height:14px;display:inline;"></i> Đánh giá
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
@if (_currentStep > 0)
|
||||
{
|
||||
<button style="padding:12px 20px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="() => _currentStep--">
|
||||
<i data-lucide="arrow-left" style="width:14px;height:14px;display:inline;"></i> Quay lại
|
||||
</button>
|
||||
}
|
||||
<span style="flex:1;"></span>
|
||||
@if (_currentStep < _steps.Count - 1)
|
||||
{
|
||||
<button class="pos-btn-checkout" style="width:auto;padding:12px 24px;" @onclick="() => _currentStep++">
|
||||
Tiếp <i data-lucide="arrow-right" style="width:14px;height:14px;display:inline;"></i>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="pos-btn-checkout" style="width:auto;padding:12px 24px;" @onclick="@(() => NavigateTo("cafe"))">
|
||||
<i data-lucide="home" style="width:14px;height:14px;display:inline;"></i> Về trang chính
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Pulse animation / VI: Hiệu ứng nhấp nháy *@
|
||||
<style>
|
||||
@@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private int _currentStep = 0;
|
||||
|
||||
// EN: Journey steps / VI: Các bước hành trình
|
||||
private readonly List<StepInfo> _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<OrderItem> _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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="display:flex;height:100%;">
|
||||
@* ═══ CUSTOMIZATION PANEL / PANEL TÙY CHỈNH ═══ *@
|
||||
<div style="flex:1;padding:24px;overflow-y:auto;">
|
||||
<div style="max-width:560px;margin:0 auto;">
|
||||
@* EN: Product header / VI: Tiêu đề sản phẩm *@
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:28px;">
|
||||
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
|
||||
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("cafe"))">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
|
||||
</button>
|
||||
<div style="width:72px;height:72px;border-radius:var(--pos-radius);background:var(--pos-bg-elevated);display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="coffee" style="width:32px;height:32px;color:var(--pos-orange-primary);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:20px;font-weight:700;">@_productName</div>
|
||||
<div style="font-size:15px;color:var(--pos-orange-primary);font-weight:600;">@FormatPrice(_basePrice + _extraPrice)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ MILK TYPE / LOẠI SỮA ═══ *@
|
||||
<div style="margin-bottom:24px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="milk" style="width:14px;height:14px;display:inline;"></i> Loại sữa
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
||||
@foreach (var milk in _milkTypes)
|
||||
{
|
||||
<button style="padding:12px 16px;border-radius:var(--pos-radius);border:2px solid @(_selectedMilk == milk.Name ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(_selectedMilk == milk.Name ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:500;"
|
||||
@onclick="() => SelectMilk(milk)">
|
||||
@milk.Name
|
||||
@if (milk.Extra > 0)
|
||||
{
|
||||
<span style="font-size:11px;color:var(--pos-orange-primary);margin-left:4px;">+@FormatPrice(milk.Extra)</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ FOAM LEVEL / MỨC FOAM ═══ *@
|
||||
<div style="margin-bottom:24px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="cloud" style="width:14px;height:14px;display:inline;"></i> Mức foam
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
@foreach (var foam in _foamLevels)
|
||||
{
|
||||
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);border:2px solid @(_selectedFoam == foam ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(_selectedFoam == foam ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:500;"
|
||||
@onclick="() => _selectedFoam = foam">
|
||||
@foam
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TEMPERATURE / NHIỆT ĐỘ ═══ *@
|
||||
<div style="margin-bottom:24px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="thermometer" style="width:14px;height:14px;display:inline;"></i> Nhiệt độ
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
@foreach (var temp in _temperatures)
|
||||
{
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:2px solid @(_selectedTemp == temp.Label ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(_selectedTemp == temp.Label ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");color:var(--pos-text-primary);cursor:pointer;text-align:center;"
|
||||
@onclick="() => _selectedTemp = temp.Label">
|
||||
<div style="font-size:20px;margin-bottom:4px;">@temp.Emoji</div>
|
||||
<div style="font-size:14px;font-weight:600;">@temp.Label</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ EXTRAS / THÊM ═══ *@
|
||||
<div style="margin-bottom:24px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="plus-circle" style="width:14px;height:14px;display:inline;"></i> Thêm
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(2, 1fr);gap:8px;">
|
||||
@foreach (var extra in _extras)
|
||||
{
|
||||
var isSelected = _selectedExtras.Contains(extra.Name);
|
||||
<button style="padding:12px 16px;border-radius:var(--pos-radius);border:2px solid @(isSelected ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(isSelected ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");color:var(--pos-text-primary);cursor:pointer;display:flex;justify-content:space-between;align-items:center;"
|
||||
@onclick="() => ToggleExtra(extra)">
|
||||
<span style="font-size:14px;">@extra.Name</span>
|
||||
<span style="font-size:12px;color:var(--pos-orange-primary);">+@FormatPrice(extra.Price)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ SUMMARY PANEL / PANEL TÓM TẮT ═══ *@
|
||||
<div style="width:300px;background:var(--pos-bg-elevated);border-left:1px solid var(--pos-border-subtle);padding:24px;display:flex;flex-direction:column;">
|
||||
<div style="font-size:15px;font-weight:600;margin-bottom:16px;">Tóm tắt</div>
|
||||
<div style="flex:1;font-size:13px;color:var(--pos-text-secondary);display:flex;flex-direction:column;gap:8px;">
|
||||
<div>Sữa: <strong style="color:var(--pos-text-primary);">@_selectedMilk</strong></div>
|
||||
<div>Foam: <strong style="color:var(--pos-text-primary);">@_selectedFoam</strong></div>
|
||||
<div>Nhiệt độ: <strong style="color:var(--pos-text-primary);">@_selectedTemp</strong></div>
|
||||
<div>Thêm: <strong style="color:var(--pos-text-primary);">@(_selectedExtras.Any() ? string.Join(", ", _selectedExtras) : "Không")</strong></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--pos-border-subtle);padding-top:16px;margin-top:16px;">
|
||||
<div class="pos-cart-total" style="margin-bottom:16px;">
|
||||
<span class="pos-cart-total__label">Tổng</span>
|
||||
<span class="pos-cart-total__value" style="font-size:20px;">@FormatPrice(_basePrice + _extraPrice)</span>
|
||||
</div>
|
||||
<button class="pos-btn-checkout" @onclick="Confirm">Xác nhận</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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<string> _selectedExtras = new();
|
||||
|
||||
// EN: Milk type options / VI: Các loại sữa
|
||||
private readonly List<MilkOption> _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<TempOption> _temperatures = new()
|
||||
{
|
||||
new("Nóng", "🔥"),
|
||||
new("Lạnh", "🧊"),
|
||||
new("Ấm", "☕"),
|
||||
};
|
||||
|
||||
// EN: Extra options / VI: Tùy chọn thêm
|
||||
private readonly List<ExtraOption> _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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="shield-alert" style="width:18px;height:18px;display:inline;"></i> Cảnh báo dị ứng
|
||||
</span>
|
||||
<span style="flex:1;"></span>
|
||||
<span style="font-size:12px;padding:4px 10px;border-radius:6px;font-weight:600;
|
||||
background:rgba(239,68,68,.15);color:var(--pos-danger);">
|
||||
2 chất gây dị ứng
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@* ═══ CURRENT ITEM / MÓN HIỆN TẠI ═══ *@
|
||||
<div style="padding:12px 16px;">
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;display:flex;align-items:center;gap:14px;">
|
||||
<div style="width:56px;height:56px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="utensils" style="width:24px;height:24px;color:var(--pos-orange-primary);"></i>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:16px;font-weight:700;">@_currentItem</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">@FormatPrice(65_000)</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
@foreach (var tag in _itemAllergens)
|
||||
{
|
||||
<span style="font-size:11px;padding:4px 8px;border-radius:6px;font-weight:600;
|
||||
background:rgba(239,68,68,.15);color:var(--pos-danger);">@tag</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding:0 16px 16px;">
|
||||
@* ═══ ALLERGEN GRID / LƯỚI CHẤT GÂY DỊ ỨNG ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">Chất gây dị ứng phổ biến</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:10px;">
|
||||
@foreach (var allergen in _allergens)
|
||||
{
|
||||
var isActive = _activeAllergens.Contains(allergen.Name);
|
||||
<div style="padding:16px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
|
||||
background:@(isActive ? SeverityBg(allergen.Severity) : "var(--pos-bg-elevated)");
|
||||
border:2px solid @(isActive ? SeverityColor(allergen.Severity) : "transparent");
|
||||
transition:all .2s ease;"
|
||||
@onclick="() => ToggleAllergen(allergen.Name)">
|
||||
<div style="font-size:28px;margin-bottom:8px;">@allergen.Icon</div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--pos-text-primary);">@allergen.Name</div>
|
||||
@if (isActive)
|
||||
{
|
||||
<div style="font-size:10px;font-weight:600;margin-top:4px;color:@SeverityColor(allergen.Severity);">
|
||||
@SeverityLabel(allergen.Severity)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ SEVERITY LEGEND / CHÚ THÍCH MỨC ĐỘ ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">Mức độ nghiêm trọng</div>
|
||||
<div style="display:flex;gap:12px;">
|
||||
<div style="flex:1;padding:12px;border-radius:var(--pos-radius);background:rgba(239,68,68,.1);border-left:4px solid var(--pos-danger);text-align:center;">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--pos-danger);">Cao</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:2px;">Nguy hiểm</div>
|
||||
</div>
|
||||
<div style="flex:1;padding:12px;border-radius:var(--pos-radius);background:rgba(255,92,0,.1);border-left:4px solid var(--pos-orange-primary);text-align:center;">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--pos-orange-primary);">Trung bình</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:2px;">Cẩn thận</div>
|
||||
</div>
|
||||
<div style="flex:1;padding:12px;border-radius:var(--pos-radius);background:rgba(245,158,11,.1);border-left:4px solid var(--pos-warning);text-align:center;">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--pos-warning);">Thấp</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:2px;">Lưu ý</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CUSTOMER PROFILE / HỒ SƠ KHÁCH ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="user" style="width:14px;height:14px;display:inline;"></i> Hồ sơ dị ứng khách hàng
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<div style="width:40px;height:40px;border-radius:50%;background:var(--pos-orange-primary);display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#fff;">
|
||||
B
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:13px;font-weight:600;">Trần Thị B</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);">Dị ứng: Hải sản, Đậu phộng</div>
|
||||
</div>
|
||||
<span style="font-size:11px;padding:4px 8px;border-radius:6px;font-weight:600;
|
||||
background:rgba(239,68,68,.15);color:var(--pos-danger);">Cao</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("restaurant"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button class="pos-btn-checkout" style="flex:1;" @onclick="ConfirmAllergens">
|
||||
<i data-lucide="check" style="width:14px;height:14px;display:inline;"></i> Xác nhận
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _currentItem = "Gỏi cuốn tôm";
|
||||
private readonly string[] _itemAllergens = { "Hải sản", "Đậu phộng" };
|
||||
private readonly HashSet<string> _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<AllergenInfo> _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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="timer" style="width:18px;height:18px;display:inline;"></i> Bàn 7 — Tiệc 5 món
|
||||
</span>
|
||||
<span style="flex:1;"></span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">
|
||||
<i data-lucide="clock" style="width:12px;height:12px;display:inline;"></i> Bắt đầu: 18:30
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@* ═══ AUTO-FIRE RULE / QUY TẮC TỰ ĐỘNG ═══ *@
|
||||
<div style="padding:10px 16px;border-bottom:1px solid var(--pos-border-subtle);">
|
||||
<div style="background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);border-radius:8px;padding:10px 14px;
|
||||
display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="zap" style="width:14px;height:14px;color:var(--pos-info);"></i>
|
||||
<span style="font-size:12px;color:var(--pos-info);font-weight:600;">Tự động: Phục vụ mỗi món cách 15 phút</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ COURSE LIST / DANH SÁCH COURSE ═══ *@
|
||||
<div style="flex:1;overflow-y:auto;padding:16px;">
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var course in _courses)
|
||||
{
|
||||
var statusColor = CourseStatusColor(course.Status);
|
||||
var statusLabel = CourseStatusLabel(course.Status);
|
||||
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;
|
||||
border-left:4px solid @statusColor;">
|
||||
@* EN: Course header / VI: Tiêu đề course *@
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||
<div style="width:36px;height:36px;border-radius:50%;background:@CourseStatusBg(course.Status);
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@course.Icon" style="width:16px;height:16px;color:@statusColor;"></i>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:700;">@course.Name</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">@course.Items</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<span style="font-size:12px;font-weight:600;padding:4px 10px;border-radius:6px;
|
||||
background:@CourseStatusBg(course.Status);color:@statusColor;">
|
||||
@statusLabel
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Timeline bar / VI: Thanh thời gian *@
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">
|
||||
<span>@course.EstTime phút</span>
|
||||
<span>@course.Progress%</span>
|
||||
</div>
|
||||
<div style="height:6px;border-radius:3px;background:var(--pos-bg-interactive);overflow:hidden;">
|
||||
<div style="height:100%;width:@(course.Progress)%;border-radius:3px;background:@statusColor;
|
||||
transition:width .3s ease;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Fire button / VI: Nút kích hoạt *@
|
||||
@if (course.Status == "queued")
|
||||
{
|
||||
<button style="width:100%;padding:8px;border-radius:8px;border:1px solid @statusColor;
|
||||
background:@CourseStatusBg(course.Status);color:@statusColor;font-size:13px;font-weight:600;cursor:pointer;"
|
||||
@onclick="() => FireCourse(course)">
|
||||
<i data-lucide="flame" style="width:14px;height:14px;display:inline;"></i> Fire — Bắt đầu nấu
|
||||
</button>
|
||||
}
|
||||
else if (course.Status == "cooking")
|
||||
{
|
||||
<button style="width:100%;padding:8px;border-radius:8px;border:none;
|
||||
background:rgba(34,197,94,.15);color:var(--pos-success);font-size:13px;font-weight:600;cursor:pointer;"
|
||||
@onclick="() => ServeCourse(course)">
|
||||
<i data-lucide="check" style="width:14px;height:14px;display:inline;"></i> Đã phục vụ
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER STATS / THỐNG KÊ ═══ *@
|
||||
<div style="padding:10px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:20px;font-size:12px;color:var(--pos-text-secondary);">
|
||||
<span style="color:var(--pos-success);">Đã phục vụ: @_courses.Count(c => c.Status == "served")</span>
|
||||
<span style="color:var(--pos-warning);">Đang nấu: @_courses.Count(c => c.Status == "cooking")</span>
|
||||
<span style="color:var(--pos-text-tertiary);">Chờ: @_courses.Count(c => c.Status == "queued")</span>
|
||||
<span style="margin-left:auto;font-weight:600;">Tổng: @_courses.Count course</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// EN: Course list / VI: Danh sách course
|
||||
private readonly List<CourseInfo> _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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="message-square" style="width:18px;height:18px;display:inline;"></i> Ghi chú đặc biệt
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding:16px;">
|
||||
<div style="max-width:600px;margin:0 auto;">
|
||||
@* ═══ CURRENT ITEM / MÓN HIỆN TẠI ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;margin-bottom:20px;
|
||||
display:flex;align-items:center;gap:14px;">
|
||||
<div style="width:56px;height:56px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="beef" style="width:24px;height:24px;color:var(--pos-orange-primary);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:16px;font-weight:700;">@_currentItem</div>
|
||||
<div style="font-size:13px;color:var(--pos-orange-primary);font-weight:600;">@FormatPrice(75_000)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ QUICK NOTE CHIPS / CHIP GHI CHÚ NHANH ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">Ghi chú nhanh</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
||||
@foreach (var chip in _quickChips)
|
||||
{
|
||||
var isSelected = _selectedChips.Contains(chip);
|
||||
<button style="padding:10px 16px;border-radius:20px;font-size:13px;font-weight:500;cursor:pointer;
|
||||
border:2px solid @(isSelected ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(isSelected ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");
|
||||
color:@(isSelected ? "var(--pos-orange-primary)" : "var(--pos-text-primary)");"
|
||||
@onclick="() => ToggleChip(chip)">
|
||||
@chip
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CUSTOM NOTE / GHI CHÚ TÙY CHỈNH ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">Ghi chú tùy chỉnh</div>
|
||||
<textarea @bind="_customNote" placeholder="Nhập ghi chú cho món ăn..."
|
||||
style="width:100%;min-height:100px;padding:12px;border-radius:var(--pos-radius);
|
||||
border:1px solid var(--pos-border-default);background:var(--pos-bg-interactive);
|
||||
color:var(--pos-text-primary);font-size:13px;font-family:var(--pos-font);resize:vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
@* ═══ ALLERGEN TAGS / THẺ DỊ ỨNG ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
<i data-lucide="shield-alert" style="width:14px;height:14px;display:inline;"></i> Cảnh báo dị ứng
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
@foreach (var tag in _allergenTags)
|
||||
{
|
||||
<span style="font-size:12px;padding:6px 12px;border-radius:6px;font-weight:600;
|
||||
background:rgba(239,68,68,.1);color:var(--pos-danger);border:1px solid rgba(239,68,68,.3);">
|
||||
@tag
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ KITCHEN PRIORITY / ƯU TIÊN BẾP ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">Ưu tiên bếp</div>
|
||||
<div style="display:flex;gap:10px;">
|
||||
@foreach (var priority in _priorities)
|
||||
{
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
|
||||
border:2px solid @(_selectedPriority == priority.Key ? priority.Color : "var(--pos-border-default)");
|
||||
background:@(_selectedPriority == priority.Key ? priority.Bg : "var(--pos-bg-elevated)");"
|
||||
@onclick="() => _selectedPriority = priority.Key">
|
||||
<i data-lucide="@priority.Icon" style="width:18px;height:18px;color:@priority.Color;display:block;margin:0 auto 6px;"></i>
|
||||
<div style="font-size:13px;font-weight:600;color:@priority.Color;">@priority.Label</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ APPLY ALL CHECKBOX / ÁP DỤNG CHO TẤT CẢ ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px;
|
||||
display:flex;align-items:center;gap:10px;cursor:pointer;"
|
||||
@onclick="() => _applyToAll = !_applyToAll">
|
||||
<div style="width:22px;height:22px;border-radius:6px;border:2px solid @(_applyToAll ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
|
||||
background:@(_applyToAll ? "var(--pos-orange-primary)" : "transparent");
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
@if (_applyToAll)
|
||||
{
|
||||
<i data-lucide="check" style="width:14px;height:14px;color:#fff;"></i>
|
||||
}
|
||||
</div>
|
||||
<span style="font-size:13px;font-weight:500;">Áp dụng cho tất cả các món</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("restaurant"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button class="pos-btn-checkout" style="flex:1;" @onclick="SaveNote">
|
||||
<i data-lucide="save" style="width:14px;height:14px;display:inline;"></i> Lưu ghi chú
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _currentItem = "Phở bò tái";
|
||||
private string _customNote = string.Empty;
|
||||
private string _selectedPriority = "normal";
|
||||
private bool _applyToAll = false;
|
||||
private readonly HashSet<string> _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<PriorityOption> _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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
|
||||
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
|
||||
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Hành trình nhà hàng</span>
|
||||
<span style="flex:1;"></span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">Khách: Nguyễn Văn A · 4 người · Bàn 7</span>
|
||||
</div>
|
||||
|
||||
@* ═══ STEP TRACKER / THANH BƯỚC ═══ *@
|
||||
<div style="padding:20px 16px;flex-shrink:0;overflow-x:auto;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:0;min-width:600px;">
|
||||
@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 *@
|
||||
<div style="display:flex;flex-direction:column;align-items:center;z-index:1;">
|
||||
<div style="width:36px;height:36px;border-radius:50%;background:@bgColor;
|
||||
display:flex;align-items:center;justify-content:center;color:@fgColor;
|
||||
font-size:13px;font-weight:700;cursor:pointer;"
|
||||
@onclick="() => _currentStep = stepIdx">
|
||||
@if (isCompleted)
|
||||
{
|
||||
<i data-lucide="check" style="width:16px;height:16px;"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i data-lucide="@step.Icon" style="width:16px;height:16px;"></i>
|
||||
}
|
||||
</div>
|
||||
<div style="font-size:10px;margin-top:5px;font-weight:@(isActive ? "700" : "500");white-space:nowrap;
|
||||
color:@(isActive ? "var(--pos-orange-primary)" : isCompleted ? "var(--pos-success)" : "var(--pos-text-tertiary)");">
|
||||
@step.Label
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Connector line / VI: Đường nối *@
|
||||
@if (stepIdx < _steps.Count - 1)
|
||||
{
|
||||
<div style="flex:1;height:2px;max-width:60px;margin:0 6px;margin-bottom:18px;
|
||||
background:@(stepIdx < _currentStep ? "var(--pos-success)" : "var(--pos-border-default)");"></div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ GUEST INFO CARD / THẺ KHÁCH ═══ *@
|
||||
<div style="padding:0 16px 12px;">
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px;
|
||||
display:flex;align-items:center;gap:14px;">
|
||||
<div style="width:44px;height:44px;border-radius:50%;background:var(--pos-orange-primary);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#fff;flex-shrink:0;">
|
||||
A
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:700;">Nguyễn Văn A</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">4 người · Bàn 7 · Khu VIP</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);">Mã đơn</div>
|
||||
<div style="font-size:13px;font-weight:600;font-family:monospace;">#NH-0047</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ STEP CONTENT / NỘI DUNG BƯỚC ═══ *@
|
||||
<div style="flex:1;overflow-y:auto;padding:0 16px 16px;">
|
||||
@switch (_currentStep)
|
||||
{
|
||||
case 0:
|
||||
@* ═══ ĐÓN KHÁCH / WELCOME STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="hand" style="width:18px;height:18px;display:inline;"></i> Đón khách
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Tên khách</span>
|
||||
<span style="font-weight:600;">Nguyễn Văn A</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Số người</span>
|
||||
<span style="font-weight:600;">4 người</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Giờ đến</span>
|
||||
<span style="font-weight:600;color:var(--pos-success);">18:30</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Ghi chú</span>
|
||||
<span style="font-weight:600;color:var(--pos-warning);">Sinh nhật</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 1:
|
||||
@* ═══ CHỌN BÀN / TABLE SELECT STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="layout-grid" style="width:18px;height:18px;display:inline;"></i> Chọn bàn
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;">
|
||||
@foreach (var table in _availableTables)
|
||||
{
|
||||
var isSelected = table.Name == "Bàn 7";
|
||||
<div style="padding:16px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
|
||||
background:@(isSelected ? "rgba(255,92,0,.15)" : "var(--pos-bg-interactive)");
|
||||
border:2px solid @(isSelected ? "var(--pos-orange-primary)" : "transparent");">
|
||||
<div style="font-size:16px;font-weight:700;">@table.Name</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:4px;">@table.Seats chỗ · @table.Section</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 2:
|
||||
@* ═══ ĐẶT MÓN / ORDER STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="utensils" style="width:18px;height:18px;display:inline;"></i> Đặt món
|
||||
</div>
|
||||
@foreach (var item in _orderItems)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px;
|
||||
background:var(--pos-bg-interactive);border-radius:8px;margin-bottom:8px;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;">@item.Name</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);">x@item.Qty</div>
|
||||
</div>
|
||||
<span style="font-size:14px;font-weight:600;">@FormatPrice(item.Price * item.Qty)</span>
|
||||
</div>
|
||||
}
|
||||
<div style="display:flex;justify-content:space-between;margin-top:8px;padding-top:10px;
|
||||
border-top:1px solid var(--pos-border-subtle);font-size:14px;font-weight:700;">
|
||||
<span>Tổng</span>
|
||||
<span style="color:var(--pos-orange-primary);">@FormatPrice(_orderItems.Sum(i => i.Price * i.Qty))</span>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 3:
|
||||
@* ═══ BẾP / KITCHEN STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="chef-hat" style="width:18px;height:18px;display:inline;"></i> Bếp đang chuẩn bị
|
||||
</div>
|
||||
@foreach (var item in _orderItems)
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:10px;
|
||||
background:var(--pos-bg-interactive);border-radius:8px;margin-bottom:8px;
|
||||
border-left:3px solid @(item.Status == "done" ? "var(--pos-success)" : item.Status == "cooking" ? "var(--pos-warning)" : "var(--pos-text-tertiary)");">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:13px;font-weight:600;">@item.Name</div>
|
||||
</div>
|
||||
<span style="font-size:12px;font-weight:600;color:@(item.Status == "done" ? "var(--pos-success)" : item.Status == "cooking" ? "var(--pos-warning)" : "var(--pos-text-tertiary)");">
|
||||
@(item.Status == "done" ? "Xong" : item.Status == "cooking" ? "Đang nấu" : "Chờ")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 4:
|
||||
@* ═══ TIẾP TỤC / CONTINUE STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="utensils-crossed" style="width:18px;height:18px;display:inline;"></i> Phục vụ bàn
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Trạng thái</span>
|
||||
<span style="font-weight:600;color:var(--pos-success);">Đã phục vụ xong</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Gọi thêm?</span>
|
||||
<span style="font-weight:600;">Có thể gọi thêm món</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:10px;background:var(--pos-bg-interactive);border-radius:8px;">
|
||||
<span style="color:var(--pos-text-secondary);">Thời gian ngồi</span>
|
||||
<span style="font-weight:600;">45 phút</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 5:
|
||||
@* ═══ THANH TOÁN / PAYMENT STEP ═══ *@
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
|
||||
<div style="font-size:15px;font-weight:700;margin-bottom:16px;">
|
||||
<i data-lucide="credit-card" style="width:18px;height:18px;display:inline;"></i> Thanh toán
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var item in _orderItems)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;">
|
||||
<span>@item.Qty x @item.Name</span>
|
||||
<span style="font-weight:600;">@FormatPrice(item.Price * item.Qty)</span>
|
||||
</div>
|
||||
}
|
||||
<div style="border-top:1px solid var(--pos-border-subtle);padding-top:10px;margin-top:4px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
|
||||
<span style="color:var(--pos-text-secondary);">Tạm tính</span>
|
||||
<span>@FormatPrice(_orderItems.Sum(i => i.Price * i.Qty))</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
|
||||
<span style="color:var(--pos-text-secondary);">VAT (8%)</span>
|
||||
<span>@FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 0.08m)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;padding-top:8px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<span>Tổng thanh toán</span>
|
||||
<span style="color:var(--pos-orange-primary);">@FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 1.08m)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case 6:
|
||||
@* ═══ HOÀN TẤT / COMPLETE STEP ═══ *@
|
||||
<div style="text-align:center;padding:40px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:50%;background:rgba(34,197,94,.15);
|
||||
display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<i data-lucide="check-circle" style="width:40px;height:40px;color:#22C55E;"></i>
|
||||
</div>
|
||||
<div style="font-size:20px;font-weight:700;color:var(--pos-success);margin-bottom:8px;">Hoàn tất!</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-secondary);margin-bottom:4px;">
|
||||
Cảm ơn quý khách Nguyễn Văn A
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--pos-text-tertiary);margin-bottom:20px;">
|
||||
Bàn 7 · 4 người · Tổng: @FormatPrice(_orderItems.Sum(i => i.Price * i.Qty) * 1.08m)
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;">
|
||||
<button style="padding:12px 24px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;">
|
||||
<i data-lucide="printer" style="width:14px;height:14px;display:inline;"></i> In hóa đơn
|
||||
</button>
|
||||
<button style="padding:12px 24px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;">
|
||||
<i data-lucide="star" style="width:14px;height:14px;display:inline;"></i> Đánh giá
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
@if (_currentStep > 0)
|
||||
{
|
||||
<button style="padding:12px 20px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="() => _currentStep--">
|
||||
<i data-lucide="arrow-left" style="width:14px;height:14px;display:inline;"></i> Quay lại
|
||||
</button>
|
||||
}
|
||||
<span style="flex:1;"></span>
|
||||
@if (_currentStep < _steps.Count - 1)
|
||||
{
|
||||
<button class="pos-btn-checkout" style="width:auto;padding:12px 24px;" @onclick="() => _currentStep++">
|
||||
Tiếp <i data-lucide="arrow-right" style="width:14px;height:14px;display:inline;"></i>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="pos-btn-checkout" style="width:auto;padding:12px 24px;" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="home" style="width:14px;height:14px;display:inline;"></i> Về trang chính
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _currentStep = 0;
|
||||
|
||||
// EN: Journey steps / VI: Các bước hành trình
|
||||
private readonly List<StepInfo> _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<TableInfo> _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<OrderItem> _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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="book-open" style="width:18px;height:18px;display:inline;"></i> Quản lý thực đơn
|
||||
</span>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="pos-category-tab pos-category-tab--active" @onclick="() => _editMode = !_editMode">
|
||||
<i data-lucide="@(_editMode ? "eye" : "pencil")" style="width:14px;"></i>
|
||||
@(_editMode ? "Xem" : "Chỉnh sửa")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ CATEGORY TABS / TAB DANH MỤC ═══ *@
|
||||
<div class="pos-category-tabs">
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<button class="pos-category-tab @(cat == _activeCategory ? "pos-category-tab--active" : "")"
|
||||
@onclick="() => _activeCategory = cat">@cat</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ QUICK ACTIONS / THAO TÁC NHANH ═══ *@
|
||||
@if (_editMode)
|
||||
{
|
||||
<div style="padding:8px 16px;display:flex;gap:8px;border-bottom:1px solid var(--pos-border-subtle);">
|
||||
<button style="padding:8px 14px;border-radius:8px;border:1px solid var(--pos-danger);
|
||||
background:rgba(239,68,68,.1);color:var(--pos-danger);font-size:12px;font-weight:600;cursor:pointer;"
|
||||
@onclick="MarkSoldOut">
|
||||
<i data-lucide="x-circle" style="width:12px;height:12px;display:inline;"></i> Hết món (86)
|
||||
</button>
|
||||
<button style="padding:8px 14px;border-radius:8px;border:1px solid var(--pos-orange-primary);
|
||||
background:rgba(255,92,0,.1);color:var(--pos-orange-primary);font-size:12px;font-weight:600;cursor:pointer;">
|
||||
<i data-lucide="star" style="width:12px;height:12px;display:inline;"></i> Đặc biệt hôm nay
|
||||
</button>
|
||||
<button style="padding:8px 14px;border-radius:8px;border:1px solid var(--pos-success);
|
||||
background:rgba(34,197,94,.1);color:var(--pos-success);font-size:12px;font-weight:600;cursor:pointer;">
|
||||
<i data-lucide="percent" style="width:12px;height:12px;display:inline;"></i> Giảm giá
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@
|
||||
<div style="flex:1;overflow-y:auto;padding:16px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;">
|
||||
@foreach (var item in FilteredMenu)
|
||||
{
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);overflow:hidden;
|
||||
opacity:@(item.Available ? "1" : "0.5");
|
||||
border:2px solid @(_selectedItem == item.Name ? "var(--pos-orange-primary)" : "transparent");"
|
||||
@onclick="() => _selectedItem = item.Name">
|
||||
@* EN: Photo placeholder / VI: Ảnh giữ chỗ *@
|
||||
<div style="height:100px;background:var(--pos-bg-interactive);display:flex;align-items:center;justify-content:center;position:relative;">
|
||||
<i data-lucide="@item.Icon" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
|
||||
@if (!item.Available)
|
||||
{
|
||||
<div style="position:absolute;top:8px;right:8px;background:var(--pos-danger);color:#fff;
|
||||
font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px;">86</div>
|
||||
}
|
||||
</div>
|
||||
<div style="padding:12px;">
|
||||
<div style="font-size:13px;font-weight:600;margin-bottom:4px;">@item.Name</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-size:14px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(item.Price)</span>
|
||||
@if (_editMode)
|
||||
{
|
||||
<button style="width:28px;height:28px;border-radius:50%;border:none;cursor:pointer;
|
||||
background:@(item.Available ? "rgba(34,197,94,.15)" : "rgba(239,68,68,.15)");
|
||||
color:@(item.Available ? "var(--pos-success)" : "var(--pos-danger)");font-size:14px;"
|
||||
@onclick="() => item.Available = !item.Available"
|
||||
@onclick:stopPropagation="true">
|
||||
@(item.Available ? "✓" : "✗")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(item.Note))
|
||||
{
|
||||
<div style="font-size:11px;color:var(--pos-warning);margin-top:4px;font-style:italic;">@item.Note</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@
|
||||
<div style="padding:10px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:20px;font-size:12px;color:var(--pos-text-secondary);">
|
||||
<span>Tổng: <b>@_menuItems.Count</b> món</span>
|
||||
<span style="color:var(--pos-success);">Có sẵn: @_menuItems.Count(i => i.Available)</span>
|
||||
<span style="color:var(--pos-danger);">Hết (86): @_menuItems.Count(i => !i.Available)</span>
|
||||
<span style="margin-left:auto;font-weight:600;">@_activeCategory</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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<RestMenuItem> _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<RestMenuItem> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="combine" style="width:18px;height:18px;display:inline;"></i> Ghép / Tách bàn
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@* ═══ MODE TABS / TAB CHẾ ĐỘ ═══ *@
|
||||
<div class="pos-category-tabs">
|
||||
<button class="pos-category-tab @(_mode == "merge" ? "pos-category-tab--active" : "")"
|
||||
@onclick="@(() => SwitchMode("merge"))">
|
||||
<i data-lucide="merge" style="width:14px;"></i> Ghép bàn
|
||||
</button>
|
||||
<button class="pos-category-tab @(_mode == "split" ? "pos-category-tab--active" : "")"
|
||||
@onclick="@(() => SwitchMode("split"))">
|
||||
<i data-lucide="split" style="width:14px;"></i> Tách bàn
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding:16px;">
|
||||
@if (_mode == "merge")
|
||||
{
|
||||
@* ═══ MERGE MODE / CHẾ ĐỘ GHÉP ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
Chọn bàn để ghép (2+ bàn)
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;">
|
||||
@foreach (var table in _mergeTables)
|
||||
{
|
||||
var isSelected = _mergeSelected.Contains(table.Id);
|
||||
<div style="padding:16px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
|
||||
background:@(isSelected ? "rgba(255,92,0,.15)" : "var(--pos-bg-elevated)");
|
||||
border:2px solid @(isSelected ? "var(--pos-orange-primary)" : "transparent");
|
||||
transition:all .2s ease;"
|
||||
@onclick="() => ToggleMerge(table.Id)">
|
||||
@if (isSelected)
|
||||
{
|
||||
<div style="width:20px;height:20px;border-radius:50%;background:var(--pos-orange-primary);
|
||||
display:flex;align-items:center;justify-content:center;margin:0 auto 8px;">
|
||||
<i data-lucide="check" style="width:12px;height:12px;color:#fff;"></i>
|
||||
</div>
|
||||
}
|
||||
<div style="font-size:18px;font-weight:700;">@table.Name</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:4px;">@table.Seats chỗ</div>
|
||||
<div style="font-size:11px;font-weight:600;margin-top:4px;color:@(table.Status == "available" ? "var(--pos-success)" : "var(--pos-text-tertiary)");">
|
||||
@(table.Status == "available" ? "Trống" : "Đang dùng")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Merge preview / VI: Xem trước ghép *@
|
||||
@if (_mergeSelected.Count >= 2)
|
||||
{
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;
|
||||
border:1px solid var(--pos-orange-primary);">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:10px;color:var(--pos-orange-primary);">
|
||||
<i data-lucide="eye" style="width:14px;height:14px;display:inline;"></i> Xem trước
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
@foreach (var id in _mergeSelected)
|
||||
{
|
||||
var t = _mergeTables.First(x => x.Id == id);
|
||||
<span style="font-size:14px;font-weight:600;">@t.Name</span>
|
||||
@if (id != _mergeSelected.Last())
|
||||
{
|
||||
<span style="color:var(--pos-text-tertiary);">+</span>
|
||||
}
|
||||
}
|
||||
<span style="color:var(--pos-text-tertiary);">→</span>
|
||||
<span style="font-size:16px;font-weight:700;color:var(--pos-orange-primary);">
|
||||
@MergedName (@MergedCapacity chỗ)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ═══ SPLIT MODE / CHẾ ĐỘ TÁCH ═══ *@
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--pos-text-secondary);">
|
||||
Chọn bàn để tách
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;">
|
||||
@foreach (var table in _splitTables)
|
||||
{
|
||||
var isSelected = _splitSelected == table.Id;
|
||||
<div style="padding:16px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
|
||||
background:@(isSelected ? "rgba(59,130,246,.15)" : "var(--pos-bg-elevated)");
|
||||
border:2px solid @(isSelected ? "var(--pos-info)" : "transparent");"
|
||||
@onclick="() => _splitSelected = table.Id">
|
||||
<div style="font-size:18px;font-weight:700;">@table.Name</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:4px;">@table.Seats chỗ</div>
|
||||
<div style="font-size:11px;color:var(--pos-orange-primary);font-weight:600;margin-top:4px;">
|
||||
@table.OrderCount món
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Split preview / VI: Xem trước tách *@
|
||||
@if (_splitSelected is not null)
|
||||
{
|
||||
var selected = _splitTables.First(t => t.Id == _splitSelected);
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;
|
||||
border:1px solid var(--pos-info);">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:10px;color:var(--pos-info);">
|
||||
<i data-lucide="eye" style="width:14px;height:14px;display:inline;"></i> Xem trước tách
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<span style="font-size:14px;font-weight:600;">@selected.Name (@selected.Seats chỗ)</span>
|
||||
<span style="color:var(--pos-text-tertiary);">→</span>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--pos-info);">
|
||||
@(selected.Name)A (@(selected.Seats / 2) chỗ)
|
||||
</span>
|
||||
<span style="color:var(--pos-text-tertiary);">+</span>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--pos-info);">
|
||||
@(selected.Name)B (@(selected.Seats - selected.Seats / 2) chỗ)
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:8px;">
|
||||
@selected.OrderCount món sẽ được chia giữa 2 bàn
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("restaurant"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button class="pos-btn-checkout" style="flex:1;" @onclick="ConfirmAction"
|
||||
disabled="@(!CanConfirm)">
|
||||
<i data-lucide="check" style="width:14px;height:14px;display:inline;"></i>
|
||||
@(_mode == "merge" ? "Xác nhận ghép" : "Xác nhận tách")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _mode = "merge";
|
||||
private readonly HashSet<string> _mergeSelected = new();
|
||||
private string? _splitSelected;
|
||||
|
||||
// EN: Tables for merge / VI: Bàn để ghép
|
||||
private readonly List<MergeTable> _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<SplitTable> _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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("restaurant"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">
|
||||
<i data-lucide="armchair" style="width:18px;height:18px;display:inline;"></i> Chọn bàn
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@* ═══ GUEST COUNT + SECTION FILTER / SỐ KHÁCH + LỌC KHU VỰC ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:16px;">
|
||||
@* EN: Guest count input / VI: Nhập số khách *@
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="users" style="width:16px;height:16px;color:var(--pos-text-tertiary);"></i>
|
||||
<span style="font-size:13px;color:var(--pos-text-secondary);">Số khách:</span>
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<button style="width:32px;height:32px;border-radius:8px;border:1px solid var(--pos-border-default);
|
||||
background:var(--pos-bg-interactive);color:var(--pos-text-primary);cursor:pointer;font-size:16px;font-weight:700;"
|
||||
@onclick="() => { if (_guestCount > 1) _guestCount--; }">−</button>
|
||||
<span style="width:32px;text-align:center;font-size:18px;font-weight:700;">@_guestCount</span>
|
||||
<button style="width:32px;height:32px;border-radius:8px;border:1px solid var(--pos-border-default);
|
||||
background:var(--pos-bg-interactive);color:var(--pos-text-primary);cursor:pointer;font-size:16px;font-weight:700;"
|
||||
@onclick="() => _guestCount++">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* EN: Section filter / VI: Lọc khu vực *@
|
||||
<div class="pos-category-tabs" style="padding:0;flex:1;">
|
||||
@foreach (var sec in _sections)
|
||||
{
|
||||
<button class="pos-category-tab @(sec == _activeSection ? "pos-category-tab--active" : "")"
|
||||
style="font-size:12px;padding:6px 12px;" @onclick="() => _activeSection = sec">@sec</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABLE GRID / LƯỚI BÀN ═══ *@
|
||||
<div style="flex:1;overflow-y:auto;padding:16px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;">
|
||||
@foreach (var table in FilteredTables)
|
||||
{
|
||||
var matchColor = CapacityColor(table.Seats, _guestCount);
|
||||
var isSelected = _selectedTable == table.Id;
|
||||
|
||||
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:18px;
|
||||
text-align:center;cursor:pointer;transition:all .2s ease;
|
||||
border:2px solid @(isSelected ? "var(--pos-orange-primary)" : "transparent");"
|
||||
@onclick="() => _selectedTable = table.Id">
|
||||
@* EN: Capacity indicator / VI: Chỉ báo sức chứa *@
|
||||
<div style="width:12px;height:12px;border-radius:50%;background:@matchColor;margin:0 auto 8px;"></div>
|
||||
<div style="font-size:22px;font-weight:800;">@table.Name</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:6px;">
|
||||
<i data-lucide="users" style="width:12px;height:12px;display:inline;"></i> @table.Seats chỗ
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:2px;">@table.Section</div>
|
||||
|
||||
@if (isSelected)
|
||||
{
|
||||
<div style="margin-top:8px;width:24px;height:24px;border-radius:50%;background:var(--pos-orange-primary);
|
||||
display:flex;align-items:center;justify-content:center;margin-left:auto;margin-right:auto;">
|
||||
<i data-lucide="check" style="width:14px;height:14px;color:#fff;"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!FilteredTables.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:40px;color:var(--pos-text-tertiary);">
|
||||
<i data-lucide="search-x" style="width:40px;height:40px;display:block;margin:0 auto 12px;"></i>
|
||||
<div style="font-size:14px;">Không tìm thấy bàn trống phù hợp</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@
|
||||
@if (_selectedTable is not null)
|
||||
{
|
||||
var sel = _availableTables.First(t => t.Id == _selectedTable);
|
||||
<div style="padding:10px 16px;background:var(--pos-bg-elevated);border-top:1px solid var(--pos-border-subtle);
|
||||
display:flex;align-items:center;gap:16px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:14px;font-weight:700;">@sel.Name</span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">@sel.Seats chỗ · @sel.Section</span>
|
||||
</div>
|
||||
<span style="flex:1;"></span>
|
||||
<div style="width:10px;height:10px;border-radius:50%;background:@CapacityColor(sel.Seats, _guestCount);"></div>
|
||||
<span style="font-size:12px;color:var(--pos-text-secondary);">
|
||||
@CapacityLabel(sel.Seats, _guestCount)
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ═══ FOOTER ACTIONS / NÚT HÀNH ĐỘNG ═══ *@
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:8px;flex-shrink:0;">
|
||||
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
|
||||
border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;"
|
||||
@onclick="@(() => NavigateTo("restaurant"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button class="pos-btn-checkout" style="flex:2;" @onclick="OpenTable"
|
||||
disabled="@(_selectedTable is null)">
|
||||
<i data-lucide="door-open" style="width:14px;height:14px;display:inline;"></i> Mở bàn
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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<AvailableTable> _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<AvailableTable> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user