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:
Cursor Agent
2026-02-26 18:36:08 +00:00
parent 8953a6c1d9
commit ccf72aa5d0
9 changed files with 1757 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}