diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopAppointments.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopAppointments.razor new file mode 100644 index 00000000..4ff25df6 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopAppointments.razor @@ -0,0 +1,136 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+ +
+@if (_showApptForm) +{ +
+

Thêm lịch hẹn mới

+
+
+
+
+
+
+ + +
+ @if (_apptFormMessage != null) {
@_apptFormMessage
} +
+
+} +@{ + var calWeekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + 1 + _calendarWeekOffset * 7); + var calWeekEnd = calWeekStart.AddDays(7); + var weekAppts = _appointments.Where(a => a.StartTime >= calWeekStart && a.StartTime < calWeekEnd).ToList(); +} +
+
+
@_appointments.CountTổng lịch hẹn
+
@_appointments.Count(a => a.Status == "Confirmed")Đã xác nhận
+
@_appointments.Count(a => a.Status == "Pending")Chờ xác nhận
+
@weekAppts.CountTuần này
+
+
+
+
+

Lịch hẹn tuần @calWeekStart.ToString("dd/MM") — @calWeekEnd.AddDays(-1).ToString("dd/MM")

+
+ + + +
+
+
+
+ @for (int d = 0; d < 7; d++) + { + var day = calWeekStart.AddDays(d); + var dayAppts = weekAppts.Where(a => a.StartTime.Date == day.Date).OrderBy(a => a.StartTime).ToList(); + var isToday = day.Date == DateTime.Today; +
+
+
@DayLabel((int)day.DayOfWeek)
+
@day.Day
+
+ @foreach (var appt in dayAppts) + { + var apptColor = appt.Status switch { "Confirmed" => "#22C55E", "Pending" => "#F59E0B", "Completed" => "#3B82F6", _ => "#6B6B6F" }; +
+
@appt.StartTime.ToString("HH:mm")-@appt.EndTime.ToString("HH:mm")
+
@(appt.ResourceName ?? "—")
+ @if (appt.Status != "Cancelled" && appt.Status != "Completed") + { + + } +
+ } + @if (!dayAppts.Any()) + { +
+ } +
+ } +
+
+
+ +@code { + [Parameter] public Guid ShopId { get; set; } + + // Appointments state + private List _appointments = new(); + // Appointments form state + private bool _showApptForm; + private DateTime _newApptStart = DateTime.Today.AddHours(9); + private DateTime _newApptEnd = DateTime.Today.AddHours(10); + private string? _apptFormMessage; + private bool _apptFormSuccess; + private int _calendarWeekOffset; + private string? _errorMessage; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + _appointments = await DataService.GetAppointmentsAsync(ShopId); + } + + private async Task AddAppointment() + { + _apptFormMessage = null; + if (ShopId == Guid.Empty) + { + _apptFormMessage = "Thiếu thông tin cửa hàng."; _apptFormSuccess = false; return; + } + try + { + await DataService.CreateAppointmentAsync(new PosDataService.CreateAppointmentRequest( + ShopId, null, null, null, null, _newApptStart, _newApptEnd)); + _apptFormMessage = "Đã thêm lịch hẹn thành công!"; _apptFormSuccess = true; + _showApptForm = false; + _appointments = await DataService.GetAppointmentsAsync(ShopId); + } + catch (Exception ex) { _apptFormMessage = $"Lỗi: {ex.Message}"; _apptFormSuccess = false; } + } + + private async Task CancelAppt(Guid apptId) + { + try + { + await DataService.CancelAppointmentAsync(apptId); + if (ShopId != Guid.Empty) _appointments = await DataService.GetAppointmentsAsync(ShopId); + } + catch (Exception ex) { _errorMessage = $"Không thể hủy lịch hẹn: {ex.Message}"; } + } + + private static string DayLabel(int dow) => dow switch + { + 0 => "CN", 1 => "T2", 2 => "T3", 3 => "T4", + 4 => "T5", 5 => "T6", 6 => "T7", _ => $"#{dow}" + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopCustomers.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopCustomers.razor new file mode 100644 index 00000000..02951224 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopCustomers.razor @@ -0,0 +1,491 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService +@inject NavigationManager Nav + +
+
+ + Dữ liệu khách hàng chung cho tất cả cửa hàng trong thương hiệu +
+
+ @foreach (var (tab, label) in new[] { ("members","Khách hàng"), ("levels","Cấp bậc"), ("exp","Điểm EXP") }) + { + + } +
+ @if (_customerSubTab == "levels") + { + @* ─── Level CRUD ─── *@ +
+

@_memberLevels.Count cấp bậc

+ +
+ @if (_showLevelForm) + { +
+

@(_editingLevelId.HasValue ? "Sửa cấp bậc" : "Thêm cấp bậc")

+
+
+
+
+
+
+
+
+ @if (_levelFormMessage != null) + { +
@_levelFormMessage
+ } +
+ + +
+
+
+ } +
+
+ + + + + + + + + + @foreach (var lvl in _memberLevels.OrderBy(l => l.Level)) + { + + + + + + + + + + } +
LevelTênEXP cầnMô tảMàuThành viên
@lvl.Level@lvl.Name@lvl.RequiredExp.ToString("N0")@(lvl.Description ?? "—")@lvl.MemberCount + + +
+
+
+ } + else if (_customerSubTab == "exp") + { + @* ─── EXP Management ─── *@ +
+

Cộng điểm EXP

+
+
+
+ +
+
+
+ +
+
+ @if (_expFormMessage != null) + { +
@_expFormMessage
+ } + +
+
+ @if (_memberProgress != null) + { +
+

Tiến trình: @(_memberProgress.CurrentLevelName ?? "Level") @_memberProgress.CurrentLevel

+
+
+
EXP hiện tại
@_memberProgress.CurrentExp.ToString("N0")
+
Tổng EXP
@_memberProgress.TotalExpEarned.ToString("N0")
+
Cần thêm
@_memberProgress.ExpToNextLevel.ToString("N0")
+
Level kế
@(_memberProgress.NextLevelName ?? "Max")
+
+
+
+
+
@_memberProgress.ProgressPercent%
+
+
+ } + @if (_expHistory.Any()) + { +
+

Lịch sử EXP

+
+ + + + + + + + @foreach (var tx in _expHistory) + { + + + + + + + + } +
NgàyĐiểmNguồnTham chiếuLevel
@tx.CreatedAt.ToString("dd/MM/yyyy HH:mm")@(tx.Points > 0 ? "+" : "")@tx.Points@(tx.Source ?? "—")@(tx.ReferenceId ?? "—")@tx.LevelAtTime
+
+
+ } + } + else + { + @* ─── Members list ─── *@ +
+

@_members.Count khách hàng

+
+
+ + +
+ +
+
+ @if (_showMemberForm) + { +
+

@(_editingMemberId.HasValue ? "Sửa khách hàng" : "Thêm khách hàng")

+
+
+
+
+
+ +
+
+
+ @if (_memberFormMessage != null) + { +
@_memberFormMessage
+ } +
+ + +
+
+
+ } + var filteredMembers = string.IsNullOrWhiteSpace(_customerSearch) + ? _members + : _members.Where(m => m.Id.ToString().Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) + || (m.DisplayName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) + || (m.Phone ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) + || (m.LevelName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)).ToList(); + @if (!filteredMembers.Any()) + { +
+
+ +
+

Chưa có khách hàng

+

Khách hàng sẽ hiển thị khi có giao dịch

+ + + Mở POS bán hàng + +
+ } + else + { +
+
@filteredMembers.CountTổng khách hàng
+
@_memberLevels.CountCấp bậc
+
@(filteredMembers.Any() ? filteredMembers.Max(m => m.TotalExpEarned).ToString("N0") : "0")EXP cao nhất
+
+ @if (_memberLevels.Any()) + { +
+

Cấp bậc thành viên

+
+ + + + + + + @foreach (var lvl in _memberLevels.OrderBy(l => l.Level)) + { + + + + + + + } +
LevelTênEXP cầnThành viên
@lvl.Level@lvl.Name@lvl.MinExp.ToString("N0") — @lvl.MaxExp.ToString("N0")@lvl.MemberCount
+
+
+ } +
+

Danh sách khách hàng

+
+ + + + + + + + + @foreach (var m in filteredMembers) + { + var isExpanded = _selectedCustomerId == m.Id; + + + + + + + + + @if (isExpanded) + { + + } + } +
TênSĐTCấp bậcEXPNgày tham gia
@(m.DisplayName ?? m.Id.ToString()[..8])@(m.Phone ?? "—")@(m.LevelName ?? "—")@m.TotalExpEarned.ToString("N0")@m.CreatedAt.ToString("dd/MM/yyyy")
+
+
+
+ +
@m.Id.ToString()
+
+
+ +
@(m.LevelName ?? "Chưa xếp hạng") - @m.TotalExpEarned.ToString("N0") EXP
+
+
+ +
@m.CreatedAt.ToString("dd/MM/yyyy HH:mm")
+
+
+
+ + + + + +
+
+
+
+
+ } + } +
+ +@if (!string.IsNullOrEmpty(_toastMessage)) +{ +
+ @_toastMessage +
+} + +@code { + [Parameter] public Guid ShopGuid { get; set; } + [Parameter] public string ShopId { get; set; } = ""; + [Parameter] public string PosVertical { get; set; } = "cafe"; + + // Members data + private List _members = new(); + private List _memberLevels = new(); + + // Customer sub-tab + private string _customerSubTab = "members"; + private string _customerSearch = ""; + private Guid? _selectedCustomerId; + + // Member form + private bool _showMemberForm; + private Guid? _editingMemberId; + private string _newMemberGender = ""; + private string _newMemberCountry = "VN"; + private string _newMemberName = ""; + private string _newMemberPhone = ""; + private string? _memberFormMessage; + private bool _memberFormSuccess; + + // Level form + private bool _showLevelForm; + private Guid? _editingLevelId; + private int _newLevelNumber; + private string _newLevelName = ""; + private int _newLevelRequiredExp; + private string _newLevelDescription = ""; + private string _newLevelBadgeColor = "#CD7F32"; + private string? _levelFormMessage; + private bool _levelFormSuccess; + + // EXP management + private Guid? _expMemberId; + private int _expPoints = 100; + private int _expSourceId = 7; + private string? _expFormMessage; + private bool _expFormSuccess; + private PosDataService.MemberProgressInfo? _memberProgress; + private List _expHistory = new(); + + // Toast + private string? _toastMessage; + + protected override async Task OnInitializedAsync() + { + await RefreshMembersAndLevels(); + } + + private async Task RefreshMembersAndLevels() + { + var rm = await DataService.GetMembersAsync(); + var rl = await DataService.GetMembershipLevelsAsync(); + _memberLevels = PosDataService.EnrichLevelDefinitions(rl, rm); + _members = PosDataService.ResolveMemberLevelNames(rm, rl); + } + + private async Task SaveMember() + { + _memberFormMessage = null; + if (_editingMemberId.HasValue) + { + var ok = await DataService.UpdateMemberAsync(_editingMemberId.Value, new PosDataService.UpdateMemberRequest(_newMemberGender, null)); + if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; await RefreshMembersAndLevels(); } + } + else + { + var (ok, err) = await DataService.CreateMemberAsync(new PosDataService.CreateMemberRequest(_newMemberGender, _newMemberCountry, + string.IsNullOrWhiteSpace(_newMemberName) ? null : _newMemberName, + string.IsNullOrWhiteSpace(_newMemberPhone) ? null : _newMemberPhone)); + if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; _memberFormMessage = null; await RefreshMembersAndLevels(); } + else { _memberFormMessage = err ?? "Lỗi tạo khách hàng."; _memberFormSuccess = false; } + } + } + + private void EditMember(PosDataService.MemberInfo m) + { + _editingMemberId = m.Id; _newMemberGender = m.Gender ?? ""; _newMemberCountry = m.CountryCode ?? "VN"; + _newMemberName = m.DisplayName ?? ""; _newMemberPhone = m.Phone ?? ""; + _showMemberForm = true; + } + + private async Task DeleteMemberItem(Guid memberId) + { + await DataService.DeleteMemberAsync(memberId); + await RefreshMembersAndLevels(); + } + + private async Task SaveLevel() + { + _levelFormMessage = null; + if (string.IsNullOrWhiteSpace(_newLevelName) || _newLevelNumber <= 0) + { _levelFormMessage = "Nhập tên và số level."; _levelFormSuccess = false; return; } + var req = new PosDataService.CreateLevelRequest(_newLevelNumber, _newLevelName, _newLevelRequiredExp, _newLevelDescription, _newLevelBadgeColor); + if (_editingLevelId.HasValue) + { + var (ok, err) = await DataService.UpdateLevelAsync(_editingLevelId.Value, req); + _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã cập nhật!" : err ?? "Lỗi."; + } + else + { + var (ok, err) = await DataService.CreateLevelAsync(req); + _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã thêm!" : err ?? "Lỗi."; + } + if (_levelFormSuccess) { _showLevelForm = false; await RefreshMembersAndLevels(); } + } + + private void EditLevel(PosDataService.LevelDefinitionInfo lvl) + { + _editingLevelId = lvl.Id; _newLevelNumber = lvl.LevelNumber; _newLevelName = lvl.Name; + _newLevelRequiredExp = lvl.RequiredExp; _newLevelDescription = lvl.Description ?? ""; + _newLevelBadgeColor = lvl.BadgeColor ?? "#CD7F32"; _showLevelForm = true; + } + + private async Task DeleteLevel(Guid id) + { + await DataService.DeleteLevelAsync(id); + await RefreshMembersAndLevels(); + } + + private async Task AddExpToMember() + { + _expFormMessage = null; + if (!_expMemberId.HasValue || _expPoints <= 0) + { _expFormMessage = "Chọn thành viên và nhập số điểm > 0."; _expFormSuccess = false; return; } + var result = await DataService.AddExperienceAsync(_expMemberId.Value, new PosDataService.AddExpRequest(_expPoints, _expSourceId, null)); + if (result != null) + { + _expFormSuccess = true; + _expFormMessage = result.LeveledUp ? $"Đã cộng {result.PointsAdded} EXP! Lên level {result.CurrentLevel}!" : $"Đã cộng {result.PointsAdded} EXP! Tổng: {result.TotalExpEarned}"; + _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value); + _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value); + await RefreshMembersAndLevels(); + } + else { _expFormMessage = "Lỗi khi cộng EXP."; _expFormSuccess = false; } + } + + private async Task LoadMemberProgress() + { + if (_expMemberId.HasValue) + { + _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value); + _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value); + } + else { _memberProgress = null; _expHistory = new(); } + } + + private void ShowComingSoonPromo() => ShowComingSoon("Tặng ưu đãi"); + private void ShowComingSoonMessage() => ShowComingSoon("Gửi tin nhắn"); + private void GoToOrderHistory() => Nav.NavigateTo($"/admin/shop/{ShopId}/finance"); + + private async void ShowComingSoon(string feature) + { + _toastMessage = $"{feature} — tính năng sắp ra mắt!"; + StateHasChanged(); + await Task.Delay(3000); + _toastMessage = null; + StateHasChanged(); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopDrive.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopDrive.razor new file mode 100644 index 00000000..5508e7ef --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopDrive.razor @@ -0,0 +1,114 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService +@inject NavigationManager Nav + +
+
+
+
+@if (_showFolderForm) +{ +
+} +@if (_currentFolderId.HasValue) +{ +
+} +@if (_storageFolders.Any()) +{ +
+ @foreach (var folder in _storageFolders) + { +
@folder.Name
@folder.CreatedAt.ToString("dd/MM/yyyy")
+ } +
+} +

Tệp (@_storageFiles.Count)

+@if (!_storageFiles.Any()) +{ +
Chưa có tệp nào.
+} +else +{ + + @foreach (var f in _storageFiles) + { + + } +
Tên tệpLoạiKích thướcNgày uploadThao tác
@f.FileName@(f.ContentType ?? "—")@ShopHelpers.FormatFileSize(f.FileSizeBytes)@f.UploadedAt.ToString("dd/MM/yyyy HH:mm")
+} +
+ +@code { + [Parameter] public Guid ShopId { get; set; } + + private List _storageFiles = new(); + private List _storageFolders = new(); + private Guid? _currentFolderId; + private string _newFolderName = ""; + private bool _showFolderForm; + private string _storageSearch = ""; + + protected override async Task OnInitializedAsync() + { + _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); + _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + } + + private void ToggleFolderForm() => _showFolderForm = !_showFolderForm; + private void HideFolderForm() => _showFolderForm = false; + + private async Task SearchStorageFiles() => _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + + private async Task NavigateToFolder(Guid id) + { + _currentFolderId = id; + _storageFolders = await DataService.GetFoldersAsync(id); + _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + } + + private async Task NavigateToParentFolder() + { + _currentFolderId = null; + _storageFolders = await DataService.GetFoldersAsync(null); + _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + } + + private async Task CreateFolder() + { + if (string.IsNullOrWhiteSpace(_newFolderName)) return; + await DataService.CreateFolderAsync(new PosDataService.CreateFolderRequest(_newFolderName.Trim(), _currentFolderId)); + _newFolderName = ""; + _showFolderForm = false; + _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); + } + + private async Task DeleteFolder(Guid id) + { + await DataService.DeleteFolderAsync(id); + _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); + } + + private async Task DeleteStorageFile(Guid id) + { + await DataService.DeleteStorageFileAsync(id); + _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + } + + private async Task DownloadStorageFile(Guid id) + { + var url = await DataService.GetDownloadUrlAsync(id); + if (url != null) Nav.NavigateTo(url, forceLoad: true); + } + + private async Task HandleDriveUpload(InputFileChangeEventArgs e) + { + foreach (var f in e.GetMultipleFiles(20)) + { + using var s = f.OpenReadStream(10_485_760); + await DataService.UploadImageAsync(s, f.Name, f.ContentType); + } + _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor new file mode 100644 index 00000000..43d2894d --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor @@ -0,0 +1,180 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+ + Đơn hàng theo cửa hàng · Ví tiền chung cho tài khoản +
+@{ + var finOrders = _financePeriod switch { + "7d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7)).ToList(), + "30d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-30)).ToList(), + _ => _orders }; +} +
+

Tài chính

+
+ @foreach (var (label, val) in new[] { ("7 ngày", "7d"), ("30 ngày", "30d"), ("Tất cả", "all") }) + { + + } +
+
+
+
@ShopHelpers.FormatVND(finOrders.Sum(o => o.TotalAmount))Tổng doanh thu
+
@finOrders.CountĐơn hàng
+
@ShopHelpers.FormatVND(finOrders.Any() ? finOrders.Average(o => o.TotalAmount) : 0)TB/đơn
+
@ShopHelpers.FormatVND(_wallets.Sum(w => w.Balance))Số dư ví
+
+@if (_walletTxns.Any()) +{ +
+

Giao dịch ví gần đây

+
+ + + + + + @foreach (var t in _walletTxns.Take(15)) + { + + + + + + } +
Mô tảSố tiềnNgày
@(t.Description ?? t.ItemName ?? "—")@(t.Amount >= 0 ? "+" : "")@ShopHelpers.FormatVND(t.Amount)@t.CreatedAt.ToString("dd/MM HH:mm")
+
+
+} +@if (_orders.Any()) +{ +
+

Đơn hàng gần đây

+
+ + + + + + + + @foreach (var o in _orders.Take(20)) + { + var isOrderExpanded = _selectedOrderId == o.Id; + + + + + + + + @if (isOrderExpanded && _orderDetail != null) + { + + } + } +
IDSố tiềnTrạng tháiNgày
@o.Id.ToString()[..8]@ShopHelpers.FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM HH:mm")
+
+
+
@(_orderDetail.Order?.PaymentMethod ?? "—")
+
@(_orderDetail.Order?.Notes ?? "—")
+
@(_orderDetail.Order?.CreatedAt.ToString("dd/MM/yyyy HH:mm") ?? "—")
+
+ @if (_orderDetail.Items?.Any() == true) + { + + + + @foreach (var item in _orderDetail.Items) + { + + + + + + + } +
Sản phẩmSLĐơn giáThành tiền
@(item.ProductName ?? "—")@item.Quantity@ShopHelpers.FormatVND(item.UnitPrice)@ShopHelpers.FormatVND(item.Subtotal)
+ } +
+ +
+
+
+
+
+} +else +{ + @RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{PosVertical}") +} + +@code { + [Parameter] public Guid ShopId { get; set; } + [Parameter] public string PosVertical { get; set; } = "cafe"; + + // Finance data + private List _orders = new(); + private List _wallets = new(); + private List _walletTxns = new(); + // Finance date range filter state + private string _financePeriod = "all"; // 7d, 30d, all + // Order detail state + private Guid? _selectedOrderId; + private PosDataService.OrderDetailResponse? _orderDetail; + + protected override async Task OnInitializedAsync() + { + _orders = await DataService.GetOrdersAsync(ShopId == Guid.Empty ? null : ShopId); + _wallets = await DataService.GetWalletsAsync(); + _walletTxns = await DataService.GetWalletTransactionsAsync(); + } + + // ═══ ORDER DETAIL ═══ + private async Task ViewOrderDetail(Guid orderId) + { + if (_selectedOrderId == orderId) { _selectedOrderId = null; _orderDetail = null; return; } + _selectedOrderId = orderId; + try { _orderDetail = await DataService.GetOrderDetailAsync(orderId, ShopId == Guid.Empty ? null : ShopId); } + catch { _orderDetail = null; } + } + + private async Task CancelOrderItem(Guid orderId) + { + var ok = await DataService.CancelOrderAsync(orderId); + if (ok) + { + _selectedOrderId = null; + _orderDetail = null; + _orders = await DataService.GetOrdersAsync(ShopId == Guid.Empty ? null : ShopId); + } + } + + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => + { +
+
+ +
+

@title

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + } +
+ }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopHelpers.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopHelpers.cs new file mode 100644 index 00000000..a46e85e5 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopHelpers.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace WebClientTpos.Client.Pages.Admin.Shop; + +/// +/// EN: Shared static helpers used across Shop sub-components. +/// VI: Các helper tĩnh dùng chung cho các component con của Shop. +/// +public static class ShopHelpers +{ + public static string FormatVND(decimal val) => val.ToString("N0") + " ₫"; + + public static string HexToRgb(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length != 6) return "0,0,0"; + return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}"; + } + + public static string DayLabel(int dow) => dow switch + { + 0 => "CN", 1 => "T2", 2 => "T3", 3 => "T4", + 4 => "T5", 5 => "T6", 6 => "T7", _ => $"#{dow}" + }; + + public static string FormatFileSize(long b) => b < 1024 ? $"{b} B" : b < 1048576 ? $"{b / 1024.0:F1} KB" : b < 1073741824 ? $"{b / 1048576.0:F1} MB" : $"{b / 1073741824.0:F2} GB"; + + public static string GetFileIcon(string? ct) => ct switch { string s when s.StartsWith("image/") => "image", string s when s.StartsWith("video/") => "video", string s when s.Contains("pdf") => "file-text", _ => "file" }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopInventory.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopInventory.razor new file mode 100644 index 00000000..d845bad9 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopInventory.razor @@ -0,0 +1,312 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+ @if (!_inventory.Any() && _invSubTab == "levels") + { +
+
+ +
+

Chưa có tồn kho

+

Tồn kho sẽ hiển thị khi có sản phẩm

+
+ } + else + { + +
+
@_inventory.Count(i => i.Quantity > 10)Còn hàng
+
@_inventory.Count(i => i.Quantity > 0 && i.Quantity <= 10)Sắp hết
+
@_inventory.Count(i => i.Quantity <= 0)Hết hàng
+
+ + +
+ @foreach (var (label, val, icon) in new[] { ("Tồn kho", "levels", "package"), ("Nhập kho", "stock-in", "arrow-down-to-line"), ("Xuất kho", "stock-out", "arrow-up-from-line"), ("Điều chỉnh", "adjust", "settings-2"), ("Lịch sử", "transactions", "history"), ("Cảnh báo", "low-stock", "alert-triangle") }) + { + + } +
+ + @if (_invFormMessage != null) + { +
+ @_invFormMessage +
+ } + + @switch (_invSubTab) + { + case "levels": +
+
+ + + + + + + @foreach (var item in _inventory) + { + var qtyColor = item.Quantity <= 0 ? "#EF4444" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "#F59E0B" : "#22C55E"; + var bgColor = item.Quantity <= 0 ? "rgba(239,68,68,0.05)" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "rgba(245,158,11,0.05)" : "transparent"; + + + + + + + } +
Sản phẩmSố lượngMức nhập lạiThao tác
@(item.ProductName ?? item.ProductId.ToString()[..8])@item.Quantity@item.ReorderLevel +
+ + +
+
+
+
+ break; + + case "stock-in": +
+

Nhập kho

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ break; + + case "stock-out": +
+

Xuất kho

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ break; + + case "adjust": +
+

Điều chỉnh tồn kho

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ break; + + case "transactions": +
+

Lịch sử giao dịch kho

+
+ @if (_invTxns.Any()) + { + + + + + + + @foreach (var tx in _invTxns.OrderByDescending(t => t.CreatedAt).Take(50)) + { + var txColor = tx.QuantityChange > 0 ? "#22C55E" : tx.QuantityChange < 0 ? "#EF4444" : "#6B7280"; + var txLabel = tx.TransactionType switch { "StockIn" => "Nhập kho", "StockOut" => "Xuất kho", "Adjustment" => "Điều chỉnh", "OrderDeduction" => "Đơn hàng", _ => tx.TransactionType ?? "N/A" }; + + + + + + + } +
Thời gianLoạiSố lượngLý do
@tx.CreatedAt.ToLocalTime().ToString("dd/MM HH:mm") + @txLabel + @(tx.QuantityChange > 0 ? "+" : "")@tx.QuantityChange@(tx.Reason ?? "—")
+ } + else + { +
Chưa có giao dịch kho nào.
+ } +
+
+ break; + + case "low-stock": +
+
+

Cảnh báo tồn kho thấp

+ +
+
+ @if (_lowStockItems.Any()) + { + + + + + + + @foreach (var item in _lowStockItems) + { + + + + + + + } +
Sản phẩmTồn khoNgưỡngHành động
@(item.ProductName ?? item.ProductId.ToString()[..8])@item.Quantity@item.LowStockThreshold + +
+ } + else + { +
+
+ Tất cả sản phẩm đều đủ hàng! +
+ } +
+
+ break; + } + } +
+ +@code { + [Parameter] public Guid ShopId { get; set; } + + private Guid? _shopGuid => ShopId != Guid.Empty ? ShopId : null; + + private List _inventory = new(); + private List _invTxns = new(); + private string _invSubTab = "levels"; + private Guid _invSelectedProductId; + private int _invAmount; + private int _invNewQty; + private string _invNotes = ""; + private string? _invFormMessage; + private bool _invFormSuccess; + private List _lowStockItems = new(); + + protected override async Task OnInitializedAsync() + { + _inventory = await DataService.GetInventoryAsync(_shopGuid); + _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); + } + + private async Task SwitchInvSubTab(string tab) + { + _invSubTab = tab; + _invFormMessage = null; + if (tab == "low-stock") await LoadLowStock(); + StateHasChanged(); + } + + private async Task LoadLowStock() + { + _lowStockItems = await DataService.GetLowStockAsync(_shopGuid); + StateHasChanged(); + } + + private async Task DoStockIn() + { + _invFormMessage = null; + if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; } + var ok = await DataService.StockInAsync(new PosDataService.StockInRequest(_invSelectedProductId, ShopId, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes)); + _invFormMessage = ok ? $"Đã nhập kho thành công +{_invAmount}!" : "Lỗi khi nhập kho."; + _invFormSuccess = ok; + if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } + } + + private async Task DoStockOut() + { + _invFormMessage = null; + if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; } + var ok = await DataService.StockOutAsync(new PosDataService.StockOutRequest(_invSelectedProductId, ShopId, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes)); + _invFormMessage = ok ? $"Đã xuất kho thành công -{_invAmount}!" : "Lỗi khi xuất kho. Kiểm tra số lượng tồn."; + _invFormSuccess = ok; + if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } + } + + private async Task DoAdjustStock() + { + _invFormMessage = null; + if (_invSelectedProductId == Guid.Empty || string.IsNullOrWhiteSpace(_invNotes)) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập lý do điều chỉnh."; _invFormSuccess = false; return; } + var ok = await DataService.AdjustStockAsync(new PosDataService.AdjustStockRequest(_invSelectedProductId, ShopId, _invNewQty, _invNotes)); + _invFormMessage = ok ? $"Đã điều chỉnh tồn kho = {_invNewQty}!" : "Lỗi khi điều chỉnh."; + _invFormSuccess = ok; + if (ok) { _invNewQty = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopKitchen.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopKitchen.razor new file mode 100644 index 00000000..2b43267c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopKitchen.razor @@ -0,0 +1,115 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+
+ @foreach (var st in new[] { ("all", "", "Tất cả"), ("pending", "clock", "Chờ"), ("preparing", "flame", "Đang làm"), ("completed", "check-circle", "Xong") }) + { + + } +
+
+ Chờ: @_kitchenTickets.Count(t => t.Status == "pending") + Đang làm: @_kitchenTickets.Count(t => t.Status == "preparing") + Xong: @_kitchenTickets.Count(t => t.Status == "completed") +
+
+@if (!_kitchenTickets.Any()) +{ + @RenderEmpty("flame", "#F59E0B", "Không có ticket bếp", "Ticket sẽ xuất hiện khi có đơn từ POS") +} +else +{ +
+ @foreach (var ticket in _kitchenTickets) + { + var ticketColor = ticket.Status switch { "pending" => "#F59E0B", "preparing" => "#3B82F6", "completed" => "#22C55E", _ => "#6B6B6F" }; + var ticketLabel = ticket.Status switch { "pending" => "Chờ", "preparing" => "Đang làm", "completed" => "Hoàn thành", _ => ticket.Status }; + var elapsed = (DateTime.UtcNow - ticket.CreatedAt).TotalMinutes; +
+
+ P@(ticket.Priority) + @ticketLabel +
+
@ticket.ItemName
+
@(ticket.Station ?? "Bếp chính")
+
+ @((int)elapsed) phút + @if (ticket.Status != "completed") + { + + } +
+
+ } +
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + + // Kitchen state + private List _kitchenTickets = new(); + private string _kitchenStatusFilter = "all"; + private string? _errorMessage; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + _kitchenTickets = await DataService.GetKitchenTicketsAsync(ShopId); + } + + // ═══ KITCHEN ═══ + private async Task LoadKitchenTickets(string status) + { + _kitchenStatusFilter = status; + try + { + if (ShopId != Guid.Empty) + _kitchenTickets = await DataService.GetKitchenTicketsAsync(ShopId, status); + } + catch (Exception ex) { _errorMessage = $"Không thể tải kitchen tickets: {ex.Message}"; } + StateHasChanged(); + } + + private async Task MarkTicketDone(Guid ticketId) + { + try + { + await DataService.UpdateTicketStatusAsync(ticketId, new PosDataService.UpdateTicketStatusRequest("completed")); + if (ShopId != Guid.Empty) + _kitchenTickets = await DataService.GetKitchenTicketsAsync(ShopId, _kitchenStatusFilter); + } + catch (Exception ex) { _errorMessage = $"Không thể cập nhật trạng thái: {ex.Message}"; } + StateHasChanged(); + } + + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => + { +
+
+ +
+

@title

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + } +
+ }; + + private static string HexToRgb(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length != 6) return "0,0,0"; + return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}"; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopMenu.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopMenu.razor new file mode 100644 index 00000000..8fd0f16a --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopMenu.razor @@ -0,0 +1,388 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+
+

@(FilteredProducts.Count) sản phẩm

+
+ @if (_categories.Any()) + { + + } +
+ + +
+ +
+
+ @if (_showProductForm) + { +
+

@(_editingProductId.HasValue ? "Chỉnh sửa sản phẩm" : "Thêm sản phẩm mới")

+
+
+
+
+
+ +
+
+
+ +
+
+
+ @if (_productImagePreview != null) + { + Preview + } + +
+
+
+
+ + +
+ @if (!string.IsNullOrEmpty(_formMessage)) + { +
@_formMessage
+ } +
+
+ } + @if (!_products.Any()) + { +
+
+ +
+

Chưa có sản phẩm

+

Thêm sản phẩm để bắt đầu bán hàng

+
+ } + else if (_productView == "grid") + { +
+ @foreach (var p in PagedProducts) + { + var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" }; + var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" }; +
+
+
+ + +
+ @if (!string.IsNullOrWhiteSpace(p.ImageUrl)) + { + @p.Name + } + else + { +
+ } +
@p.Name
+
@(p.CategoryName ?? "—")
+ @typeLabel +
@p.Price.ToString("N0")₫
+
+
+ } +
+ } + else + { + @* LIST VIEW *@ +
+
+ + + + + + + + @foreach (var p in PagedProducts) + { + var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" }; + var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" }; + + + + + + + + } +
TênDanh mụcLoạiGiá
@p.Name@(p.CategoryName ?? "—")@typeLabel@p.Price.ToString("N0")₫ +
+ + +
+
+
+
+ } + @* ═══ PAGINATION ═══ *@ + @if (ProductTotalPages > 1) + { +
+ + @for (var i = 1; i <= ProductTotalPages; i++) + { + var pg = i; + + } + + Trang @_productPage / @ProductTotalPages +
+ } + @* Categories Management *@ +
+
+

Danh mục (@_categories.Count)

+ +
+ @if (_showCategoryForm) + { +
+

@(_editingCategoryId.HasValue ? "Chỉnh sửa danh mục" : "Thêm danh mục mới")

+
+
+
+
+
+ @if (_categoryImagePreview != null) { Preview } + +
+
+
+
+ + +
+ @if (_categoryFormMessage != null) {
@_categoryFormMessage
} +
+ } +
+ + + + + + @foreach (var c in _categories) + { + + + + + + + } + +
TÊNMÔ TẢTHỨ TỰHÀNH ĐỘNG
@c.Name@c.Description@c.DisplayOrder +
+ + +
+
+ @if (!_categories.Any()) {
Chưa có danh mục. Nhấn "Thêm danh mục" để tạo mới.
} +
+
+
+ +@code { + [Parameter] public Guid ShopId { get; set; } + + private Guid? _shopGuid => ShopId != Guid.Empty ? ShopId : null; + + // Product state + private List _products = new(); + private bool _showProductForm; + private Guid? _editingProductId; + private string _newProductName = ""; + private decimal _newProductPrice; + private string _newProductType = "PreparedFood"; + private string _newProductDesc = ""; + private string _newProductCategoryId = ""; + private string? _formMessage; + private bool _formSuccess; + private int _productPage = 1; + private int _productPageSize = 20; + private string _productView = "grid"; + private string _productCategoryFilter = ""; + private int ProductTotalPages => Math.Max(1, (int)Math.Ceiling((double)FilteredProducts.Count / _productPageSize)); + private List FilteredProducts => + string.IsNullOrEmpty(_productCategoryFilter) ? _products : + _products.Where(p => (p.CategoryId?.ToString() ?? "") == _productCategoryFilter).ToList(); + private List PagedProducts => + FilteredProducts.Skip((_productPage - 1) * _productPageSize).Take(_productPageSize).ToList(); + + // Category state + private List _categories = new(); + private bool _showCategoryForm; + private Guid? _editingCategoryId; + private string _newCategoryName = ""; + private string _newCategoryDesc = ""; + private int _newCategoryOrder; + private string? _categoryFormMessage; + private bool _categoryFormSuccess; + + // Image upload state + private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _productImageFile; + private string? _productImagePreview; + private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _categoryImageFile; + private string? _categoryImagePreview; + private string _newCategoryImageUrl = ""; + + protected override async Task OnInitializedAsync() + { + _products = await DataService.GetAllProductsAsync(_shopGuid); + _categories = await DataService.GetAllCategoriesAsync(_shopGuid); + } + + private async Task AddProduct() + { + _formMessage = null; + if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || ShopId == Guid.Empty) + { + _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return; + } + try + { + Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid) ? cid : null; + var imgUrl = await UploadFileIfNeeded(_productImageFile); + await DataService.CreateProductAsync(new PosDataService.CreateProductRequest( + ShopId, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl, null, catId)); + _formMessage = $"Đã thêm '{_newProductName}' thành công!"; _formSuccess = true; + _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductCategoryId = ""; _productImageFile = null; _productImagePreview = null; + _products = await DataService.GetAllProductsAsync(_shopGuid); + } + catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; } + } + + private async Task DeleteProduct(Guid productId) + { + try + { + await DataService.DeleteProductAsync(productId); + _products = await DataService.GetAllProductsAsync(_shopGuid); + } + catch (Exception ex) { _formMessage = $"Không thể xóa: {ex.Message}"; _formSuccess = false; } + } + + private void EditProduct(PosDataService.AdminProductInfo p) + { + _editingProductId = p.Id; + _newProductName = p.Name; + _newProductPrice = p.Price; + _newProductType = p.Type ?? "PreparedFood"; + _newProductDesc = p.Description ?? ""; + _newProductCategoryId = p.CategoryId?.ToString() ?? ""; + _productImageFile = null; + _productImagePreview = p.ImageUrl; + _formMessage = null; + _showProductForm = true; + } + + private async Task SaveProduct() + { + _formMessage = null; + if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || ShopId == Guid.Empty || !_editingProductId.HasValue) + { + _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return; + } + try + { + Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid2) ? cid2 : null; + var imgUrl2 = await UploadFileIfNeeded(_productImageFile); + await DataService.UpdateProductAsync(_editingProductId.Value, new PosDataService.CreateProductRequest( + ShopId, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl2, null, catId)); + _formMessage = $"Đã cập nhật '{_newProductName}' thành công!"; _formSuccess = true; + _editingProductId = null; + _products = await DataService.GetAllProductsAsync(_shopGuid); + } + catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; } + } + + private async Task SaveCategory() + { + if (string.IsNullOrWhiteSpace(_newCategoryName)) { _categoryFormMessage = "Tên danh mục không được trống."; _categoryFormSuccess = false; return; } + var catImgUrl = await UploadFileIfNeeded(_categoryImageFile) ?? _newCategoryImageUrl; + var req = new PosDataService.AdminCreateCategoryRequest(_shopGuid ?? Guid.Empty, _newCategoryName, _newCategoryDesc, _newCategoryOrder, string.IsNullOrWhiteSpace(catImgUrl) ? null : catImgUrl); + bool ok; + if (_editingCategoryId.HasValue) + ok = await DataService.UpdateCategoryAsync(_editingCategoryId.Value, req); + else + ok = await DataService.CreateCategoryAsync(req); + _categoryFormMessage = ok ? (_editingCategoryId.HasValue ? "Đã cập nhật danh mục!" : "Đã thêm danh mục!") : "Lỗi khi lưu danh mục."; + _categoryFormSuccess = ok; + if (ok) { _showCategoryForm = false; _categoryImageFile = null; _categoryImagePreview = null; _newCategoryImageUrl = ""; _categories = await DataService.GetAllCategoriesAsync(_shopGuid); } + } + + private void EditCategory(PosDataService.AdminCategoryInfo c) + { + _editingCategoryId = c.Id; _newCategoryName = c.Name ?? ""; _newCategoryDesc = c.Description ?? ""; _newCategoryOrder = c.DisplayOrder; _newCategoryImageUrl = c.ImageUrl ?? ""; _categoryImageFile = null; _categoryImagePreview = c.ImageUrl; _showCategoryForm = true; _categoryFormMessage = null; + } + + private async Task DeleteCategoryItem(Guid id) { await DataService.DeleteCategoryAsync(id); _categories = await DataService.GetAllCategoriesAsync(_shopGuid); } + + private async Task OnProductImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) + { + _productImageFile = e.File; + var format = "image/png"; + var resized = await e.File.RequestImageFileAsync(format, 200, 200); + var buffer = new byte[resized.Size]; + await resized.OpenReadStream().ReadAsync(buffer); + _productImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; + StateHasChanged(); + } + + private async Task OnCategoryImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) + { + _categoryImageFile = e.File; + var format = "image/png"; + var resized = await e.File.RequestImageFileAsync(format, 200, 200); + var buffer = new byte[resized.Size]; + await resized.OpenReadStream().ReadAsync(buffer); + _categoryImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; + StateHasChanged(); + } + + private async Task UploadFileIfNeeded(Microsoft.AspNetCore.Components.Forms.IBrowserFile? file) + { + if (file == null) return null; + using var stream = file.OpenReadStream(maxAllowedSize: 10_485_760); + return await DataService.UploadImageAsync(stream, file.Name, file.ContentType); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor index efaf0638..e6624d9a 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor @@ -6,8 +6,8 @@ @using WebClientTpos.Client.Services @* - EN: Shop-scoped page — renders different content per section with real data. - VI: Trang theo cửa hàng — hiển thị nội dung theo section với dữ liệu thật. + EN: Shop-scoped page — thin shell that routes to child components per section. + VI: Trang theo cửa hàng — shell mỏng route tới component con theo section. *@ @_sectionTitle — @(_shopName ?? "Cửa hàng") — GoodGo Admin @@ -30,18 +30,20 @@ {
- 🔔 Thông báo + Thông báo Xem tất cả
@foreach (var notif in new[] { - new { Icon = "⚠️", Title = "Sắp hết hàng", Desc = "Espresso chỉ còn 3 phần", Time = "5 phút trước", Color = "#F59E0B" }, - new { Icon = "📦", Title = "Đơn hàng mới", Desc = "Bàn 3 — Phở bò x2, Gỏi cuốn x1", Time = "12 phút trước", Color = "#3B82F6" }, - new { Icon = "✅", Title = "Ca làm hoàn thành", Desc = "Nguyễn Văn A đã kết thúc ca chiều", Time = "1 giờ trước", Color = "#22C55E" }, - new { Icon = "💰", Title = "Giao dịch ví", Desc = "Nạp thêm 500,000₫ vào ví store", Time = "3 giờ trước", Color = "#8B5CF6" } }) + new { Icon = "alert-triangle", Title = "Sắp hết hàng", Desc = "Espresso chỉ còn 3 phần", Time = "5 phút trước", Color = "#F59E0B" }, + new { Icon = "package", Title = "Đơn hàng mới", Desc = "Bàn 3 — Phở bò x2, Gỏi cuốn x1", Time = "12 phút trước", Color = "#3B82F6" }, + new { Icon = "check-circle", Title = "Ca làm hoàn thành", Desc = "Nguyễn Văn A đã kết thúc ca chiều", Time = "1 giờ trước", Color = "#22C55E" }, + new { Icon = "wallet", Title = "Giao dịch ví", Desc = "Nạp thêm 500,000₫ vào ví store", Time = "3 giờ trước", Color = "#8B5CF6" } }) {
- @notif.Icon +
+ +
@notif.Title
@notif.Desc
@@ -89,12 +91,11 @@ // ═══ OVERVIEW DASHBOARD ═══ case "overview":
-
@FormatVND(_ovOrders.Sum(o => o.TotalAmount))Doanh thu
+
@ShopHelpers.FormatVND(_ovOrders.Sum(o => o.TotalAmount))Doanh thu
@_ovOrders.CountĐơn hàng
@_ovProducts.CountSản phẩm
@_ovStaff.CountNhân viên
- @* Vertical-specific overview KPIs *@ @if (_posVertical == "restaurant" && _ovTables.Any()) {
@@ -140,7 +141,7 @@ { @o.Id.ToString()[..8] - @FormatVND(o.TotalAmount) + @ShopHelpers.FormatVND(o.TotalAmount) @o.CreatedAt.ToString("dd/MM HH:mm") } @@ -188,2405 +189,146 @@
break; - // ═══ MENU / PRODUCTS ═══ + // ═══ EXTRACTED COMPONENTS ═══ case "menu": case "products": -
-

@(FilteredProducts.Count) sản phẩm

-
- @if (_categories.Any()) - { - - } -
- - -
- -
-
- @if (_showProductForm) + + break; + + case "inventory": + + break; + + case "finance": + + break; + + case "staff": + + break; + + case "customers": + + break; + + case "tables": + + break; + + case "rooms": + + break; + + case "zones": + + break; + + case "appointments": + + break; + + case "kitchen": + + break; + + case "resources": + + break; + + case "reports": + + break; + + case "recipes": + + break; + + case "promotions": + + break; + + case "settings": + + break; + + case "schedule": + + break; + + case "shifts": + + break; + + case "drive": + + break; + + // ═══ SERVICES (reuses products, keep inline) ═══ + case "services": + @if (!_serviceProducts.Any()) { -
-

@(_editingProductId.HasValue ? "Chỉnh sửa sản phẩm" : "Thêm sản phẩm mới")

-
-
-
-
-
- -
-
-
- -
-
-
- @if (_productImagePreview != null) - { - Preview - } - -
-
-
-
- - -
- @if (!string.IsNullOrEmpty(_formMessage)) - { -
@_formMessage
- } -
-
+ @RenderEmpty("sparkles", "#EC4899", "Chưa có dịch vụ", "Thêm dịch vụ trong mục Menu/Sản phẩm", "coffee", "Quản lý sản phẩm", $"/admin/shop/{ShopId}/menu") } - @if (!_products.Any()) - { - @RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng", "plus-circle", "Thêm sản phẩm", $"/admin/shop/{ShopId}/menu") - } - else if (_productView == "grid") + else {
- @foreach (var p in PagedProducts) + @foreach (var p in _serviceProducts) { - var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" }; - var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" }; -
+
-
- - -
- @if (!string.IsNullOrWhiteSpace(p.ImageUrl)) - { - @p.Name - } - else - { -
- } +
@p.Name
-
@(p.CategoryName ?? "—")
- @typeLabel
@p.Price.ToString("N0")₫
}
} - else - { - @* LIST VIEW *@ -
-
- - - - - - - - @foreach (var p in PagedProducts) - { - var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" }; - var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" }; - - - - - - - - } -
TênDanh mụcLoạiGiá
@p.Name@(p.CategoryName ?? "—")@typeLabel@p.Price.ToString("N0")₫ -
- - -
-
-
-
- } - @* ═══ PAGINATION ═══ *@ - @if (ProductTotalPages > 1) - { -
- - @for (var i = 1; i <= ProductTotalPages; i++) - { - var pg = i; - - } - - Trang @_productPage / @ProductTotalPages -
- } - @* Categories Management *@ -
-
-

Danh mục (@_categories.Count)

- -
- @if (_showCategoryForm) - { -
-

@(_editingCategoryId.HasValue ? "Chỉnh sửa danh mục" : "Thêm danh mục mới")

-
-
-
-
-
- @if (_categoryImagePreview != null) { Preview } - -
-
-
-
- - -
- @if (_categoryFormMessage != null) {
@_categoryFormMessage
} -
- } -
- - - - - - @foreach (var c in _categories) - { - - - - - - - } - -
TÊNMÔ TẢTHỨ TỰHÀNH ĐỘNG
@c.Name@c.Description@c.DisplayOrder -
- - -
-
- @if (!_categories.Any()) {
Chưa có danh mục. Nhấn "Thêm danh mục" để tạo mới.
} -
-
break; - // ═══ INVENTORY ═══ - case "inventory": - @if (!_inventory.Any() && _invSubTab == "levels") - { - @RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm", "package", "Thêm sản phẩm trước", $"/admin/shop/{ShopId}/menu") - } - else - { - -
-
@_inventory.Count(i => i.Quantity > 10)Còn hàng
-
@_inventory.Count(i => i.Quantity > 0 && i.Quantity <= 10)Sắp hết
-
@_inventory.Count(i => i.Quantity <= 0)Hết hàng
-
- - -
- @foreach (var (label, val, icon) in new[] { ("Tồn kho", "levels", "package"), ("Nhập kho", "stock-in", "arrow-down-to-line"), ("Xuất kho", "stock-out", "arrow-up-from-line"), ("Điều chỉnh", "adjust", "settings-2"), ("Lịch sử", "transactions", "history"), ("Cảnh báo", "low-stock", "alert-triangle") }) - { - - } -
- - @if (_invFormMessage != null) - { -
- @_invFormMessage -
- } - - @switch (_invSubTab) - { - case "levels": -
-
- - - - - - - @foreach (var item in _inventory) - { - var qtyColor = item.Quantity <= 0 ? "#EF4444" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "#F59E0B" : "#22C55E"; - var bgColor = item.Quantity <= 0 ? "rgba(239,68,68,0.05)" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "rgba(245,158,11,0.05)" : "transparent"; - - - - - - - } -
Sản phẩmSố lượngMức nhập lạiThao tác
@(item.ProductName ?? item.ProductId.ToString()[..8])@item.Quantity@item.ReorderLevel -
- - -
-
-
-
- break; - - case "stock-in": -
-

Nhập kho

-
-
- - -
-
- - -
-
- - -
- -
-
- break; - - case "stock-out": -
-

Xuất kho

-
-
- - -
-
- - -
-
- - -
- -
-
- break; - - case "adjust": -
-

Điều chỉnh tồn kho

-
-
- - -
-
- - -
-
- - -
- -
-
- break; - - case "transactions": -
-

Lịch sử giao dịch kho

-
- @if (_invTxns.Any()) - { - - - - - - - @foreach (var tx in _invTxns.OrderByDescending(t => t.CreatedAt).Take(50)) - { - var txColor = tx.QuantityChange > 0 ? "#22C55E" : tx.QuantityChange < 0 ? "#EF4444" : "#6B7280"; - var txLabel = tx.TransactionType switch { "StockIn" => "Nhập kho", "StockOut" => "Xuất kho", "Adjustment" => "Điều chỉnh", "OrderDeduction" => "Đơn hàng", _ => tx.TransactionType ?? "N/A" }; - - - - - - - } -
Thời gianLoạiSố lượngLý do
@tx.CreatedAt.ToLocalTime().ToString("dd/MM HH:mm") - @txLabel - @(tx.QuantityChange > 0 ? "+" : "")@tx.QuantityChange@(tx.Reason ?? "—")
- } - else - { -
Chưa có giao dịch kho nào.
- } -
-
- break; - - case "low-stock": -
-
-

Cảnh báo tồn kho thấp

- -
-
- @if (_lowStockItems.Any()) - { - - - - - - - @foreach (var item in _lowStockItems) - { - - - - - - - } -
Sản phẩmTồn khoNgưỡngHành động
@(item.ProductName ?? item.ProductId.ToString()[..8])@item.Quantity@item.LowStockThreshold - -
- } - else - { -
-
- Tất cả sản phẩm đều đủ hàng! -
- } -
-
- break; - } - } - break; - - // ═══ FINANCE ═══ - case "finance": -
- - Đơn hàng theo cửa hàng · Ví tiền chung cho tài khoản -
- var finOrders = _financePeriod switch { - "7d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7)).ToList(), - "30d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-30)).ToList(), - _ => _orders }; -
-

Tài chính

-
- @foreach (var (label, val) in new[] { ("7 ngày", "7d"), ("30 ngày", "30d"), ("Tất cả", "all") }) - { - - } -
-
-
-
@FormatVND(finOrders.Sum(o => o.TotalAmount))Tổng doanh thu
-
@finOrders.CountĐơn hàng
-
@FormatVND(finOrders.Any() ? finOrders.Average(o => o.TotalAmount) : 0)TB/đơn
-
@FormatVND(_wallets.Sum(w => w.Balance))Số dư ví
-
- @if (_walletTxns.Any()) - { -
-

Giao dịch ví gần đây

-
- - - - - - @foreach (var t in _walletTxns.Take(15)) - { - - - - - - } -
Mô tảSố tiềnNgày
@(t.Description ?? t.ItemName ?? "—")@(t.Amount >= 0 ? "+" : "")@FormatVND(t.Amount)@t.CreatedAt.ToString("dd/MM HH:mm")
-
-
- } - @if (_orders.Any()) - { -
-

Đơn hàng gần đây

-
- - - - - - - - @foreach (var o in _orders.Take(20)) - { - var isOrderExpanded = _selectedOrderId == o.Id; - - - - - - - - @if (isOrderExpanded && _orderDetail != null) - { - - } - } -
IDSố tiềnTrạng tháiNgày
@o.Id.ToString()[..8]@FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM HH:mm")
-
-
-
@(_orderDetail.Order?.PaymentMethod ?? "—")
-
@(_orderDetail.Order?.Notes ?? "—")
-
@(_orderDetail.Order?.CreatedAt.ToString("dd/MM/yyyy HH:mm") ?? "—")
-
- @if (_orderDetail.Items?.Any() == true) - { - - - - @foreach (var item in _orderDetail.Items) - { - - - - - - - } -
Sản phẩmSLĐơn giáThành tiền
@(item.ProductName ?? "—")@item.Quantity@FormatVND(item.UnitPrice)@FormatVND(item.Subtotal)
- } -
- -
-
-
-
-
- } - else - { - @RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}") - } - break; - - // ═══ STAFF ═══ - case "staff": -
-

@(_staff.Count) nhân viên

- -
- @if (_showStaffForm) - { -
-

@(_editingStaffId.HasValue ? "Chỉnh sửa nhân viên" : "Thêm nhân viên mới")

-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
- - @if (_staffDocFrontPreview != null) - { - CCCD trước - } - -
-
- - @if (_staffDocBackPreview != null) - { - CCCD sau - } - -
-
-
- @if (!_editingStaffId.HasValue) - { -
- - @if (_createStaffAccount) - { -
-
-
- } -
- } -
- - -
- @if (!string.IsNullOrEmpty(_staffFormMessage)) - { -
@_staffFormMessage
- } -
-
- } - @if (!_staff.Any() && !_showStaffForm) - { -
-
- -
-

Chưa có nhân viên

-

Thêm nhân viên để quản lý cửa hàng

- -
- } - else if (_staff.Any()) - { -
-
@_staff.Count(s => s.Status == "Active")Đang hoạt động
-
@_staff.CountTổng nhân viên
-
-
-
- - - - - - - - - @foreach (var s in _staff) - { - var staffDisplayName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : null; - - - - - - - - - } -
Nhân viênMã NVVai tròTrạng tháiSĐTHành động
@(staffDisplayName ?? s.EmployeeCode ?? s.Id.ToString()[..6])@(s.EmployeeCode ?? "—")@(s.Role ?? "—")@(s.Status ?? "—")@(s.Phone ?? s.Email ?? "—") -
- - -
-
-
-
- } - break; - - // ═══ CUSTOMERS + MEMBERSHIP LEVELS ═══ - case "customers": -
- - Dữ liệu khách hàng chung cho tất cả cửa hàng trong thương hiệu -
-
- @foreach (var (tab, label) in new[] { ("members","Khách hàng"), ("levels","Cấp bậc"), ("exp","Điểm EXP") }) - { - - } -
- @if (_customerSubTab == "levels") - { - @* ─── Level CRUD ─── *@ -
-

@_memberLevels.Count cấp bậc

- -
- @if (_showLevelForm) - { -
-

@(_editingLevelId.HasValue ? "Sửa cấp bậc" : "Thêm cấp bậc")

-
-
-
-
-
-
-
-
- @if (_levelFormMessage != null) - { -
@_levelFormMessage
- } -
- - -
-
-
- } -
-
- - - - - - - - - - @foreach (var lvl in _memberLevels.OrderBy(l => l.Level)) - { - - - - - - - - - - } -
LevelTênEXP cầnMô tảMàuThành viên
@lvl.Level@lvl.Name@lvl.RequiredExp.ToString("N0")@(lvl.Description ?? "—")@lvl.MemberCount - - -
-
-
- } - else if (_customerSubTab == "exp") - { - @* ─── EXP Management ─── *@ -
-

Cộng điểm EXP

-
-
-
- -
-
-
- -
-
- @if (_expFormMessage != null) - { -
@_expFormMessage
- } - -
-
- @if (_memberProgress != null) - { -
-

Tiến trình: @(_memberProgress.CurrentLevelName ?? "Level") @_memberProgress.CurrentLevel

-
-
-
EXP hiện tại
@_memberProgress.CurrentExp.ToString("N0")
-
Tổng EXP
@_memberProgress.TotalExpEarned.ToString("N0")
-
Cần thêm
@_memberProgress.ExpToNextLevel.ToString("N0")
-
Level kế
@(_memberProgress.NextLevelName ?? "Max")
-
-
-
-
-
@_memberProgress.ProgressPercent%
-
-
- } - @if (_expHistory.Any()) - { -
-

Lịch sử EXP

-
- - - - - - - - @foreach (var tx in _expHistory) - { - - - - - - - - } -
NgàyĐiểmNguồnTham chiếuLevel
@tx.CreatedAt.ToString("dd/MM/yyyy HH:mm")@(tx.Points > 0 ? "+" : "")@tx.Points@(tx.Source ?? "—")@(tx.ReferenceId ?? "—")@tx.LevelAtTime
-
-
- } - } - else - { - @* ─── Members list (existing) ─── *@ -
-

@_members.Count khách hàng

-
-
- - -
- -
-
- @if (_showMemberForm) - { -
-

@(_editingMemberId.HasValue ? "Sửa khách hàng" : "Thêm khách hàng")

-
-
-
-
-
- -
-
-
- @if (_memberFormMessage != null) - { -
@_memberFormMessage
- } -
- - -
-
-
- } - var filteredMembers = string.IsNullOrWhiteSpace(_customerSearch) - ? _members - : _members.Where(m => m.Id.ToString().Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) - || (m.DisplayName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) - || (m.Phone ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase) - || (m.LevelName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)).ToList(); - @if (!filteredMembers.Any()) - { - @RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}") - } - else - { -
-
@filteredMembers.CountTổng khách hàng
-
@_memberLevels.CountCấp bậc
-
@(filteredMembers.Any() ? filteredMembers.Max(m => m.TotalExpEarned).ToString("N0") : "0")EXP cao nhất
-
- @if (_memberLevels.Any()) - { -
-

Cấp bậc thành viên

-
- - - - - - - @foreach (var lvl in _memberLevels.OrderBy(l => l.Level)) - { - - - - - - - } -
LevelTênEXP cầnThành viên
@lvl.Level@lvl.Name@lvl.MinExp.ToString("N0") — @lvl.MaxExp.ToString("N0")@lvl.MemberCount
-
-
- } -
-

Danh sách khách hàng

-
- - - - - - - - - @foreach (var m in filteredMembers) - { - var isExpanded = _selectedCustomerId == m.Id; - - - - - - - - - @if (isExpanded) - { - - } - } -
TênSĐTCấp bậcEXPNgày tham gia
@(m.DisplayName ?? m.Id.ToString()[..8])@(m.Phone ?? "—")@(m.LevelName ?? "—")@m.TotalExpEarned.ToString("N0")@m.CreatedAt.ToString("dd/MM/yyyy")
-
-
-
- -
@m.Id.ToString()
-
-
- -
@(m.LevelName ?? "Chưa xếp hạng") • @m.TotalExpEarned.ToString("N0") EXP
-
-
- -
@m.CreatedAt.ToString("dd/MM/yyyy HH:mm")
-
-
-
- - - - - -
-
-
-
-
- } - } - break; - - // ═══ TABLES (Restaurant) ═══ - case "tables": -
-
- Trống: @_tables.Count(t => t.Status == "available") - Đang dùng: @_tables.Count(t => t.Status == "occupied") - Đã đặt: @_tables.Count(t => t.Status == "reserved") -
- -
- @if (_showTableForm) - { -
-

@(_editingTableId.HasValue ? "Chỉnh sửa bàn" : "Thêm bàn mới")

-
-
-
-
-
-
-
- - -
- @if (_tableFormMessage != null) {
@_tableFormMessage
} -
-
- } - @if (!_tables.Any()) - { - @RenderEmpty("grid-3x3", "#F59E0B", "Chưa có bàn nào", "Thêm bàn để quản lý sơ đồ phục vụ") - } - else - { -
- @foreach (var table in _tables) - { - var bgColor = table.Status switch { "available" => "rgba(34,197,94,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" }; - var borderColor = table.Status switch { "available" => "rgba(34,197,94,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" }; - var statusColor = table.Status switch { "available" => "#22C55E", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" }; - var statusText = table.Status switch { "available" => "Trống", "occupied" => "Đang dùng", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => table.Status }; -
-
- - -
-
@table.TableNumber
-
@(table.Zone ?? "Chung") • @table.Capacity chỗ
-
- - @statusText -
- @if (table.SessionId.HasValue) - { -
- @table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—") -
- } -
- } -
- } - break; - - // ═══ ROOMS (Karaoke — reuse tables structure) ═══ - case "rooms": - @if (!_tables.Any()) - { - @RenderEmpty("door-open", "#8B5CF6", "Chưa có phòng nào", "Thêm phòng để quản lý Karaoke", "plus-circle", "Thêm phòng") - } - else - { -
-
- Trống: @_tables.Count(t => t.Status == "available") - Đang hát: @_tables.Count(t => t.Status == "occupied") - Đã đặt: @_tables.Count(t => t.Status == "reserved") -
-
@_tables.Count phòng
-
-
- @foreach (var room in _tables) - { - var bgColor = room.Status switch { "available" => "rgba(139,92,246,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" }; - var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" }; - var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" }; - var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status }; - var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) }; -
-
-
- - Phòng @room.TableNumber -
- @roomType.Item1 -
-
@(room.Zone ?? "Chung") • @room.Capacity chỗ
-
@FormatVND(roomType.Item3)/giờ
-
- - @statusText -
- @if (room.SessionId.HasValue) - { - var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow); - var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours)); - var bill = hours * roomType.Item3; -
-
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
-
- @hours giờ - @FormatVND(bill) -
-
- } -
- } -
- } - break; - - // ═══ APPOINTMENTS (Spa / Thẩm mỹ) — Calendar View + CRUD ═══ - case "appointments": -
- -
- @if (_showApptForm) - { -
-

Thêm lịch hẹn mới

-
-
-
-
-
-
- - -
- @if (_apptFormMessage != null) {
@_apptFormMessage
} -
-
- } - var calWeekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + 1 + _calendarWeekOffset * 7); - var calWeekEnd = calWeekStart.AddDays(7); - var weekAppts = _appointments.Where(a => a.StartTime >= calWeekStart && a.StartTime < calWeekEnd).ToList(); -
-
-
@_appointments.CountTổng lịch hẹn
-
@_appointments.Count(a => a.Status == "Confirmed")Đã xác nhận
-
@_appointments.Count(a => a.Status == "Pending")Chờ xác nhận
-
@weekAppts.CountTuần này
-
-
-
-
-

Lịch hẹn tuần @calWeekStart.ToString("dd/MM") — @calWeekEnd.AddDays(-1).ToString("dd/MM")

-
- - - -
-
-
-
- @for (int d = 0; d < 7; d++) - { - var day = calWeekStart.AddDays(d); - var dayAppts = weekAppts.Where(a => a.StartTime.Date == day.Date).OrderBy(a => a.StartTime).ToList(); - var isToday = day.Date == DateTime.Today; -
-
-
@DayLabel((int)day.DayOfWeek)
-
@day.Day
-
- @foreach (var appt in dayAppts) - { - var apptColor = appt.Status switch { "Confirmed" => "#22C55E", "Pending" => "#F59E0B", "Completed" => "#3B82F6", _ => "#6B6B6F" }; -
-
@appt.StartTime.ToString("HH:mm")-@appt.EndTime.ToString("HH:mm")
-
@(appt.ResourceName ?? "—")
- @if (appt.Status != "Cancelled" && appt.Status != "Completed") - { - - } -
- } - @if (!dayAppts.Any()) - { -
- } -
- } -
-
-
- break; - - // ═══ SERVICES (Spa — products with type=Service) ═══ - case "services": - @if (!_products.Any(p => p.Type == "Service")) - { - @RenderEmpty("sparkles", "#EC4899", "Chưa có dịch vụ", "Thêm dịch vụ để khách có thể đặt lịch", "plus-circle", "Thêm dịch vụ", $"/admin/shop/{ShopId}/menu") - } - else - { -
-
- - - - - - @foreach (var s in _products.Where(p => p.Type == "Service")) - { - - - - - - } -
Dịch vụGiáTrạng thái
@s.Name
@(s.Description ?? "—")
@FormatVND(s.Price)@(s.IsActive ? "Hoạt động" : "Tạm ngưng")
-
-
- } - break; - - // ═══ POS — Redirect prompt ═══ + // ═══ POS redirect ═══ case "pos":
-
-
- -
-

POS Bán hàng

-

Mở giao diện bán hàng tại điểm để tạo đơn, thanh toán và in hóa đơn.

- - - Mở POS +
break; - // ═══ REPORTS — Enhanced with top products ═══ - case "reports": -
-
@FormatVND(_reportOrders.Sum(o => o.TotalAmount))Tổng doanh thu
-
@_reportOrders.CountTổng đơn hàng
-
@FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0)Giá trị TB / đơn
-
@_reportProducts.CountSản phẩm
-
- @* Revenue Report *@ -
-
-

Doanh thu theo kỳ

-
- @foreach (var (label, val) in new[] { ("Ngày", "daily"), ("Tuần", "weekly"), ("Tháng", "monthly") }) - { - - } -
-
-
- @if (_revenueReport.Any()) - { - - - - - - @foreach (var r in _revenueReport) - { - - - - - - } - -
KỲĐƠN HÀNGDOANH THU
@r.Period.ToString("dd/MM/yyyy")@r.OrderCount@FormatVND(r.Revenue)
- } - else - { -
Nhấn Ngày / Tuần / Tháng để tải dữ liệu doanh thu.
- } -
-
- @* ─── Top products from real order_items data ─── *@ - @if (_topProducts.Any()) - { -
-

Top sản phẩm bán chạy

-
- - - - - - - @{ var tpRank = 1; } - @foreach (var tp in _topProducts) - { - - - - - - - } -
#Tên SPĐã bánDoanh thu
@(tpRank++)@(tp.ProductName ?? "—")@tp.TotalSold@FormatVND(tp.TotalRevenue)
-
-
- } - @if (_reportOrders.Any()) - { -
-

Đơn hàng gần nhất

-
- - - - - - - @foreach (var o in _reportOrders.Take(20)) - { - - - - - - - } -
Mã đơnGiá trịTrạng tháiNgày tạo
@o.Id.ToString()[..8]@FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM/yyyy HH:mm")
-
-
- } - else - { - @RenderEmpty("bar-chart-2", "#22C55E", "Chưa có dữ liệu báo cáo", "Dữ liệu sẽ hiển thị khi có đơn hàng và hoạt động kinh doanh") - } - break; - - // ═══ KITCHEN — KDS with real ticket data ═══ - case "kitchen": -
-
- @foreach (var st in new[] { ("all", "", "Tất cả"), ("pending", "clock", "Chờ"), ("preparing", "flame", "Đang làm"), ("completed", "check-circle", "Xong") }) - { - - } -
-
- Chờ: @_kitchenTickets.Count(t => t.Status == "pending") - Đang làm: @_kitchenTickets.Count(t => t.Status == "preparing") - Xong: @_kitchenTickets.Count(t => t.Status == "completed") -
-
- @if (!_kitchenTickets.Any()) - { - @RenderEmpty("flame", "#F59E0B", "Không có ticket bếp", "Ticket sẽ xuất hiện khi có đơn từ POS") - } - else - { -
- @foreach (var ticket in _kitchenTickets) - { - var ticketColor = ticket.Status switch { "pending" => "#F59E0B", "preparing" => "#3B82F6", "completed" => "#22C55E", _ => "#6B6B6F" }; - var ticketLabel = ticket.Status switch { "pending" => "Chờ", "preparing" => "Đang làm", "completed" => "Hoàn thành", _ => ticket.Status }; - var elapsed = (DateTime.UtcNow - ticket.CreatedAt).TotalMinutes; -
-
- P@(ticket.Priority) - @ticketLabel -
-
@ticket.ItemName
-
@(ticket.Station ?? "Bếp chính")
-
- @((int)elapsed) phút - @if (ticket.Status != "completed") - { - - } -
-
- } -
- } - break; - - // ═══ RESOURCES (Spa/Beauty — phòng, giường, thiết bị) ═══ - case "resources": -
-
-
@_resources.CountTổng
-
@_resources.Count(r => r.IsActive)Hoạt động
-
- -
- @if (_showResourceForm) - { -
-

@(_editingResourceId.HasValue ? "Chỉnh sửa" : "Thêm tài nguyên")

-
-
-
-
- -
-
-
-
- - -
- @if (_resourceFormMessage != null) {
@_resourceFormMessage
} -
-
- } - @if (!_resources.Any()) - { - @RenderEmpty("door-open", "#EC4899", "Chưa có tài nguyên", "Thêm phòng, giường, thiết bị cho cửa hàng") - } - else - { -
-
- - - - - - - - @foreach (var r in _resources) - { - - - - - - - - } -
TênLoạiSức chứaTrạng thái
@r.Name@(r.ResourceType ?? "—")@r.Capacity@(r.IsActive ? "Active" : "Inactive") -
- - -
-
-
-
- } - break; - - // ═══ TREATMENTS (Beauty — Liệu trình) ═══ + // ═══ STUB SECTIONS ═══ case "treatments": -
- @foreach (var tab in new[] { ("treatment", "📋 Liệu trình"), ("medical", "🏥 Hồ sơ y tế"), ("photos", "📷 Ảnh Before/After") }) - { - - } -
- @if (_treatmentTab == "treatment") - { -
-
-

Liệu trình điều trị

-
-
-
- -
-

Quản lý liệu trình dài hạn

-

Theo dõi tiến trình điều trị nhiều buổi, lịch tái khám sau phẫu thuật.

-
-
- } - else if (_treatmentTab == "medical") - { -
-

🏥 Hồ sơ y tế khách hàng

-
-
-
-
Chưa chọn khách
-
-
-
-
-
-
-
-
- } - else - { -
-

📷 Ảnh Before / After

-
-
-
-
-
-
- @foreach (var (label, icon) in new[] { ("Before", "image"), ("After", "image-plus") }) - { -
-
-
Upload ảnh @label
-
Kéo thả hoặc click để chọn ảnh
-
JPG, PNG, HEIC • Max 10MB
-
- } -
-
-

📅 Lịch sử ảnh

-
- @foreach (var (date, desc, color) in new[] { ("15/02", "Buổi 1 — Before", "#3B82F6"), ("22/02", "Buổi 2 — After", "#22C55E"), ("01/03", "Buổi 3 — After", "#8B5CF6") }) - { -
-
-
-
@desc
-
@date/2026
-
-
- } -
-
-
-
- } + @RenderStubSection("clipboard-list", "#A855F7", "Liệu trình", "Theo dõi liệu trình điều trị — tính năng đang phát triển.") break; - // ═══ STAFF SCHEDULE (Spa/Beauty — Lịch làm việc) ═══ - case "schedule": -
-
-
@_staffSchedules.Select(s => s.StaffId).Distinct().Count()NV có lịch
-
@_staffSchedules.CountCa làm việc
-
- -
- @if (_showScheduleForm) - { -
-

Thêm lịch làm việc

-
-
-
- -
-
- -
-
-
-
-
- - -
- @if (_schedFormMessage != null) {
@_schedFormMessage
} -
-
- } - @if (!_staffSchedules.Any()) - { - @RenderEmpty("calendar-clock", "#8B5CF6", "Chưa có lịch làm việc", "Thiết lập lịch ca cho nhân viên") - } - else - { -
-

Lịch làm việc theo tuần

-
- - - - - - - - - @foreach (var s in _staffSchedules.OrderBy(x => x.DayOfWeek).ThenBy(x => x.StartTime)) - { - var schedStaff = _staff.FirstOrDefault(st => st.Id == s.StaffId); - var schedStaffName = schedStaff != null && (!string.IsNullOrWhiteSpace(schedStaff.LastName) || !string.IsNullOrWhiteSpace(schedStaff.FirstName)) ? $"{schedStaff.LastName} {schedStaff.FirstName}".Trim() : (s.EmployeeCode ?? s.StaffId.ToString()[..8]); - - - - - - - - - } -
Nhân viênVai tròThứBắt đầuKết thúc
@schedStaffName@(s.Role ?? "—")@DayLabel(s.DayOfWeek)@s.StartTime@s.EndTime
-
-
- } - break; - - // ═══ RECIPES (Cafe/Restaurant — Công thức & Nguyên liệu) ═══ - case "recipes": -
-

@_recipes.Count công thức

- -
- @if (_showRecipeForm) - { -
-

@(_editingRecipeId.HasValue ? "Chỉnh sửa công thức" : "Thêm công thức mới")

-
-
-
-
-
-
-
-
Nguyên liệu
- @for (var idx = 0; idx < _recipeIngredients.Count; idx++) - { - var i = idx; -
- - - - - -
- } -
-
- - -
- @if (_recipeFormMessage != null) {
@_recipeFormMessage
} -
-
- } - @if (!_recipes.Any()) - { - @RenderEmpty("flask-conical", "#FF5C00", "Chưa có công thức", "Thêm công thức và nguyên liệu pha chế") - } - else - { -
- @foreach (var recipe in _recipes) - { - var isExpanded = _expandedRecipeId == recipe.Id; -
-
-
-
@recipe.Name
-
- -
-
-
@recipe.PrepTimeMinutes phút chuẩn bị
- @if (isExpanded && !string.IsNullOrEmpty(recipe.Instructions)) - { -
@recipe.Instructions
- } -
-
- } -
- } - break; - - // ═══ PROMOTIONS / CAMPAIGNS (CRUD) ═══ - case "promotions": -
- - Chiến dịch khuyến mãi chung cho tất cả cửa hàng trong thương hiệu -
- @* ─── Sub-tabs: Campaigns | Vouchers ─── *@ -
- @{ var promoTabs = new[] { ("campaigns", "Chiến dịch", "tag"), ("vouchers", "Mã voucher", "ticket") }; } - @foreach (var (tab, label, icon) in promoTabs) - { - var t = tab; - var isActive = _promoSubTab == t; - - } -
- @if (_promoSubTab == "campaigns") - { -
-

@_campaigns.Count chiến dịch

- -
- @if (_showCampaignForm) - { -
-

@(_editingCampaignId.HasValue ? "Sửa chiến dịch" : "Thêm chiến dịch")

-
-
-
-
-
- -
-
-
-
-
-
- @if (_campaignFormMessage != null) - { -
@_campaignFormMessage
- } -
- - -
-
-
- } - @if (!_campaigns.Any()) - { - @RenderEmpty("tag", "#22C55E", "Chưa có chiến dịch", "Tạo chiến dịch voucher, khuyến mãi cho khách hàng", "plus-circle", "Thêm chiến dịch") - } - else - { -
-
@_campaigns.CountTổng chiến dịch
-
@_campaigns.Count(c => c.Status == "Active")Đang hoạt động
-
@_campaigns.Sum(c => c.TotalVouchers)Tổng voucher
-
@_campaigns.Sum(c => c.IssuedVouchers)Đã phát
-
-
-

Danh sách chiến dịch

-
- - - - - - - - - - @foreach (var c in _campaigns) - { - - - - - - - - - - } -
TênLoạiGiá trịĐã phát/TổngBắt đầuKết thúc
@c.Name@(c.Description?.Contains("%") == true ? "%" : "₫")@(c.Description?.Contains("%") == true ? $"{c.FaceValue}%" : FormatVND(c.FaceValue))@c.IssuedVouchers / @c.TotalVouchers@(c.StartDate?.ToString("dd/MM/yy") ?? "—")@(c.EndDate?.ToString("dd/MM/yy") ?? "—") -
- - -
-
-
-
- } @* end else *@ - } @* end campaigns sub-tab *@ - @if (_promoSubTab == "vouchers") - { -
-

@_vouchers.Count mã voucher

- -
- @if (!_vouchers.Any()) - { - @RenderEmpty("ticket", "#EC4899", "Chưa có voucher", "Tạo chiến dịch để tự động sinh mã voucher", "tag", "Tạo chiến dịch") - } - else - { -
-

Danh sách mã voucher

-
- - - - - - - - - - @foreach (var v in _vouchers) - { - - - - - - - - - - } -
Chiến dịchMệnh giáCòn lạiTrạng tháiNgày tạo
@v.Code@(v.CampaignName ?? "—")@FormatVND(v.FaceValue)@FormatVND(v.RemainingValue)@GetVoucherStatusLabel(v.Status)@(v.CreatedAt?.ToLocalTime().ToString("dd/MM/yy") ?? "—") - @if (v.Status?.ToLower() == "available") - { - - } -
-
-
- } - } - break; - - case "drive": -
-
-
-
- @if (_showFolderForm) - { -
- } - @if (_currentFolderId.HasValue) - { -
- } - @if (_storageFolders.Any()) - { -
- @foreach (var folder in _storageFolders) - { -
@folder.Name
@folder.CreatedAt.ToString("dd/MM/yyyy")
- } -
- } -

Tệp (@_storageFiles.Count)

- @if (!_storageFiles.Any()) - { -
Chưa có tệp nào.
- } - else - { - - @foreach (var f in _storageFiles) - { - - } -
Tên tệpLoạiKích thướcNgày uploadThao tác
@f.FileName@(f.ContentType ?? "—")@FormatFileSize(f.FileSizeBytes)@f.UploadedAt.ToString("dd/MM/yyyy HH:mm")
- } -
- break; - - // ═══ SETTINGS ═══ - case "settings": - @* ─── Shop info (read-only) ─── *@ -
-

Thông tin cửa hàng

-
-
-
@(_shopName ?? "—")
-
@_verticalLabel
-
-
-
- @* ─── Opening hours + business days ─── *@ -
-

Giờ & ngày hoạt động

-
-
-
- - -
-
- - -
-
-
- -
- @foreach (var (day, code) in new[] { ("T2","Monday"),("T3","Tuesday"),("T4","Wednesday"),("T5","Thursday"),("T6","Friday"),("T7","Saturday"),("CN","Sunday") }) - { - var isOn = _settingsOpenDays.Contains(code); - - } -
-
-
-
- @* ─── Features config toggles ─── *@ -
-

Tính năng cửa hàng

-
-
- @{ void RenderToggle(string label, bool isOn, Action setter) { -
-
-
-
- @label -
; - } } - @{ RenderToggle("Quản lý tồn kho", _featHasInventory, v => _featHasInventory = v); } - @{ RenderToggle("Đặt lịch hẹn", _featHasBooking, v => _featHasBooking = v); } - @{ RenderToggle("Quản lý bàn", _featHasTables, v => _featHasTables = v); } - @{ RenderToggle("Hiển thị bếp", _featHasKitchen, v => _featHasKitchen = v); } - @{ RenderToggle("Vận chuyển", _featHasShipping, v => _featHasShipping = v); } - @{ RenderToggle("Giao hàng", _featHasDelivery, v => _featHasDelivery = v); } -
-
-
- @* ─── Save button ─── *@ -
- - @if (_settingsMessage != null) - { - @_settingsMessage - } -
- break; - - // ═══ R3: RESERVATIONS / ĐẶT BÀN (Nhà hàng) ═══ case "reservations": -
-
5Hôm nay
-
3Đã xác nhận
-
2Chờ duyệt
-
-
-
-

📅 Danh sách đặt bàn

- -
-
- - - - - - - - - @foreach (var (guest, ppl, time, table, status, note, color) in new[] { - ("Nguyễn Văn A", "4", "18:00", "Bàn 5", "Xác nhận", "Sinh nhật", "#22C55E"), - ("Trần Thị B", "2", "19:00", "Bàn 2", "Xác nhận", "", "#22C55E"), - ("Lê Minh C", "8", "19:30", "VIP 1", "Chờ duyệt", "Tiệc công ty", "#F59E0B"), - ("Phạm Dương D", "6", "20:00", "Bàn 7", "Xác nhận", "Kỷ niệm", "#22C55E"), - ("Hoàng E", "3", "20:30", "Bàn 3", "Chờ duyệt", "", "#F59E0B") }) - { - - - - - - - - - } -
KháchSố ngườiThời gianBànTrạng tháiGhi chú
@guest@ppl@time@table@status@note
-
-
+ @RenderStubSection("calendar-check", "#3B82F6", "Đặt bàn", "Quản lý đặt bàn trước — tính năng đang phát triển.") break; - // ═══ K3: HAPPY HOUR (Karaoke) ═══ case "happy-hour": -
-

Cấu hình Happy Hour

-
-
-
-
-
-
-
-
-
-
-
- @foreach (var day in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" }) - { - var isActive = day == "T2" || day == "T3" || day == "T4" || day == "T5"; - - } -
-
- -
-
-
-

🎉 Combo Happy Hour

-
-
- @foreach (var (combo, items, oldPrice, newPrice) in new[] { - ("Combo Đôi", "2 giờ phòng + 2 bia", "350,000₫", "245,000₫"), - ("Combo Nhóm", "3 giờ phòng + 5 đồ uống", "750,000₫", "525,000₫"), - ("Combo VIP", "2 giờ VIP + trái cây + 4 bia", "980,000₫", "686,000₫") }) - { -
-
@combo
-
@items
-
- @oldPrice - @newPrice -
-
- } -
-
-
+ @RenderStubSection("clock", "#F59E0B", "Happy Hour", "Cấu hình khung giờ giảm giá — tính năng đang phát triển.") break; - // ═══ S4: SERVICE PACKAGES / GÓI DỊCH VỤ (Spa) ═══ case "packages": -
-
-

🎁 Gói dịch vụ

- -
-
-
- @foreach (var (pkg, sessions, services, price, savings) in new[] { - ("Gói Thư Giãn", "5 buổi", "Massage body + Xông hơi", "2,500,000₫", "Tiết kiệm 500,000₫"), - ("Gói VIP", "10 buổi", "Massage + Facial + Xông hơi + Chăm sóc da", "6,800,000₫", "Tiết kiệm 2,200,000₫"), - ("Gói Cặp Đôi", "4 buổi", "2 người — Massage + Xông hơi", "3,600,000₫", "Tiết kiệm 800,000₫") }) - { -
-
-
@pkg
-
@sessions
-
-
-
@services
-
- @price - @savings -
-
-
- } -
-
-
+ @RenderStubSection("gift", "#8B5CF6", "Gói dịch vụ", "Quản lý gói combo dịch vụ — tính năng đang phát triển.") break; - // ═══ B5: CONSENT FORM / CAM KẾT KH (Thẩm mỹ) ═══ case "consent": -
-
-

📝 Mẫu cam kết khách hàng

- -
-
-
- @foreach (var (title, desc, icon, fields) in new[] { - ("Cam kết Phẫu thuật", "Biểu mẫu đồng ý trước phẫu thuật thẩm mỹ", "syringe", "12 trường"), - ("Cam kết Tiêm Filler", "Xác nhận rủi ro và đồng ý tiêm filler", "droplets", "8 trường"), - ("Cam kết Laser", "Biểu mẫu đồng ý điều trị laser da", "zap", "10 trường"), - ("Cam kết Chung", "Mẫu cam kết dịch vụ thẩm mỹ tổng quát", "file-text", "6 trường") }) - { -
-
-
-
@title
@fields
-
-
@desc
-
- - -
-
- } -
-
-
+ @RenderStubSection("file-check", "#EC4899", "Cam kết KH", "Biểu mẫu đồng ý khách hàng — tính năng đang phát triển.") break; - // ═══ B6: DOCTORS / BÁC SĨ (Thẩm mỹ) ═══ case "doctors": -
-
-

🩺 Bác sĩ & Chuyên gia

- -
-
-
- @foreach (var (name, spec, cert, exp, color) in new[] { - ("BS. Nguyễn Văn A", "Phẫu thuật thẩm mỹ", "Chứng chỉ BVTM", "15 năm", "#3B82F6"), - ("BS. Trần Thị B", "Da liễu", "Thạc sĩ Y khoa", "10 năm", "#8B5CF6"), - ("BS. Lê Minh C", "Nội tiết", "Ph.D. Nội tiết học", "12 năm", "#EC4899"), - ("KTV. Phạm D", "Chăm sóc da", "Chứng chỉ Quốc tế CIDESCO", "8 năm", "#22C55E") }) - { -
-
-
@name[4]
-
@name
@spec
-
-
- @cert - @exp -
-
- - -
-
- } -
-
-
+ @RenderStubSection("stethoscope", "#3B82F6", "Bác sĩ / CK", "Quản lý bác sĩ và chuyên gia — tính năng đang phát triển.") break; - // ═══ B7: FOLLOW-UP / TÁI KHÁM (Thẩm mỹ) ═══ case "followup": -
-
8Tái khám tháng này
-
2Sắp đến hạn
-
6Đã hoàn thành
-
-
-

📅 Lịch tái khám

-
- - - - - - - - @foreach (var (patient, service, date, doctor, status, color) in new[] { - ("Nguyễn Thị Hương", "Nâng mũi sụn", "05/03/2026", "BS. Nguyễn A", "Sắp đến", "#F59E0B"), - ("Trần Văn Nam", "Cấy mỡ tự thân", "07/03/2026", "BS. Trần B", "Sắp đến", "#F59E0B"), - ("Lê Thu Trang", "Trị nám laser", "01/03/2026", "BS. Lê C", "Hôm nay", "#EF4444"), - ("Phạm Minh Tuấn", "Filler môi", "28/02/2026", "BS. Nguyễn A", "Hoàn thành", "#22C55E"), - ("Hoàng Lan", "Botox trán", "25/02/2026", "BS. Trần B", "Hoàn thành", "#22C55E") }) - { - - - - - - - - } -
Khách hàngDịch vụ đã làmNgày tái khámBác sĩTrạng thái
@patient@service@date@doctor@status
-
-
+ @RenderStubSection("calendar-heart", "#EC4899", "Tái khám", "Lịch tái khám sau điều trị — tính năng đang phát triển.") break; - // ═══ R4: ZONES / KHU VỰC (Nhà hàng) ═══ - case "zones": - var tableZones = _tables.GroupBy(t => t.Zone ?? "Chung").Select(g => new { Name = g.Key, Count = g.Count() }).ToList(); - var allZones = tableZones.Select(z => z.Name).Union(_customZones).Distinct().OrderBy(z => z).ToList(); - var zoneGroups = allZones.Select((z, i) => new { Name = z, Count = tableZones.FirstOrDefault(tz => tz.Name == z)?.Count ?? 0, Color = _zoneColors[i % _zoneColors.Length], Icon = _zoneIcons[i % _zoneIcons.Length] }).ToList(); -
-
-

Quản lý khu vực

- -
-
- @if (_showZoneForm) - { -
-
@(_editingZoneOriginalName != null ? "Đổi tên khu vực" : "Thêm khu vực mới")
-
- - - -
- @if (_zoneFormMessage != null) {
@_zoneFormMessage
} -
- } - @if (!zoneGroups.Any()) - { - @RenderEmpty("map-pin", "#F59E0B", "Chưa có khu vực nào", "Nhấn 'Thêm khu vực' ở trên để tạo khu vực đầu tiên", "", "", "") - } - else - { -
- @foreach (var zone in zoneGroups) - { - var rgbVal = zone.Color switch { "#3B82F6" => "59,130,246", "#A855F7" => "168,85,247", "#22C55E" => "34,197,94", "#F59E0B" => "245,158,11", "#EC4899" => "236,72,153", _ => "99,102,241" }; -
-
-
- -
-
-
@zone.Name
-
@zone.Count bàn
-
-
-
- Đang hoạt động -
-
- -
-
- } -
- } -
-
- break; - - // ═══ S6: COMBO DỊCH VỤ (Spa) ═══ case "combos": -
-
-

Combo dịch vụ

- -
-
-
- @foreach (var (name, services, duration, price, orig, sold, color) in new[] { - ("Combo Thư Giãn Toàn Thân", "Massage body 60' + Xông hơi 30' + Đắp mặt nạ 20'", "110 phút", "800,000₫", "1,050,000₫", 45, "#3B82F6"), - ("Combo Detox & Làm Đẹp", "Tẩy tế bào chết + Massage mặt + Chăm sóc da 5 bước", "90 phút", "650,000₫", "850,000₫", 32, "#A855F7"), - ("Combo Cặp Đôi Premium", "2 Massage body 90' + 2 Xông hơi + Trà thảo mộc", "120 phút", "1,500,000₫", "2,100,000₫", 18, "#EC4899") }) - { -
-
-

@name

- Đang bán -
-

@services

-
- - @duration -
-
-
- @price - @orig -
- Đã bán: @sold -
-
- } -
-
-
- break; - - // ═══ C5: CA LÀM VIỆC / SHIFTS (Café) ═══ - case "shifts": -
-
@_staffSchedules.CountCa đã phân
-
@_staff.CountNhân viên
-
@_staffSchedules.Select(s => s.DayOfWeek).Distinct().Count()Ngày có ca
-
- @* ─── Add Schedule Form ─── *@ -
-
-

Phân ca làm việc

- -
- @if (_showScheduleForm) - { -
-
-
- -
-
- -
-
-
-
-
- - -
- @if (_schedFormMessage != null) {
@_schedFormMessage
} -
- } -
- @* ─── Weekly Grid ─── *@ -
-

Lịch ca — Tuần

-
- @if (!_staff.Any()) - { -
Chưa có nhân viên. Thêm nhân viên trong mục Nhân sự.
- } - else - { - - - @foreach (var d in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" }) - { - - } - - @foreach (var emp in _staff) - { - - - @foreach (var dow in new[] { 1, 2, 3, 4, 5, 6, 0 }) - { - var sched = _staffSchedules.FirstOrDefault(s => s.StaffId == emp.Id && s.DayOfWeek == dow); - if (sched != null) - { - var isMorning = sched.StartTime?.CompareTo("12:00") < 0; - var bg = isMorning ? "rgba(59,130,246,0.12)" : "rgba(168,85,247,0.12)"; - var fg = isMorning ? "#3B82F6" : "#A855F7"; - var label = $"{sched.StartTime?[..5]}"; - - } - else - { - - } - } - - } -
Nhân viên@d
@(emp.EmployeeCode ?? emp.Id.ToString()[..8]) - @label - - - -
- } -
-
-
-
Sáng (<12:00)
-
Chiều (≥12:00)
-
— = Nghỉ
-
+ @RenderStubSection("layers", "#A855F7", "Combo dịch vụ", "Quản lý combo dịch vụ — tính năng đang phát triển.") break; // ═══ UNKNOWN SECTIONS ═══ @@ -2631,119 +373,19 @@ private string? _errorMessage; private string _posVertical = "cafe"; private Guid? _shopGuid; + private bool _showNotifications; + private string? _toastMessage; - // ═══ DATA ═══ - private List _products = new(); - private List _inventory = new(); - private List _orders = new(); - private List _staff = new(); - private List _members = new(); - private List _tables = new(); - private List _appointments = new(); - // Reports data (separate from section-specific _orders) - private List _reportOrders = new(); - private List _reportProducts = new(); // Overview data private List _ovOrders = new(); private List _ovProducts = new(); private List _ovStaff = new(); private List _ovTables = new(); private List _ovAppts = new(); - // Product form state - private bool _showProductForm; - private Guid? _editingProductId; - private string _newProductName = ""; - private decimal _newProductPrice; - private string _newProductType = "PreparedFood"; - private string _newProductDesc = ""; - private string _newProductCategoryId = ""; - private string? _formMessage; - private bool _formSuccess; - // Product pagination + view state - private int _productPage = 1; - private int _productPageSize = 20; - private string _productView = "grid"; // grid | list - private string _productCategoryFilter = ""; - private int ProductTotalPages => Math.Max(1, (int)Math.Ceiling((double)FilteredProducts.Count / _productPageSize)); - private List FilteredProducts => - string.IsNullOrEmpty(_productCategoryFilter) ? _products : - _products.Where(p => (p.CategoryId?.ToString() ?? "") == _productCategoryFilter).ToList(); - private List PagedProducts => - FilteredProducts.Skip((_productPage - 1) * _productPageSize).Take(_productPageSize).ToList(); - // Staff form state - private bool _showStaffForm; - private Guid? _editingStaffId; - private string _newStaffCode = ""; - private string _newStaffRole = "Cashier"; - private string _newStaffPhone = ""; - private string _newStaffEmail = ""; - private string? _staffFormMessage; - private bool _staffFormSuccess; - private bool _createStaffAccount; - private string _newStaffFirstName = ""; - private string _newStaffLastName = ""; - private string _newStaffPassword = ""; - private Guid? _merchantId; - // New data: wallets, promotions, campaigns, member levels, schedules, inv txns - private List _wallets = new(); - private List _walletTxns = new(); - private List _promotions = new(); - private List _campaigns = new(); - private List _memberLevels = new(); - // Campaign form state - private bool _showCampaignForm; - private Guid? _editingCampaignId; - private string _newCampaignName = ""; - private string _newCampaignDesc = ""; - private decimal _newCampaignValue; - private int _newCampaignVouchers; - private DateTime _newCampaignStart = DateTime.Today; - private DateTime _newCampaignEnd = DateTime.Today.AddMonths(1); - private string _newCampaignDiscountType = "fixed"; - private string? _campaignFormMessage; - private bool _campaignFormSuccess; - // Member form state - private bool _showMemberForm; - private Guid? _editingMemberId; - private string _newMemberGender = ""; - private string _newMemberCountry = "VN"; - private string _newMemberName = ""; - private string _newMemberPhone = ""; - private List _staffSchedules = new(); - private List _invTxns = new(); - // P2 state: calendar, KDS, treatments - private int _calendarWeekOffset; - private string _kdsStation = "all"; - private string _treatmentTab = "treatment"; - // P4 state: notifications, customer detail - private bool _showNotifications; - private Guid? _selectedCustomerId; - private List _resources = new(); - // Customer filter state - private string _customerSearch = ""; - // Inventory sub-tab and form state - private string _invSubTab = "levels"; // levels, stock-in, stock-out, adjust, transactions, low-stock - private Guid _invSelectedProductId; - private int _invAmount; - private int _invNewQty; - private string _invNotes = ""; - private string? _invFormMessage; - private bool _invFormSuccess; - private List _lowStockItems = new(); - // Finance date range filter state - private string _financePeriod = "all"; // 7d, 30d, all - // Category form state - private bool _showCategoryForm; - private Guid? _editingCategoryId; - private string _newCategoryName = ""; - private string _newCategoryDesc = ""; - private int _newCategoryOrder; - private string? _categoryFormMessage; - private bool _categoryFormSuccess; - private List _categories = new(); - // Order detail state - private Guid? _selectedOrderId; - private PosDataService.OrderDetailResponse? _orderDetail; + + // Services section (reuses products) + private List _serviceProducts = new(); + // Shop edit state private bool _editingShop; private string _shopEditName = ""; @@ -2754,112 +396,6 @@ private string _shopEditCloseTime = ""; private string? _shopEditMessage; private bool _shopEditSuccess; - // Revenue report state - private string _reportPeriod = "daily"; - private List _revenueReport = new(); - // Settings state - private PosDataService.ShopSettingsInfo? _shopSettings; - private string _settingsOpenTime = ""; - private string _settingsCloseTime = ""; - private List _settingsOpenDays = new(); - private bool _featHasInventory, _featHasBooking, _featHasTables; - private bool _featHasKitchen, _featHasShipping, _featHasDelivery; - private string? _settingsMessage; - private bool _settingsSuccess; - // Customer sub-tab state - private string _customerSubTab = "members"; // members | levels | exp - // Level CRUD state - private bool _showLevelForm; - private Guid? _editingLevelId; - private int _newLevelNumber; - private string _newLevelName = ""; - private int _newLevelRequiredExp; - private string _newLevelDescription = ""; - private string _newLevelBadgeColor = "#CD7F32"; - private string? _levelFormMessage; - private bool _levelFormSuccess; - // EXP management state - private Guid? _expMemberId; - private int _expPoints = 100; - private int _expSourceId = 7; - private string? _expFormMessage; - private bool _expFormSuccess; - private PosDataService.MemberProgressInfo? _memberProgress; - private List _expHistory = new(); - // Staff extended fields state - private string _newStaffAddress = ""; - // Image upload state - private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _productImageFile; - private string? _productImagePreview; - private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _categoryImageFile; - private string? _categoryImagePreview; - private string _newCategoryImageUrl = ""; - private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _staffDocFrontFile; - private string? _staffDocFrontPreview; - private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _staffDocBackFile; - private string? _staffDocBackPreview; - // Top products state - private List _topProducts = new(); - // Tables CRUD state - private bool _showTableForm; - private Guid? _editingTableId; - private string _newTableNumber = ""; - private int _newTableCapacity = 4; - private string _newTableZone = ""; - private string? _tableFormMessage; - private bool _tableFormSuccess; - // Zones CRUD state - private bool _showZoneForm; - private string _newZoneName = ""; - private string? _editingZoneOriginalName; - private string? _zoneFormMessage; - private bool _zoneFormSuccess; - private readonly List _customZones = new(); - private static readonly string[] _zoneColors = { "#3B82F6", "#A855F7", "#22C55E", "#F59E0B", "#EC4899", "#6366F1" }; - private static readonly string[] _zoneIcons = { "building", "crown", "trees", "wine", "coffee", "map-pin" }; - private List AllZoneNames => _tables.Select(t => t.Zone ?? "Chung").Distinct() - .Union(_customZones).Distinct().OrderBy(z => z).ToList(); - // Kitchen state - private List _kitchenTickets = new(); - private string _kitchenStatusFilter = "all"; - // Appointments form state - private bool _showApptForm; - private DateTime _newApptStart = DateTime.Today.AddHours(9); - private DateTime _newApptEnd = DateTime.Today.AddHours(10); - private string? _apptFormMessage; - private bool _apptFormSuccess; - // Resources CRUD state - private bool _showResourceForm; - private Guid? _editingResourceId; - private string _newResourceName = ""; - private string _newResourceType = "Room"; - private int _newResourceCapacity = 1; - private string? _resourceFormMessage; - private bool _resourceFormSuccess; - // Schedule form state - private bool _showScheduleForm; - private Guid _newSchedStaffId; - private string _newSchedStaffIdStr = ""; - private int _newSchedDay = 1; - private string _newSchedStart = "08:00"; - private string _newSchedEnd = "17:00"; - private string? _schedFormMessage; - private bool _schedFormSuccess; - // Voucher management state - private string _promoSubTab = "campaigns"; // campaigns | vouchers - private List _vouchers = new(); - private Guid? _voucherCampaignFilter; - // Recipes state - private List _recipes = new(); - private bool _showRecipeForm; - private Guid? _editingRecipeId; - private string _newRecipeName = ""; - private string _newRecipeInstructions = ""; - private int _newRecipePrepTime = 5; - private List<(string Name, string Unit, string Qty, decimal Quantity, decimal Cost)> _recipeIngredients = new(); - private Guid? _expandedRecipeId; - private string? _recipeFormMessage; - private bool _recipeFormSuccess; protected override async Task OnInitializedAsync() { @@ -2869,7 +405,6 @@ protected override async Task OnParametersSetAsync() { - // EN: Called when URL params change / VI: Gọi khi params URL thay đổi if (_section != (Section?.ToLowerInvariant() ?? "")) await LoadData(); } @@ -2892,93 +427,26 @@ _shopName = shop.Name ?? "Cửa hàng"; _verticalLabel = ShopSidebarConfig.GetVerticalLabel(shop.Category); _posVertical = MapCategoryToVertical(shop.Category); - _merchantId = shop.MerchantId; AdminLayoutRef?.SetShopContext(ShopId, _shopName, shop.Category); } } - // EN: Load only data needed for current section / VI: Chỉ tải data cần cho section hiện tại + // Only load data needed for sections handled directly by shell switch (_section) { case "overview": _ovOrders = await DataService.GetOrdersAsync(_shopGuid); _ovProducts = await DataService.GetAllProductsAsync(_shopGuid); - _ovStaff = await DataService.GetStaffForShopAsync(_shopGuid.Value); + _ovStaff = await DataService.GetStaffForShopAsync(_shopGuid!.Value); if (_shopGuid.HasValue && (_posVertical == "restaurant" || _posVertical == "karaoke")) _ovTables = await DataService.GetTablesAsync(_shopGuid.Value); if (_shopGuid.HasValue && (_posVertical == "spa" || _posVertical == "beauty")) _ovAppts = await DataService.GetAppointmentsAsync(_shopGuid.Value); break; - case "menu": - case "products": - _products = await DataService.GetAllProductsAsync(_shopGuid); - _categories = await DataService.GetAllCategoriesAsync(_shopGuid); - break; - case "inventory": - _inventory = await DataService.GetInventoryAsync(_shopGuid); - _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); - break; - case "finance": - _orders = await DataService.GetOrdersAsync(_shopGuid); - _wallets = await DataService.GetWalletsAsync(); - _walletTxns = await DataService.GetWalletTransactionsAsync(); - break; - case "staff": - _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value); - _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid); - break; - case "customers": - var rawMembers = await DataService.GetMembersAsync(); - var rawLevels = await DataService.GetMembershipLevelsAsync(); - _memberLevels = PosDataService.EnrichLevelDefinitions(rawLevels, rawMembers); - _members = PosDataService.ResolveMemberLevelNames(rawMembers, rawLevels); - break; - case "tables": - case "rooms": - case "zones": - if (_shopGuid.HasValue) - _tables = await DataService.GetTablesAsync(_shopGuid.Value); - break; - case "appointments": - if (_shopGuid.HasValue) - _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value); - break; - case "resources": - if (_shopGuid.HasValue) - _resources = await DataService.GetResourcesAsync(_shopGuid.Value); - break; case "services": - _products = await DataService.GetAllProductsAsync(_shopGuid); - break; - case "reports": - _reportOrders = await DataService.GetOrdersAsync(_shopGuid); - _reportProducts = await DataService.GetAllProductsAsync(_shopGuid); - _topProducts = await DataService.GetTopProductsAsync(_shopGuid); - break; - case "settings": - if (_shopGuid.HasValue) - await LoadShopSettings(); - break; - case "promotions": - _campaigns = await DataService.GetCampaignsAsync(); - break; - case "shifts": - case "schedule": - _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid); - _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value); - break; - case "kitchen": - if (_shopGuid.HasValue) - _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid); - break; - case "recipes": - if (_shopGuid.HasValue) - _recipes = await DataService.GetRecipesAsync(_shopGuid); - break; - case "drive": - _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); - _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); + _serviceProducts = await DataService.GetAllProductsAsync(_shopGuid); break; + // All other sections are handled by child components with their own OnInitializedAsync } } catch (Exception ex) @@ -2989,58 +457,6 @@ finally { IsLoading = false; } } - private async Task LoadShopSettings() - { - if (!_shopGuid.HasValue) return; - try - { - _shopSettings = await DataService.GetShopSettingsAsync(_shopGuid.Value); - if (_shopSettings != null) - { - _settingsOpenTime = _shopSettings.OpenTime ?? ""; - _settingsCloseTime = _shopSettings.CloseTime ?? ""; - _settingsOpenDays = _shopSettings.OpenDays ?? new(); - if (_shopSettings.Features != null) - { - _featHasInventory = _shopSettings.Features.HasInventory; - _featHasBooking = _shopSettings.Features.HasBooking; - _featHasTables = _shopSettings.Features.HasTables; - _featHasKitchen = _shopSettings.Features.HasKitchen; - _featHasShipping = _shopSettings.Features.HasShipping; - _featHasDelivery = _shopSettings.Features.HasDelivery; - } - } - } - catch { /* non-fatal */ } - } - - private async Task SaveShopSettings() - { - if (!_shopGuid.HasValue) return; - _settingsMessage = null; - var features = new PosDataService.ShopFeaturesInfo - { - HasInventory = _featHasInventory, HasBooking = _featHasBooking, HasTables = _featHasTables, - HasKitchen = _featHasKitchen, HasShipping = _featHasShipping, HasDelivery = _featHasDelivery - }; - var req = new PosDataService.UpdateShopSettingsRequest( - Features: features, - OpenTime: string.IsNullOrWhiteSpace(_settingsOpenTime) ? null : _settingsOpenTime, - CloseTime: string.IsNullOrWhiteSpace(_settingsCloseTime) ? null : _settingsCloseTime, - OpenDays: _settingsOpenDays.Any() ? _settingsOpenDays : null); - var ok = await DataService.UpdateShopSettingsAsync(_shopGuid.Value, req); - _settingsSuccess = ok; - _settingsMessage = ok ? "Đã lưu thiết lập thành công!" : "Lỗi khi lưu thiết lập."; - StateHasChanged(); - } - - private void ToggleDay(string code) - { - if (_settingsOpenDays.Contains(code)) _settingsOpenDays.Remove(code); - else _settingsOpenDays.Add(code); - StateHasChanged(); - } - private void ConfigureSection() { switch (_section) @@ -3079,47 +495,21 @@ } } - private static string FormatVND(decimal val) => val.ToString("N0") + " ₫"; - - private async Task SwitchPromoTab(string tab) + private static string MapCategoryToVertical(string? category) => (category?.ToLowerInvariant()) switch { - _promoSubTab = tab; - if (tab == "vouchers" && !_vouchers.Any()) - { - _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); - StateHasChanged(); - } - } - - private async Task LoadVouchers() - { - _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); - StateHasChanged(); - } - - private async Task RevokeVoucher(Guid voucherId) - { - var ok = await DataService.RevokeVoucherAsync(voucherId); - if (ok) _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); - } - - private static string GetVoucherStatusLabel(string? status) => (status ?? "").ToLower() switch - { - "available" => "Chưa nhận", "claimed" => "Đã nhận", "redeemed" => "Đã dùng", - "revoked" => "Thu hồi", "expired" => "Hết hạn", _ => status ?? "—" + "cafe" or "coffee" => "cafe", + "restaurant" or "nhahang" => "restaurant", + "karaoke" => "karaoke", + "spa" => "spa", + "beauty" or "salon" => "beauty", + "retail" or "banle" => "retail", + _ => "cafe" }; - private static string GetVoucherStatusColor(string? status) => (status ?? "").ToLower() switch - { - "available" => "#F59E0B", "claimed" => "#3B82F6", "redeemed" => "#22C55E", - "revoked" => "#EF4444", "expired" => "#888", _ => "#888" - }; - - // EN: Reusable empty state renderer with dynamic CTA href / VI: Renderer trạng thái trống với CTA href động private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => {
-
+

@title

@@ -3134,254 +524,25 @@
}; - private static string HexToRgb(string hex) + private RenderFragment RenderStubSection(string icon, string color, string title, string desc) => __builder => { - hex = hex.TrimStart('#'); - if (hex.Length != 6) return "0,0,0"; - return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}"; - } - - /// - /// EN: Map shop category to POS route segment. - /// VI: Chuyển đổi danh mục cửa hàng thành segment POS route. - /// - private static string MapCategoryToVertical(string? category) => (category?.ToLowerInvariant()) switch - { - "cafe" or "coffee" => "cafe", - "restaurant" or "nhahang" => "restaurant", - "karaoke" => "karaoke", - "spa" => "spa", - "beauty" or "salon" => "beauty", - "retail" or "banle" => "retail", - _ => "cafe" +
+
+
+ +
+

@title

+

@desc

+
+ + Sẽ ra mắt trong phiên bản tiếp theo +
+
+
}; - // ═══ CRUD ACTIONS ═══ - private async Task AddProduct() - { - _formMessage = null; - if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || !_shopGuid.HasValue) - { - _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return; - } - try - { - Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid) ? cid : null; - var imgUrl = await UploadFileIfNeeded(_productImageFile); - await DataService.CreateProductAsync(new PosDataService.CreateProductRequest( - _shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl, null, catId)); - _formMessage = $"Đã thêm '{_newProductName}' thành công!"; _formSuccess = true; - _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductCategoryId = ""; _productImageFile = null; _productImagePreview = null; - _products = await DataService.GetAllProductsAsync(_shopGuid); - } - catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; } - } - - private async Task DeleteProduct(Guid productId) - { - try - { - await DataService.DeleteProductAsync(productId); - _products = await DataService.GetAllProductsAsync(_shopGuid); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa: {ex.Message}"; } - } - - private void EditProduct(PosDataService.AdminProductInfo p) - { - _editingProductId = p.Id; - _newProductName = p.Name; - _newProductPrice = p.Price; - _newProductType = p.Type ?? "PreparedFood"; - _newProductDesc = p.Description ?? ""; - _newProductCategoryId = p.CategoryId?.ToString() ?? ""; - _productImageFile = null; - _productImagePreview = p.ImageUrl; - _formMessage = null; - _showProductForm = true; - } - - private async Task SaveProduct() - { - _formMessage = null; - if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || !_shopGuid.HasValue || !_editingProductId.HasValue) - { - _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return; - } - try - { - Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid2) ? cid2 : null; - var imgUrl2 = await UploadFileIfNeeded(_productImageFile); - await DataService.UpdateProductAsync(_editingProductId.Value, new PosDataService.CreateProductRequest( - _shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl2, null, catId)); - _formMessage = $"Đã cập nhật '{_newProductName}' thành công!"; _formSuccess = true; - _editingProductId = null; - _products = await DataService.GetAllProductsAsync(_shopGuid); - } - catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; } - } - - private async Task AddStaff() - { - _staffFormMessage = null; - if (!_merchantId.HasValue) - { - _staffFormMessage = "Không tìm thấy thông tin merchant. Vui lòng tải lại trang."; _staffFormSuccess = false; return; - } - if (string.IsNullOrWhiteSpace(_newStaffCode)) - { - _staffFormMessage = "Vui lòng nhập mã nhân viên."; _staffFormSuccess = false; return; - } - try - { - var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile); - var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile); - if (_createStaffAccount) - { - if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword)) - { - _staffFormMessage = "Vui lòng nhập đầy đủ email và mật khẩu."; _staffFormSuccess = false; return; - } - var (ok, err) = await DataService.InviteStaffWithAccountAsync(new PosDataService.InviteStaffWithAccountRequest( - _newStaffEmail, _newStaffPassword, _newStaffFirstName, _newStaffLastName, _newStaffRole, _shopGuid)); - if (!ok) { _staffFormMessage = err ?? "Lỗi tạo tài khoản IAM. Kiểm tra email/mật khẩu."; _staffFormSuccess = false; return; } - _staffFormMessage = $"Đã tạo tài khoản + mời NV '{_newStaffEmail}' thành công!"; _staffFormSuccess = true; - } - else - { - await DataService.CreateStaffAsync(new PosDataService.CreateStaffRequest( - _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole, - _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl)); - _staffFormMessage = $"Đã thêm NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true; - } - _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _createStaffAccount = false; - _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; - _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value); - } - catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } - } - - private void EditStaff(PosDataService.StaffInfo s) - { - _editingStaffId = s.Id; - _newStaffCode = s.EmployeeCode ?? ""; - _newStaffRole = s.Role ?? "Cashier"; - _newStaffPhone = s.Phone ?? ""; - _newStaffEmail = s.Email ?? ""; - _newStaffFirstName = s.FirstName ?? ""; - _newStaffLastName = s.LastName ?? ""; - _newStaffAddress = s.Address ?? ""; - _staffDocFrontFile = null; _staffDocBackFile = null; - _staffDocFrontPreview = s.DocumentFrontUrl; - _staffDocBackPreview = s.DocumentBackUrl; - _staffFormMessage = null; - _showStaffForm = true; - } - - private async Task SaveStaffEdit() - { - _staffFormMessage = null; - if (string.IsNullOrWhiteSpace(_newStaffCode) || !_merchantId.HasValue || !_editingStaffId.HasValue) - { - _staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return; - } - try - { - var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile) ?? (_staffDocFrontPreview?.StartsWith("data:") == true ? null : _staffDocFrontPreview); - var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile) ?? (_staffDocBackPreview?.StartsWith("data:") == true ? null : _staffDocBackPreview); - await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest( - _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole, - _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl)); - _staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true; - _editingStaffId = null; - _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; - _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value); - } - catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } - } - - private async Task DeleteStaffMember(Guid staffId) - { - try - { - await DataService.DeleteStaffAsync(staffId); - _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa nhân viên: {ex.Message}"; } - } - - // EN: Day-of-week label / VI: Nhãn ngày trong tuần - private static string DayLabel(int dow) => dow switch - { - 0 => "CN", 1 => "T2", 2 => "T3", 3 => "T4", - 4 => "T5", 5 => "T6", 6 => "T7", _ => $"#{dow}" - }; - - // ═══ CATEGORY CRUD ═══ - private async Task SaveCategory() - { - if (string.IsNullOrWhiteSpace(_newCategoryName)) { _categoryFormMessage = "Tên danh mục không được trống."; _categoryFormSuccess = false; return; } - var catImgUrl = await UploadFileIfNeeded(_categoryImageFile) ?? _newCategoryImageUrl; - var req = new PosDataService.AdminCreateCategoryRequest(_shopGuid ?? Guid.Empty, _newCategoryName, _newCategoryDesc, _newCategoryOrder, string.IsNullOrWhiteSpace(catImgUrl) ? null : catImgUrl); - bool ok; - if (_editingCategoryId.HasValue) - ok = await DataService.UpdateCategoryAsync(_editingCategoryId.Value, req); - else - ok = await DataService.CreateCategoryAsync(req); - _categoryFormMessage = ok ? (_editingCategoryId.HasValue ? "Đã cập nhật danh mục!" : "Đã thêm danh mục!") : "Lỗi khi lưu danh mục."; - _categoryFormSuccess = ok; - if (ok) { _showCategoryForm = false; _categoryImageFile = null; _categoryImagePreview = null; _newCategoryImageUrl = ""; _categories = await DataService.GetAllCategoriesAsync(_shopGuid); } - } - private void EditCategory(PosDataService.AdminCategoryInfo c) { _editingCategoryId = c.Id; _newCategoryName = c.Name ?? ""; _newCategoryDesc = c.Description ?? ""; _newCategoryOrder = c.DisplayOrder; _newCategoryImageUrl = c.ImageUrl ?? ""; _categoryImageFile = null; _categoryImagePreview = c.ImageUrl; _showCategoryForm = true; _categoryFormMessage = null; } - private async Task DeleteCategoryItem(Guid id) { await DataService.DeleteCategoryAsync(id); _categories = await DataService.GetAllCategoriesAsync(_shopGuid); } - - // ═══ INVENTORY OPERATIONS ═══ - private async Task SwitchInvSubTab(string tab) - { - _invSubTab = tab; - _invFormMessage = null; - if (tab == "low-stock") await LoadLowStock(); - StateHasChanged(); - } - private async Task LoadLowStock() - { - _lowStockItems = await DataService.GetLowStockAsync(_shopGuid); - StateHasChanged(); - } - private async Task DoStockIn() - { - _invFormMessage = null; - if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; } - var ok = await DataService.StockInAsync(new PosDataService.StockInRequest(_invSelectedProductId, _shopGuid!.Value, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes)); - _invFormMessage = ok ? $"Đã nhập kho thành công +{_invAmount}!" : "Lỗi khi nhập kho."; - _invFormSuccess = ok; - if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } - } - private async Task DoStockOut() - { - _invFormMessage = null; - if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; } - var ok = await DataService.StockOutAsync(new PosDataService.StockOutRequest(_invSelectedProductId, _shopGuid!.Value, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes)); - _invFormMessage = ok ? $"Đã xuất kho thành công -{_invAmount}!" : "Lỗi khi xuất kho. Kiểm tra số lượng tồn."; - _invFormSuccess = ok; - if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } - } - private async Task DoAdjustStock() - { - _invFormMessage = null; - if (_invSelectedProductId == Guid.Empty || string.IsNullOrWhiteSpace(_invNotes)) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập lý do điều chỉnh."; _invFormSuccess = false; return; } - var ok = await DataService.AdjustStockAsync(new PosDataService.AdjustStockRequest(_invSelectedProductId, _shopGuid!.Value, _invNewQty, _invNotes)); - _invFormMessage = ok ? $"Đã điều chỉnh tồn kho = {_invNewQty}!" : "Lỗi khi điều chỉnh."; - _invFormSuccess = ok; - if (ok) { _invNewQty = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); } - } - - // ═══ ORDER DETAIL ═══ - private async Task ViewOrderDetail(Guid orderId) { if (_selectedOrderId == orderId) { _selectedOrderId = null; _orderDetail = null; return; } _selectedOrderId = orderId; try { _orderDetail = await DataService.GetOrderDetailAsync(orderId, _shopGuid); } catch { _orderDetail = null; } } - private async Task CancelOrderItem(Guid orderId) { var ok = await DataService.CancelOrderAsync(orderId); if (ok) { _selectedOrderId = null; _orderDetail = null; await LoadData(); } } - - // ═══ SHOP EDIT ═══ private void StartEditShop() { _editingShop = true; _shopEditName = _shopName ?? ""; _shopEditPhone = ""; _shopEditEmail = ""; _shopEditDesc = ""; _shopEditOpenTime = ""; _shopEditCloseTime = ""; _shopEditMessage = null; } + private async Task SaveShopEdit() { var req = new PosDataService.UpdateShopRequest(_shopEditName, _shopEditPhone, _shopEditEmail, _shopEditDesc, _shopEditOpenTime, _shopEditCloseTime, null); @@ -3391,453 +552,6 @@ if (ok) { _editingShop = false; await LoadData(); } } - // ═══ REVENUE REPORT ═══ - private async Task LoadRevenueReport(string period) { _reportPeriod = period; _revenueReport = await DataService.GetRevenueReportAsync(period, _shopGuid); } - - // ═══ CAMPAIGN CRUD ═══ - private async Task SaveCampaign() - { - _campaignFormMessage = null; - if (string.IsNullOrWhiteSpace(_newCampaignName) || _newCampaignValue <= 0 || _newCampaignVouchers <= 0) - { - _campaignFormMessage = "Vui lòng nhập đầy đủ tên, giá trị và số lượng voucher."; _campaignFormSuccess = false; return; - } - var desc = string.IsNullOrWhiteSpace(_newCampaignDesc) ? _newCampaignDiscountType : $"{_newCampaignDesc} [{_newCampaignDiscountType}]"; - if (_newCampaignDiscountType == "percentage") desc = $"Giảm {_newCampaignValue}% [{_newCampaignDiscountType}]"; - var req = new PosDataService.CreateCampaignRequest(_newCampaignName, desc, _newCampaignValue, _newCampaignVouchers, _newCampaignStart, _newCampaignEnd); - bool ok; - if (_editingCampaignId.HasValue) - ok = await DataService.UpdateCampaignAsync(_editingCampaignId.Value, req); - else - ok = await DataService.CreateCampaignAsync(req); - _campaignFormMessage = ok ? (_editingCampaignId.HasValue ? "Đã cập nhật chiến dịch!" : "Đã thêm chiến dịch!") : "Lỗi khi lưu chiến dịch."; - _campaignFormSuccess = ok; - if (ok) { _showCampaignForm = false; _editingCampaignId = null; _campaigns = await DataService.GetCampaignsAsync(); } - } - - private void EditCampaign(PosDataService.CampaignInfo c) - { - _editingCampaignId = c.Id; _newCampaignName = c.Name; _newCampaignDesc = c.Description ?? ""; - _newCampaignValue = c.FaceValue; _newCampaignVouchers = c.TotalVouchers; - _newCampaignStart = c.StartDate?.ToLocalTime().Date ?? DateTime.Today; - _newCampaignEnd = c.EndDate?.ToLocalTime().Date ?? DateTime.Today.AddMonths(1); - _showCampaignForm = true; _campaignFormMessage = null; - } - - private async Task DeleteCampaignItem(Guid campaignId) - { - await DataService.DeleteCampaignAsync(campaignId); - _campaigns = await DataService.GetCampaignsAsync(); - } - - // ═══ MEMBER CRUD ═══ - private string? _memberFormMessage; - private bool _memberFormSuccess; - private async Task SaveMember() - { - _memberFormMessage = null; - if (_editingMemberId.HasValue) - { - var ok = await DataService.UpdateMemberAsync(_editingMemberId.Value, new PosDataService.UpdateMemberRequest(_newMemberGender, null)); - if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; await RefreshMembersAndLevels(); } - } - else - { - var (ok, err) = await DataService.CreateMemberAsync(new PosDataService.CreateMemberRequest(_newMemberGender, _newMemberCountry, - string.IsNullOrWhiteSpace(_newMemberName) ? null : _newMemberName, - string.IsNullOrWhiteSpace(_newMemberPhone) ? null : _newMemberPhone)); - if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; _memberFormMessage = null; await RefreshMembersAndLevels(); } - else { _memberFormMessage = err ?? "Lỗi tạo khách hàng."; _memberFormSuccess = false; } - } - } - - private void EditMember(PosDataService.MemberInfo m) - { - _editingMemberId = m.Id; _newMemberGender = m.Gender ?? ""; _newMemberCountry = m.CountryCode ?? "VN"; - _newMemberName = m.DisplayName ?? ""; _newMemberPhone = m.Phone ?? ""; - _showMemberForm = true; - } - - private async Task DeleteMemberItem(Guid memberId) - { - await DataService.DeleteMemberAsync(memberId); - await RefreshMembersAndLevels(); - } - - // ═══ LEVEL CRUD ═══ - private async Task SaveLevel() - { - _levelFormMessage = null; - if (string.IsNullOrWhiteSpace(_newLevelName) || _newLevelNumber <= 0) - { _levelFormMessage = "Nhập tên và số level."; _levelFormSuccess = false; return; } - var req = new PosDataService.CreateLevelRequest(_newLevelNumber, _newLevelName, _newLevelRequiredExp, _newLevelDescription, _newLevelBadgeColor); - if (_editingLevelId.HasValue) - { - var (ok, err) = await DataService.UpdateLevelAsync(_editingLevelId.Value, req); - _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã cập nhật!" : err ?? "Lỗi."; - } - else - { - var (ok, err) = await DataService.CreateLevelAsync(req); - _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã thêm!" : err ?? "Lỗi."; - } - if (_levelFormSuccess) { _showLevelForm = false; await RefreshMembersAndLevels(); } - } - private void EditLevel(PosDataService.LevelDefinitionInfo lvl) - { - _editingLevelId = lvl.Id; _newLevelNumber = lvl.LevelNumber; _newLevelName = lvl.Name; - _newLevelRequiredExp = lvl.RequiredExp; _newLevelDescription = lvl.Description ?? ""; - _newLevelBadgeColor = lvl.BadgeColor ?? "#CD7F32"; _showLevelForm = true; - } - private async Task DeleteLevel(Guid id) - { - await DataService.DeleteLevelAsync(id); - await RefreshMembersAndLevels(); - } - - // ═══ EXP MANAGEMENT ═══ - private async Task AddExpToMember() - { - _expFormMessage = null; - if (!_expMemberId.HasValue || _expPoints <= 0) - { _expFormMessage = "Chọn thành viên và nhập số điểm > 0."; _expFormSuccess = false; return; } - var result = await DataService.AddExperienceAsync(_expMemberId.Value, new PosDataService.AddExpRequest(_expPoints, _expSourceId, null)); - if (result != null) - { - _expFormSuccess = true; - _expFormMessage = result.LeveledUp ? $"Đã cộng {result.PointsAdded} EXP! Lên level {result.CurrentLevel}!" : $"Đã cộng {result.PointsAdded} EXP! Tổng: {result.TotalExpEarned}"; - _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value); - _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value); - await RefreshMembersAndLevels(); - } - else { _expFormMessage = "Lỗi khi cộng EXP."; _expFormSuccess = false; } - } - private async Task LoadMemberProgress() - { - if (_expMemberId.HasValue) - { - _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value); - _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value); - } - else { _memberProgress = null; _expHistory = new(); } - } - - // ═══ IMAGE UPLOAD HELPERS ═══ - private async Task OnProductImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) - { - _productImageFile = e.File; - var format = "image/png"; - var resized = await e.File.RequestImageFileAsync(format, 200, 200); - var buffer = new byte[resized.Size]; - await resized.OpenReadStream().ReadAsync(buffer); - _productImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; - StateHasChanged(); - } - private async Task OnCategoryImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) - { - _categoryImageFile = e.File; - var format = "image/png"; - var resized = await e.File.RequestImageFileAsync(format, 200, 200); - var buffer = new byte[resized.Size]; - await resized.OpenReadStream().ReadAsync(buffer); - _categoryImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; - StateHasChanged(); - } - private async Task OnStaffDocFrontSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) - { - _staffDocFrontFile = e.File; - var format = "image/png"; - var resized = await e.File.RequestImageFileAsync(format, 300, 200); - var buffer = new byte[resized.Size]; - await resized.OpenReadStream().ReadAsync(buffer); - _staffDocFrontPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; - StateHasChanged(); - } - private async Task OnStaffDocBackSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e) - { - _staffDocBackFile = e.File; - var format = "image/png"; - var resized = await e.File.RequestImageFileAsync(format, 300, 200); - var buffer = new byte[resized.Size]; - await resized.OpenReadStream().ReadAsync(buffer); - _staffDocBackPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; - StateHasChanged(); - } - private async Task UploadFileIfNeeded(Microsoft.AspNetCore.Components.Forms.IBrowserFile? file) - { - if (file == null) return null; - using var stream = file.OpenReadStream(maxAllowedSize: 10_485_760); - return await DataService.UploadImageAsync(stream, file.Name, file.ContentType); - } - - // ═══ TABLE CRUD ═══ - private void EditTable(PosDataService.TableInfo table) - { - _editingTableId = table.Id; - _newTableNumber = table.TableNumber; - _newTableCapacity = table.Capacity; - _newTableZone = table.Zone ?? ""; - _tableFormMessage = null; - _showTableForm = true; - } - - private async Task AddTable() - { - _tableFormMessage = null; - if (string.IsNullOrWhiteSpace(_newTableNumber) || !_shopGuid.HasValue) - { - _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return; - } - try - { - await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(_shopGuid.Value, _newTableNumber, _newTableCapacity, _newTableZone)); - _tableFormMessage = $"Đã thêm bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; - _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; - if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value); - } - catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; } - } - - private async Task SaveTable() - { - _tableFormMessage = null; - if (string.IsNullOrWhiteSpace(_newTableNumber) || !_shopGuid.HasValue || !_editingTableId.HasValue) - { - _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return; - } - try - { - await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(_shopGuid.Value, _newTableNumber, _newTableCapacity, _newTableZone)); - _tableFormMessage = $"Đã cập nhật bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; - _editingTableId = null; - if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value); - } - catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; } - } - - private async Task DeleteTableItem(Guid id) - { - try - { - await DataService.DeleteTableAsync(id); - if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa bàn: {ex.Message}"; } - } - - // ═══ ZONE MANAGEMENT ═══ - private async Task SaveZone() - { - _zoneFormMessage = null; - if (string.IsNullOrWhiteSpace(_newZoneName) || !_shopGuid.HasValue) - { - _zoneFormMessage = "Vui lòng nhập tên khu vực."; _zoneFormSuccess = false; return; - } - if (_editingZoneOriginalName != null) - { - // Rename: update all tables in old zone to new zone name - var tablesInZone = _tables.Where(t => (t.Zone ?? "Chung") == _editingZoneOriginalName).ToList(); - try - { - foreach (var t in tablesInZone) - await DataService.UpdateTableAsync(t.Id, new PosDataService.CreateTableRequest(_shopGuid.Value, t.TableNumber, t.Capacity, _newZoneName.Trim())); - _tables = await DataService.GetTablesAsync(_shopGuid.Value); - _zoneFormMessage = $"Đã đổi tên '{_editingZoneOriginalName}' → '{_newZoneName.Trim()}'"; _zoneFormSuccess = true; - _editingZoneOriginalName = null; _newZoneName = ""; _showZoneForm = false; - } - catch (Exception ex) { _zoneFormMessage = $"Lỗi: {ex.Message}"; _zoneFormSuccess = false; } - } - else - { - var zoneName = _newZoneName.Trim(); - if (!_customZones.Contains(zoneName) && !_tables.Any(t => (t.Zone ?? "Chung") == zoneName)) - _customZones.Add(zoneName); - _zoneFormMessage = $"Đã thêm khu vực '{zoneName}'."; _zoneFormSuccess = true; - _newTableZone = zoneName; - _newZoneName = ""; - _showZoneForm = false; - } - } - - // ═══ APPOINTMENT CRUD ═══ - private async Task AddAppointment() - { - _apptFormMessage = null; - if (!_shopGuid.HasValue) - { - _apptFormMessage = "Thiếu thông tin cửa hàng."; _apptFormSuccess = false; return; - } - try - { - await DataService.CreateAppointmentAsync(new PosDataService.CreateAppointmentRequest( - _shopGuid.Value, null, null, null, null, _newApptStart, _newApptEnd)); - _apptFormMessage = "Đã thêm lịch hẹn thành công!"; _apptFormSuccess = true; - _showApptForm = false; - _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value); - } - catch (Exception ex) { _apptFormMessage = $"Lỗi: {ex.Message}"; _apptFormSuccess = false; } - } - - private async Task CancelAppt(Guid apptId) - { - try - { - await DataService.CancelAppointmentAsync(apptId); - if (_shopGuid.HasValue) _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value); - } - catch (Exception ex) { _errorMessage = $"Không thể hủy lịch hẹn: {ex.Message}"; } - } - - // ═══ RESOURCE CRUD ═══ - private void EditResource(PosDataService.ResourceInfo r) - { - _editingResourceId = r.Id; - _newResourceName = r.Name; - _newResourceType = r.ResourceType ?? "Room"; - _newResourceCapacity = r.Capacity; - _resourceFormMessage = null; - _showResourceForm = true; - } - - private async Task AddResource() - { - _resourceFormMessage = null; - if (string.IsNullOrWhiteSpace(_newResourceName) || !_shopGuid.HasValue) - { - _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return; - } - try - { - await DataService.CreateResourceAsync(new PosDataService.CreateResourceRequest(_shopGuid.Value, _newResourceName, _newResourceType, _newResourceCapacity)); - _resourceFormMessage = $"Đã thêm '{_newResourceName}' thành công!"; _resourceFormSuccess = true; - _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1; - _resources = await DataService.GetResourcesAsync(_shopGuid.Value); - } - catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; } - } - - private async Task SaveResource() - { - _resourceFormMessage = null; - if (string.IsNullOrWhiteSpace(_newResourceName) || !_shopGuid.HasValue || !_editingResourceId.HasValue) - { - _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return; - } - try - { - await DataService.UpdateResourceAsync(_editingResourceId.Value, new PosDataService.CreateResourceRequest(_shopGuid.Value, _newResourceName, _newResourceType, _newResourceCapacity)); - _resourceFormMessage = $"Đã cập nhật '{_newResourceName}' thành công!"; _resourceFormSuccess = true; - _editingResourceId = null; - _resources = await DataService.GetResourcesAsync(_shopGuid.Value); - } - catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; } - } - - private async Task DeleteResourceItem(Guid id) - { - try - { - await DataService.DeleteResourceAsync(id); - if (_shopGuid.HasValue) _resources = await DataService.GetResourcesAsync(_shopGuid.Value); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa tài nguyên: {ex.Message}"; } - } - - // ═══ SCHEDULE CRUD ═══ - private async Task AddSchedule() - { - _schedFormMessage = null; - if (!Guid.TryParse(_newSchedStaffIdStr, out var staffId) || !_shopGuid.HasValue) - { - _schedFormMessage = "Vui lòng nhập đúng Staff ID."; _schedFormSuccess = false; return; - } - try - { - _newSchedStaffId = staffId; - await DataService.CreateScheduleAsync(new PosDataService.CreateScheduleRequest(_shopGuid.Value, _newSchedStaffId, _newSchedDay, _newSchedStart, _newSchedEnd)); - _schedFormMessage = "Đã thêm lịch làm việc thành công!"; _schedFormSuccess = true; - _showScheduleForm = false; - _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid); - } - catch (Exception ex) { _schedFormMessage = $"Lỗi: {ex.Message}"; _schedFormSuccess = false; } - } - - private async Task DeleteScheduleItem(Guid id) - { - try - { - await DataService.DeleteScheduleAsync(id); - _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa lịch làm việc: {ex.Message}"; } - } - - // ═══ KITCHEN ═══ - private async Task LoadKitchenTickets(string status) - { - _kitchenStatusFilter = status; - try - { - if (_shopGuid.HasValue) - _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid, status); - } - catch (Exception ex) { _errorMessage = $"Không thể tải kitchen tickets: {ex.Message}"; } - StateHasChanged(); - } - - private async Task MarkTicketDone(Guid ticketId) - { - try - { - await DataService.UpdateTicketStatusAsync(ticketId, new PosDataService.UpdateTicketStatusRequest("completed")); - if (_shopGuid.HasValue) - _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid, _kitchenStatusFilter); - } - catch (Exception ex) { _errorMessage = $"Không thể cập nhật trạng thái: {ex.Message}"; } - StateHasChanged(); - } - - // ═══ RECIPE CRUD ═══ - private async Task SaveRecipe() - { - _recipeFormMessage = null; - if (string.IsNullOrWhiteSpace(_newRecipeName) || !_shopGuid.HasValue) - { - _recipeFormMessage = "Vui lòng nhập tên công thức."; _recipeFormSuccess = false; return; - } - try - { - var ingredients = _recipeIngredients - .Where(i => !string.IsNullOrWhiteSpace(i.Name)) - .Select(i => new PosDataService.RecipeIngredientRequest(i.Name, i.Quantity, i.Unit, i.Cost)) - .ToList(); - var req = new PosDataService.CreateRecipeRequest(_shopGuid.Value, Guid.Empty, _newRecipeName, _newRecipeInstructions, _newRecipePrepTime, ingredients); - bool ok; - if (_editingRecipeId.HasValue) - ok = await DataService.UpdateRecipeAsync(_editingRecipeId.Value, req); - else - ok = await DataService.CreateRecipeAsync(req); - _recipeFormMessage = ok ? (_editingRecipeId.HasValue ? "Đã cập nhật công thức!" : "Đã thêm công thức!") : "Lỗi khi lưu công thức."; - _recipeFormSuccess = ok; - if (ok) { _showRecipeForm = false; _editingRecipeId = null; _recipes = await DataService.GetRecipesAsync(_shopGuid); } - } - catch (Exception ex) { _recipeFormMessage = $"Lỗi: {ex.Message}"; _recipeFormSuccess = false; } - } - - private async Task DeleteRecipeItem(Guid id) - { - try - { - await DataService.DeleteRecipeAsync(id); - _recipes = await DataService.GetRecipesAsync(_shopGuid); - } - catch (Exception ex) { _errorMessage = $"Không thể xóa công thức: {ex.Message}"; } - } - - private string? _toastMessage; - private void ShowComingSoonPromo() => ShowComingSoon("Tặng ưu đãi"); - private void ShowComingSoonMessage() => ShowComingSoon("Gửi tin nhắn"); - private void GoToOrderHistory() => Nav.NavigateTo($"/admin/shop/{ShopId}/finance"); private async void ShowComingSoon(string feature) { _toastMessage = $"{feature} — tính năng sắp ra mắt!"; @@ -3846,31 +560,4 @@ _toastMessage = null; StateHasChanged(); } - - private async Task RefreshMembersAndLevels() - { - var rm = await DataService.GetMembersAsync(); - var rl = await DataService.GetMembershipLevelsAsync(); - _memberLevels = PosDataService.EnrichLevelDefinitions(rl, rm); - _members = PosDataService.ResolveMemberLevelNames(rm, rl); - } - - private void ToggleFolderForm() => _showFolderForm = !_showFolderForm; - private void HideFolderForm() => _showFolderForm = false; - private List _storageFiles = new(); - private List _storageFolders = new(); - private Guid? _currentFolderId; - private string _newFolderName = ""; - private bool _showFolderForm; - private string _storageSearch = ""; - private async Task SearchStorageFiles() => _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); - private async Task NavigateToFolder(Guid id) { _currentFolderId = id; _storageFolders = await DataService.GetFoldersAsync(id); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); } - private async Task NavigateToParentFolder() { _currentFolderId = null; _storageFolders = await DataService.GetFoldersAsync(null); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); } - private async Task CreateFolder() { if (string.IsNullOrWhiteSpace(_newFolderName)) return; await DataService.CreateFolderAsync(new PosDataService.CreateFolderRequest(_newFolderName.Trim(), _currentFolderId)); _newFolderName = ""; _showFolderForm = false; _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); } - private async Task DeleteFolder(Guid id) { await DataService.DeleteFolderAsync(id); _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); } - private async Task DeleteStorageFile(Guid id) { await DataService.DeleteStorageFileAsync(id); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); } - private async Task DownloadStorageFile(Guid id) { var url = await DataService.GetDownloadUrlAsync(id); if (url != null) Nav.NavigateTo(url, forceLoad: true); } - private async Task HandleDriveUpload(InputFileChangeEventArgs e) { foreach (var f in e.GetMultipleFiles(20)) { using var s = f.OpenReadStream(10_485_760); await DataService.UploadImageAsync(s, f.Name, f.ContentType); } _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); } - private static string FormatFileSize(long b) => b < 1024 ? $"{b} B" : b < 1048576 ? $"{b/1024.0:F1} KB" : b < 1073741824 ? $"{b/1048576.0:F1} MB" : $"{b/1073741824.0:F2} GB"; - private static string GetFileIcon(string? ct) => ct switch { string s when s.StartsWith("image/") => "image", string s when s.StartsWith("video/") => "video", string s when s.Contains("pdf") => "file-text", _ => "file" }; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor new file mode 100644 index 00000000..00840826 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor @@ -0,0 +1,268 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+ + Chiến dịch khuyến mãi chung cho tất cả cửa hàng trong thương hiệu +
+@* ─── Sub-tabs: Campaigns | Vouchers ─── *@ +
+ @{ var promoTabs = new[] { ("campaigns", "Chiến dịch", "tag"), ("vouchers", "Mã voucher", "ticket") }; } + @foreach (var (tab, label, icon) in promoTabs) + { + var t = tab; + var isActive = _promoSubTab == t; + + } +
+@if (_promoSubTab == "campaigns") +{ +
+

@_campaigns.Count chiến dịch

+ +
+@if (_showCampaignForm) +{ +
+

@(_editingCampaignId.HasValue ? "Sửa chiến dịch" : "Thêm chiến dịch")

+
+
+
+
+
+ +
+
+
+
+
+
+ @if (_campaignFormMessage != null) + { +
@_campaignFormMessage
+ } +
+ + +
+
+
+} +@if (!_campaigns.Any()) +{ +
+
+ +
+

Chưa có chiến dịch

+

Tạo chiến dịch voucher, khuyến mãi cho khách hàng

+ + + Thêm chiến dịch + +
+} +else +{ +
+
@_campaigns.CountTổng chiến dịch
+
@_campaigns.Count(c => c.Status == "Active")Đang hoạt động
+
@_campaigns.Sum(c => c.TotalVouchers)Tổng voucher
+
@_campaigns.Sum(c => c.IssuedVouchers)Đã phát
+
+
+

Danh sách chiến dịch

+
+ + + + + + + + + + @foreach (var c in _campaigns) + { + + + + + + + + + + } +
TênLoạiGiá trịĐã phát/TổngBắt đầuKết thúc
@c.Name@(c.Description?.Contains("%") == true ? "%" : "₫")@(c.Description?.Contains("%") == true ? $"{c.FaceValue}%" : ShopHelpers.FormatVND(c.FaceValue))@c.IssuedVouchers / @c.TotalVouchers@(c.StartDate?.ToString("dd/MM/yy") ?? "—")@(c.EndDate?.ToString("dd/MM/yy") ?? "—") +
+ + +
+
+
+
+} @* end else *@ +} @* end campaigns sub-tab *@ +@if (_promoSubTab == "vouchers") +{ +
+

@_vouchers.Count mã voucher

+ +
+ @if (!_vouchers.Any()) + { +
+
+ +
+

Chưa có voucher

+

Tạo chiến dịch để tự động sinh mã voucher

+ + + Tạo chiến dịch + +
+ } + else + { +
+

Danh sách mã voucher

+
+ + + + + + + + + + @foreach (var v in _vouchers) + { + + + + + + + + + + } +
Chiến dịchMệnh giáCòn lạiTrạng tháiNgày tạo
@v.Code@(v.CampaignName ?? "—")@ShopHelpers.FormatVND(v.FaceValue)@ShopHelpers.FormatVND(v.RemainingValue)@GetVoucherStatusLabel(v.Status)@(v.CreatedAt?.ToLocalTime().ToString("dd/MM/yy") ?? "—") + @if (v.Status?.ToLower() == "available") + { + + } +
+
+
+ } +} + +@code { + [Parameter] public Guid ShopId { get; set; } + + private List _campaigns = new(); + private bool _showCampaignForm; + private Guid? _editingCampaignId; + private string _newCampaignName = ""; + private string _newCampaignDesc = ""; + private decimal _newCampaignValue; + private int _newCampaignVouchers; + private DateTime _newCampaignStart = DateTime.Today; + private DateTime _newCampaignEnd = DateTime.Today.AddMonths(1); + private string _newCampaignDiscountType = "fixed"; + private string? _campaignFormMessage; + private bool _campaignFormSuccess; + private string _promoSubTab = "campaigns"; + private List _vouchers = new(); + private Guid? _voucherCampaignFilter; + + protected override async Task OnInitializedAsync() + { + _campaigns = await DataService.GetCampaignsAsync(); + } + + private async Task SwitchPromoTab(string tab) + { + _promoSubTab = tab; + if (tab == "vouchers" && !_vouchers.Any()) + { + _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); + StateHasChanged(); + } + } + + private async Task LoadVouchers() + { + _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); + StateHasChanged(); + } + + private async Task RevokeVoucher(Guid voucherId) + { + var ok = await DataService.RevokeVoucherAsync(voucherId); + if (ok) _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter); + } + + private static string GetVoucherStatusLabel(string? status) => (status ?? "").ToLower() switch + { + "available" => "Chưa nhận", "claimed" => "Đã nhận", "redeemed" => "Đã dùng", + "revoked" => "Thu hồi", "expired" => "Hết hạn", _ => status ?? "—" + }; + + private static string GetVoucherStatusColor(string? status) => (status ?? "").ToLower() switch + { + "available" => "#F59E0B", "claimed" => "#3B82F6", "redeemed" => "#22C55E", + "revoked" => "#EF4444", "expired" => "#888", _ => "#888" + }; + + private async Task SaveCampaign() + { + _campaignFormMessage = null; + if (string.IsNullOrWhiteSpace(_newCampaignName) || _newCampaignValue <= 0 || _newCampaignVouchers <= 0) + { + _campaignFormMessage = "Vui lòng nhập đầy đủ tên, giá trị và số lượng voucher."; _campaignFormSuccess = false; return; + } + var desc = string.IsNullOrWhiteSpace(_newCampaignDesc) ? _newCampaignDiscountType : $"{_newCampaignDesc} [{_newCampaignDiscountType}]"; + if (_newCampaignDiscountType == "percentage") desc = $"Giảm {_newCampaignValue}% [{_newCampaignDiscountType}]"; + var req = new PosDataService.CreateCampaignRequest(_newCampaignName, desc, _newCampaignValue, _newCampaignVouchers, _newCampaignStart, _newCampaignEnd); + bool ok; + if (_editingCampaignId.HasValue) + ok = await DataService.UpdateCampaignAsync(_editingCampaignId.Value, req); + else + ok = await DataService.CreateCampaignAsync(req); + _campaignFormMessage = ok ? (_editingCampaignId.HasValue ? "Đã cập nhật chiến dịch!" : "Đã thêm chiến dịch!") : "Lỗi khi lưu chiến dịch."; + _campaignFormSuccess = ok; + if (ok) { _showCampaignForm = false; _editingCampaignId = null; _campaigns = await DataService.GetCampaignsAsync(); } + } + + private void EditCampaign(PosDataService.CampaignInfo c) + { + _editingCampaignId = c.Id; _newCampaignName = c.Name; _newCampaignDesc = c.Description ?? ""; + _newCampaignValue = c.FaceValue; _newCampaignVouchers = c.TotalVouchers; + _newCampaignStart = c.StartDate?.ToLocalTime().Date ?? DateTime.Today; + _newCampaignEnd = c.EndDate?.ToLocalTime().Date ?? DateTime.Today.AddMonths(1); + _showCampaignForm = true; _campaignFormMessage = null; + } + + private async Task DeleteCampaignItem(Guid campaignId) + { + await DataService.DeleteCampaignAsync(campaignId); + _campaigns = await DataService.GetCampaignsAsync(); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor new file mode 100644 index 00000000..b02a8fcc --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor @@ -0,0 +1,155 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+

@_recipes.Count công thức

+ +
+@if (_showRecipeForm) +{ +
+

@(_editingRecipeId.HasValue ? "Chỉnh sửa công thức" : "Thêm công thức mới")

+
+
+
+
+
+
+
+
Nguyên liệu
+ @for (var idx = 0; idx < _recipeIngredients.Count; idx++) + { + var i = idx; +
+ + + + + +
+ } +
+
+ + +
+ @if (_recipeFormMessage != null) {
@_recipeFormMessage
} +
+
+} +@if (!_recipes.Any()) +{ + @RenderEmpty("flask-conical", "#FF5C00", "Chưa có công thức", "Thêm công thức và nguyên liệu pha chế") +} +else +{ +
+ @foreach (var recipe in _recipes) + { + var isExpanded = _expandedRecipeId == recipe.Id; +
+
+
+
@recipe.Name
+
+ +
+
+
@recipe.PrepTimeMinutes phút chuẩn bị
+ @if (isExpanded && !string.IsNullOrEmpty(recipe.Instructions)) + { +
@recipe.Instructions
+ } +
+
+ } +
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + + // Recipes state + private List _recipes = new(); + private bool _showRecipeForm; + private Guid? _editingRecipeId; + private string _newRecipeName = ""; + private string _newRecipeInstructions = ""; + private int _newRecipePrepTime = 5; + private List<(string Name, string Unit, string Qty, decimal Quantity, decimal Cost)> _recipeIngredients = new(); + private Guid? _expandedRecipeId; + private string? _recipeFormMessage; + private bool _recipeFormSuccess; + private string? _errorMessage; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + _recipes = await DataService.GetRecipesAsync(ShopId); + } + + // ═══ RECIPE CRUD ═══ + private async Task SaveRecipe() + { + _recipeFormMessage = null; + if (string.IsNullOrWhiteSpace(_newRecipeName) || ShopId == Guid.Empty) + { + _recipeFormMessage = "Vui lòng nhập tên công thức."; _recipeFormSuccess = false; return; + } + try + { + var ingredients = _recipeIngredients + .Where(i => !string.IsNullOrWhiteSpace(i.Name)) + .Select(i => new PosDataService.RecipeIngredientRequest(i.Name, i.Quantity, i.Unit, i.Cost)) + .ToList(); + var req = new PosDataService.CreateRecipeRequest(ShopId, Guid.Empty, _newRecipeName, _newRecipeInstructions, _newRecipePrepTime, ingredients); + bool ok; + if (_editingRecipeId.HasValue) + ok = await DataService.UpdateRecipeAsync(_editingRecipeId.Value, req); + else + ok = await DataService.CreateRecipeAsync(req); + _recipeFormMessage = ok ? (_editingRecipeId.HasValue ? "Đã cập nhật công thức!" : "Đã thêm công thức!") : "Lỗi khi lưu công thức."; + _recipeFormSuccess = ok; + if (ok) { _showRecipeForm = false; _editingRecipeId = null; _recipes = await DataService.GetRecipesAsync(ShopId); } + } + catch (Exception ex) { _recipeFormMessage = $"Lỗi: {ex.Message}"; _recipeFormSuccess = false; } + } + + private async Task DeleteRecipeItem(Guid id) + { + try + { + await DataService.DeleteRecipeAsync(id); + _recipes = await DataService.GetRecipesAsync(ShopId); + } + catch (Exception ex) { _errorMessage = $"Không thể xóa công thức: {ex.Message}"; } + } + + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => + { +
+
+ +
+

@title

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + } +
+ }; + + private static string HexToRgb(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length != 6) return "0,0,0"; + return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}"; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor new file mode 100644 index 00000000..51de72b7 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor @@ -0,0 +1,135 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+
@ShopHelpers.FormatVND(_reportOrders.Sum(o => o.TotalAmount))Tổng doanh thu
+
@_reportOrders.CountTổng đơn hàng
+
@ShopHelpers.FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0)Giá trị TB / đơn
+
@_reportProducts.CountSản phẩm
+
+@* Revenue Report *@ +
+
+

Doanh thu theo kỳ

+
+ @foreach (var (label, val) in new[] { ("Ngày", "daily"), ("Tuần", "weekly"), ("Tháng", "monthly") }) + { + + } +
+
+
+ @if (_revenueReport.Any()) + { + + + + + + @foreach (var r in _revenueReport) + { + + + + + + } + +
KỲĐƠN HÀNGDOANH THU
@r.Period.ToString("dd/MM/yyyy")@r.OrderCount@ShopHelpers.FormatVND(r.Revenue)
+ } + else + { +
Nhấn Ngày / Tuần / Tháng để tải dữ liệu doanh thu.
+ } +
+
+@* ─── Top products from real order_items data ─── *@ +@if (_topProducts.Any()) +{ +
+

Top sản phẩm bán chạy

+
+ + + + + + + @{ var tpRank = 1; } + @foreach (var tp in _topProducts) + { + + + + + + + } +
#Tên SPĐã bánDoanh thu
@(tpRank++)@(tp.ProductName ?? "—")@tp.TotalSold@ShopHelpers.FormatVND(tp.TotalRevenue)
+
+
+} +@if (_reportOrders.Any()) +{ +
+

Đơn hàng gần nhất

+
+ + + + + + + @foreach (var o in _reportOrders.Take(20)) + { + + + + + + + } +
Mã đơnGiá trịTrạng tháiNgày tạo
@o.Id.ToString()[..8]@ShopHelpers.FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM/yyyy HH:mm")
+
+
+} +else +{ +
+
+ +
+

Chưa có dữ liệu báo cáo

+

Dữ liệu sẽ hiển thị khi có đơn hàng và hoạt động kinh doanh

+
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + + private List _reportOrders = new(); + private List _reportProducts = new(); + private List _topProducts = new(); + private List _revenueReport = new(); + private string _reportPeriod = "daily"; + + protected override async Task OnInitializedAsync() + { + var shopGuid = ShopId != Guid.Empty ? ShopId : (Guid?)null; + _reportOrders = await DataService.GetOrdersAsync(shopGuid); + _reportProducts = await DataService.GetAllProductsAsync(shopGuid); + _topProducts = await DataService.GetTopProductsAsync(shopGuid); + } + + private async Task LoadRevenueReport(string period) + { + _reportPeriod = period; + _revenueReport = await DataService.GetRevenueReportAsync(period, ShopId != Guid.Empty ? ShopId : null); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor new file mode 100644 index 00000000..9a9b0cec --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor @@ -0,0 +1,173 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +
+
+
@_resources.CountTổng
+
@_resources.Count(r => r.IsActive)Hoạt động
+
+ +
+@if (_showResourceForm) +{ +
+

@(_editingResourceId.HasValue ? "Chỉnh sửa" : "Thêm tài nguyên")

+
+
+
+
+ +
+
+
+
+ + +
+ @if (_resourceFormMessage != null) {
@_resourceFormMessage
} +
+
+} +@if (!_resources.Any()) +{ + @RenderEmpty("door-open", "#EC4899", "Chưa có tài nguyên", "Thêm phòng, giường, thiết bị cho cửa hàng") +} +else +{ +
+
+ + + + + + + + @foreach (var r in _resources) + { + + + + + + + + } +
TênLoạiSức chứaTrạng thái
@r.Name@(r.ResourceType ?? "—")@r.Capacity@(r.IsActive ? "Active" : "Inactive") +
+ + +
+
+
+
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + + // Resources CRUD state + private List _resources = new(); + private bool _showResourceForm; + private Guid? _editingResourceId; + private string _newResourceName = ""; + private string _newResourceType = "Room"; + private int _newResourceCapacity = 1; + private string? _resourceFormMessage; + private bool _resourceFormSuccess; + private string? _errorMessage; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + _resources = await DataService.GetResourcesAsync(ShopId); + } + + // ═══ RESOURCE CRUD ═══ + private void EditResource(PosDataService.ResourceInfo r) + { + _editingResourceId = r.Id; + _newResourceName = r.Name; + _newResourceType = r.ResourceType ?? "Room"; + _newResourceCapacity = r.Capacity; + _resourceFormMessage = null; + _showResourceForm = true; + } + + private async Task AddResource() + { + _resourceFormMessage = null; + if (string.IsNullOrWhiteSpace(_newResourceName) || ShopId == Guid.Empty) + { + _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return; + } + try + { + await DataService.CreateResourceAsync(new PosDataService.CreateResourceRequest(ShopId, _newResourceName, _newResourceType, _newResourceCapacity)); + _resourceFormMessage = $"Đã thêm '{_newResourceName}' thành công!"; _resourceFormSuccess = true; + _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1; + _resources = await DataService.GetResourcesAsync(ShopId); + } + catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; } + } + + private async Task SaveResource() + { + _resourceFormMessage = null; + if (string.IsNullOrWhiteSpace(_newResourceName) || ShopId == Guid.Empty || !_editingResourceId.HasValue) + { + _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return; + } + try + { + await DataService.UpdateResourceAsync(_editingResourceId.Value, new PosDataService.CreateResourceRequest(ShopId, _newResourceName, _newResourceType, _newResourceCapacity)); + _resourceFormMessage = $"Đã cập nhật '{_newResourceName}' thành công!"; _resourceFormSuccess = true; + _editingResourceId = null; + _resources = await DataService.GetResourcesAsync(ShopId); + } + catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; } + } + + private async Task DeleteResourceItem(Guid id) + { + try + { + await DataService.DeleteResourceAsync(id); + if (ShopId != Guid.Empty) _resources = await DataService.GetResourcesAsync(ShopId); + } + catch (Exception ex) { _errorMessage = $"Không thể xóa tài nguyên: {ex.Message}"; } + } + + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => + { +
+
+ +
+

@title

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + } +
+ }; + + private static string HexToRgb(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length != 6) return "0,0,0"; + return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}"; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor new file mode 100644 index 00000000..b24ef791 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor @@ -0,0 +1,234 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService +@inject NavigationManager Nav + +@if (SubSection == "schedule") +{ +
+
+
@_staffSchedules.Select(s => s.StaffId).Distinct().Count()NV có lịch
+
@_staffSchedules.CountCa làm việc
+
+ +
+ @if (_showScheduleForm) + { +
+

Thêm lịch làm việc

+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+ @if (_schedFormMessage != null) {
@_schedFormMessage
} +
+
+ } + @if (!_staffSchedules.Any()) + { +
+
+ +
+

Chưa có lịch làm việc

+

Thiết lập lịch ca cho nhân viên

+
+ } + else + { +
+

Lịch làm việc theo tuần

+
+ + + + + + + + + @foreach (var s in _staffSchedules.OrderBy(x => x.DayOfWeek).ThenBy(x => x.StartTime)) + { + var schedStaff = _staff.FirstOrDefault(st => st.Id == s.StaffId); + var schedStaffName = schedStaff != null && (!string.IsNullOrWhiteSpace(schedStaff.LastName) || !string.IsNullOrWhiteSpace(schedStaff.FirstName)) ? $"{schedStaff.LastName} {schedStaff.FirstName}".Trim() : (s.EmployeeCode ?? s.StaffId.ToString()[..8]); + + + + + + + + + } +
Nhân viênVai tròThứBắt đầuKết thúc
@schedStaffName@(s.Role ?? "—")@ShopHelpers.DayLabel(s.DayOfWeek)@s.StartTime@s.EndTime
+
+
+ } +} +else if (SubSection == "shifts") +{ +
+
@_staffSchedules.CountCa đã phân
+
@_staff.CountNhân viên
+
@_staffSchedules.Select(s => s.DayOfWeek).Distinct().Count()Ngày có ca
+
+ @* ─── Add Schedule Form ─── *@ +
+
+

Phân ca làm việc

+ +
+ @if (_showScheduleForm) + { +
+
+
+ +
+
+ +
+
+
+
+
+ + +
+ @if (_schedFormMessage != null) {
@_schedFormMessage
} +
+ } +
+ @* ─── Weekly Grid ─── *@ +
+

Lịch ca — Tuần

+
+ @if (!_staff.Any()) + { +
Chưa có nhân viên. Thêm nhân viên trong mục Nhân sự.
+ } + else + { + + + @foreach (var d in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" }) + { + + } + + @foreach (var emp in _staff) + { + + + @foreach (var dow in new[] { 1, 2, 3, 4, 5, 6, 0 }) + { + var sched = _staffSchedules.FirstOrDefault(s => s.StaffId == emp.Id && s.DayOfWeek == dow); + if (sched != null) + { + var isMorning = sched.StartTime?.CompareTo("12:00") < 0; + var bg = isMorning ? "rgba(59,130,246,0.12)" : "rgba(168,85,247,0.12)"; + var fg = isMorning ? "#3B82F6" : "#A855F7"; + var label = $"{sched.StartTime?[..5]}"; + + } + else + { + + } + } + + } +
Nhân viên@d
@(emp.EmployeeCode ?? emp.Id.ToString()[..8]) + @label + + + +
+ } +
+
+
+
Sáng (<12:00)
+
Chiều (≥12:00)
+
— = Nghỉ
+
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + [Parameter] public string SubSection { get; set; } = "schedule"; + + private List _staff = new(); + private List _staffSchedules = new(); + private bool _showScheduleForm; + private Guid _newSchedStaffId; + private string _newSchedStaffIdStr = ""; + private int _newSchedDay = 1; + private string _newSchedStart = "08:00"; + private string _newSchedEnd = "17:00"; + private string? _schedFormMessage; + private bool _schedFormSuccess; + + protected override async Task OnInitializedAsync() + { + _staffSchedules = await DataService.GetStaffSchedulesAsync(ShopId != Guid.Empty ? ShopId : null); + _staff = await DataService.GetStaffForShopAsync(ShopId); + } + + private async Task AddSchedule() + { + _schedFormMessage = null; + if (!Guid.TryParse(_newSchedStaffIdStr, out var staffId) || ShopId == Guid.Empty) + { + _schedFormMessage = "Vui lòng nhập đúng Staff ID."; _schedFormSuccess = false; return; + } + try + { + _newSchedStaffId = staffId; + await DataService.CreateScheduleAsync(new PosDataService.CreateScheduleRequest(ShopId, _newSchedStaffId, _newSchedDay, _newSchedStart, _newSchedEnd)); + _schedFormMessage = "Đã thêm lịch làm việc thành công!"; _schedFormSuccess = true; + _showScheduleForm = false; + _staffSchedules = await DataService.GetStaffSchedulesAsync(ShopId != Guid.Empty ? ShopId : null); + } + catch (Exception ex) { _schedFormMessage = $"Lỗi: {ex.Message}"; _schedFormSuccess = false; } + } + + private async Task DeleteScheduleItem(Guid id) + { + try + { + await DataService.DeleteScheduleAsync(id); + _staffSchedules = await DataService.GetStaffSchedulesAsync(ShopId != Guid.Empty ? ShopId : null); + } + catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa lịch làm việc: {ex.Message}"); } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor new file mode 100644 index 00000000..9877b098 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor @@ -0,0 +1,152 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +@* ─── Shop info (read-only) ─── *@ +
+

Thông tin cửa hàng

+
+
+
@(_shopName ?? "—")
+
@_verticalLabel
+
+
+
+@* ─── Opening hours + business days ─── *@ +
+

Giờ & ngày hoạt động

+
+
+
+ + +
+
+ + +
+
+
+ +
+ @foreach (var (day, code) in new[] { ("T2","Monday"),("T3","Tuesday"),("T4","Wednesday"),("T5","Thursday"),("T6","Friday"),("T7","Saturday"),("CN","Sunday") }) + { + var isOn = _settingsOpenDays.Contains(code); + + } +
+
+
+
+@* ─── Features config toggles ─── *@ +
+

Tính năng cửa hàng

+
+
+ @{ void RenderToggle(string label, bool isOn, Action setter) { +
+
+
+
+ @label +
; + } } + @{ RenderToggle("Quản lý tồn kho", _featHasInventory, v => _featHasInventory = v); } + @{ RenderToggle("Đặt lịch hẹn", _featHasBooking, v => _featHasBooking = v); } + @{ RenderToggle("Quản lý bàn", _featHasTables, v => _featHasTables = v); } + @{ RenderToggle("Hiển thị bếp", _featHasKitchen, v => _featHasKitchen = v); } + @{ RenderToggle("Vận chuyển", _featHasShipping, v => _featHasShipping = v); } + @{ RenderToggle("Giao hàng", _featHasDelivery, v => _featHasDelivery = v); } +
+
+
+@* ─── Save button ─── *@ +
+ + @if (_settingsMessage != null) + { + @_settingsMessage + } +
+ +@code { + [Parameter] public Guid ShopId { get; set; } + [Parameter] public string? ShopName { get; set; } + [Parameter] public string? VerticalLabel { get; set; } + + // Settings state + private PosDataService.ShopSettingsInfo? _shopSettings; + private string _settingsOpenTime = ""; + private string _settingsCloseTime = ""; + private List _settingsOpenDays = new(); + private bool _featHasInventory, _featHasBooking, _featHasTables; + private bool _featHasKitchen, _featHasShipping, _featHasDelivery; + private string? _settingsMessage; + private bool _settingsSuccess; + + private string? _shopName => ShopName; + private string _verticalLabel => VerticalLabel ?? ""; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + await LoadShopSettings(); + } + + private async Task LoadShopSettings() + { + if (ShopId == Guid.Empty) return; + try + { + _shopSettings = await DataService.GetShopSettingsAsync(ShopId); + if (_shopSettings != null) + { + _settingsOpenTime = _shopSettings.OpenTime ?? ""; + _settingsCloseTime = _shopSettings.CloseTime ?? ""; + _settingsOpenDays = _shopSettings.OpenDays ?? new(); + if (_shopSettings.Features != null) + { + _featHasInventory = _shopSettings.Features.HasInventory; + _featHasBooking = _shopSettings.Features.HasBooking; + _featHasTables = _shopSettings.Features.HasTables; + _featHasKitchen = _shopSettings.Features.HasKitchen; + _featHasShipping = _shopSettings.Features.HasShipping; + _featHasDelivery = _shopSettings.Features.HasDelivery; + } + } + } + catch { /* non-fatal */ } + } + + private async Task SaveShopSettings() + { + if (ShopId == Guid.Empty) return; + _settingsMessage = null; + var features = new PosDataService.ShopFeaturesInfo + { + HasInventory = _featHasInventory, HasBooking = _featHasBooking, HasTables = _featHasTables, + HasKitchen = _featHasKitchen, HasShipping = _featHasShipping, HasDelivery = _featHasDelivery + }; + var req = new PosDataService.UpdateShopSettingsRequest( + Features: features, + OpenTime: string.IsNullOrWhiteSpace(_settingsOpenTime) ? null : _settingsOpenTime, + CloseTime: string.IsNullOrWhiteSpace(_settingsCloseTime) ? null : _settingsCloseTime, + OpenDays: _settingsOpenDays.Any() ? _settingsOpenDays : null); + var ok = await DataService.UpdateShopSettingsAsync(ShopId, req); + _settingsSuccess = ok; + _settingsMessage = ok ? "Đã lưu thiết lập thành công!" : "Lỗi khi lưu thiết lập."; + StateHasChanged(); + } + + private void ToggleDay(string code) + { + if (_settingsOpenDays.Contains(code)) _settingsOpenDays.Remove(code); + else _settingsOpenDays.Add(code); + StateHasChanged(); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor new file mode 100644 index 00000000..1b0ff226 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor @@ -0,0 +1,288 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@using Microsoft.AspNetCore.Components.Forms +@inject PosDataService DataService + +
+

@(_staff.Count) nhân viên

+ +
+@if (_showStaffForm) +{ +
+

@(_editingStaffId.HasValue ? "Chỉnh sửa nhân viên" : "Thêm nhân viên mới")

+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+ + @if (_staffDocFrontPreview != null) + { + CCCD trước + } + +
+
+ + @if (_staffDocBackPreview != null) + { + CCCD sau + } + +
+
+
+ @if (!_editingStaffId.HasValue) + { +
+ + @if (_createStaffAccount) + { +
+
+
+ } +
+ } +
+ + +
+ @if (!string.IsNullOrEmpty(_staffFormMessage)) + { +
@_staffFormMessage
+ } +
+
+} +@if (!_staff.Any() && !_showStaffForm) +{ +
+
+ +
+

Chưa có nhân viên

+

Thêm nhân viên để quản lý cửa hàng

+ +
+} +else if (_staff.Any()) +{ +
+
@_staff.Count(s => s.Status == "Active")Đang hoạt động
+
@_staff.CountTổng nhân viên
+
+
+
+ + + + + + + + + @foreach (var s in _staff) + { + var staffDisplayName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : null; + + + + + + + + + } +
Nhân viênMã NVVai tròTrạng tháiSĐTHành động
@(staffDisplayName ?? s.EmployeeCode ?? s.Id.ToString()[..6])@(s.EmployeeCode ?? "—")@(s.Role ?? "—")@(s.Status ?? "—")@(s.Phone ?? s.Email ?? "—") +
+ + +
+
+
+
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + + // Staff data + private List _staff = new(); + // Staff form state + private bool _showStaffForm; + private Guid? _editingStaffId; + private string _newStaffCode = ""; + private string _newStaffRole = "Cashier"; + private string _newStaffPhone = ""; + private string _newStaffEmail = ""; + private string? _staffFormMessage; + private bool _staffFormSuccess; + private bool _createStaffAccount; + private string _newStaffFirstName = ""; + private string _newStaffLastName = ""; + private string _newStaffPassword = ""; + // Staff extended fields state + private string _newStaffAddress = ""; + // Image upload state for staff docs + private IBrowserFile? _staffDocFrontFile; + private string? _staffDocFrontPreview; + private IBrowserFile? _staffDocBackFile; + private string? _staffDocBackPreview; + // Merchant ID loaded from shop data + private Guid? _merchantId; + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + { + var shop = await DataService.GetShopByIdAsync(ShopId); + if (shop != null) + _merchantId = shop.MerchantId; + _staff = await DataService.GetStaffForShopAsync(ShopId); + } + } + + private async Task AddStaff() + { + _staffFormMessage = null; + if (!_merchantId.HasValue) + { + _staffFormMessage = "Không tìm thấy thông tin merchant. Vui lòng tải lại trang."; _staffFormSuccess = false; return; + } + if (string.IsNullOrWhiteSpace(_newStaffCode)) + { + _staffFormMessage = "Vui lòng nhập mã nhân viên."; _staffFormSuccess = false; return; + } + try + { + var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile); + var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile); + if (_createStaffAccount) + { + if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword)) + { + _staffFormMessage = "Vui lòng nhập đầy đủ email và mật khẩu."; _staffFormSuccess = false; return; + } + var (ok, err) = await DataService.InviteStaffWithAccountAsync(new PosDataService.InviteStaffWithAccountRequest( + _newStaffEmail, _newStaffPassword, _newStaffFirstName, _newStaffLastName, _newStaffRole, ShopId)); + if (!ok) { _staffFormMessage = err ?? "Lỗi tạo tài khoản IAM. Kiểm tra email/mật khẩu."; _staffFormSuccess = false; return; } + _staffFormMessage = $"Đã tạo tài khoản + mời NV '{_newStaffEmail}' thành công!"; _staffFormSuccess = true; + } + else + { + await DataService.CreateStaffAsync(new PosDataService.CreateStaffRequest( + _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole, + _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl)); + _staffFormMessage = $"Đã thêm NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true; + } + _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _createStaffAccount = false; + _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; + _staff = await DataService.GetStaffForShopAsync(ShopId); + } + catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } + } + + private void EditStaff(PosDataService.StaffInfo s) + { + _editingStaffId = s.Id; + _newStaffCode = s.EmployeeCode ?? ""; + _newStaffRole = s.Role ?? "Cashier"; + _newStaffPhone = s.Phone ?? ""; + _newStaffEmail = s.Email ?? ""; + _newStaffFirstName = s.FirstName ?? ""; + _newStaffLastName = s.LastName ?? ""; + _newStaffAddress = s.Address ?? ""; + _staffDocFrontFile = null; _staffDocBackFile = null; + _staffDocFrontPreview = s.DocumentFrontUrl; + _staffDocBackPreview = s.DocumentBackUrl; + _staffFormMessage = null; + _showStaffForm = true; + } + + private async Task SaveStaffEdit() + { + _staffFormMessage = null; + if (string.IsNullOrWhiteSpace(_newStaffCode) || !_merchantId.HasValue || !_editingStaffId.HasValue) + { + _staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return; + } + try + { + var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile) ?? (_staffDocFrontPreview?.StartsWith("data:") == true ? null : _staffDocFrontPreview); + var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile) ?? (_staffDocBackPreview?.StartsWith("data:") == true ? null : _staffDocBackPreview); + await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest( + _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole, + _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl)); + _staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true; + _editingStaffId = null; + _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; + _staff = await DataService.GetStaffForShopAsync(ShopId); + } + catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } + } + + private async Task DeleteStaffMember(Guid staffId) + { + try + { + await DataService.DeleteStaffAsync(staffId); + _staff = await DataService.GetStaffForShopAsync(ShopId); + } + catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa nhân viên: {ex.Message}"); } + } + + private async Task OnStaffDocFrontSelected(InputFileChangeEventArgs e) + { + _staffDocFrontFile = e.File; + var format = "image/png"; + var resized = await e.File.RequestImageFileAsync(format, 300, 200); + var buffer = new byte[resized.Size]; + await resized.OpenReadStream().ReadAsync(buffer); + _staffDocFrontPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; + StateHasChanged(); + } + + private async Task OnStaffDocBackSelected(InputFileChangeEventArgs e) + { + _staffDocBackFile = e.File; + var format = "image/png"; + var resized = await e.File.RequestImageFileAsync(format, 300, 200); + var buffer = new byte[resized.Size]; + await resized.OpenReadStream().ReadAsync(buffer); + _staffDocBackPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; + StateHasChanged(); + } + + private async Task UploadFileIfNeeded(IBrowserFile? file) + { + if (file == null) return null; + using var stream = file.OpenReadStream(maxAllowedSize: 10_485_760); + return await DataService.UploadImageAsync(stream, file.Name, file.ContentType); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor new file mode 100644 index 00000000..31ece1df --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor @@ -0,0 +1,323 @@ +@using WebClientTpos.Client.Services +@using WebClientTpos.Client.Pages.Admin.Shop +@inject PosDataService DataService + +@if (SubSection == "tables") +{ +
+
+ Trống: @_tables.Count(t => t.Status == "available") + Đang dùng: @_tables.Count(t => t.Status == "occupied") + Đã đặt: @_tables.Count(t => t.Status == "reserved") +
+ +
+ @if (_showTableForm) + { +
+

@(_editingTableId.HasValue ? "Chỉnh sửa bàn" : "Thêm bàn mới")

+
+
+
+
+
+
+
+ + +
+ @if (_tableFormMessage != null) {
@_tableFormMessage
} +
+
+ } + @if (!_tables.Any()) + { + @RenderEmpty("grid-3x3", "#F59E0B", "Chưa có bàn nào", "Thêm bàn để quản lý sơ đồ phục vụ") + } + else + { +
+ @foreach (var table in _tables) + { + var bgColor = table.Status switch { "available" => "rgba(34,197,94,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" }; + var borderColor = table.Status switch { "available" => "rgba(34,197,94,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" }; + var statusColor = table.Status switch { "available" => "#22C55E", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" }; + var statusText = table.Status switch { "available" => "Trống", "occupied" => "Đang dùng", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => table.Status }; +
+
+ + +
+
@table.TableNumber
+
@(table.Zone ?? "Chung") • @table.Capacity chỗ
+
+ + @statusText +
+ @if (table.SessionId.HasValue) + { +
+ @table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—") +
+ } +
+ } +
+ } +} +else if (SubSection == "rooms") +{ + @if (!_tables.Any()) + { + @RenderEmpty("door-open", "#8B5CF6", "Chưa có phòng nào", "Thêm phòng để quản lý Karaoke", "plus-circle", "Thêm phòng") + } + else + { +
+
+ Trống: @_tables.Count(t => t.Status == "available") + Đang hát: @_tables.Count(t => t.Status == "occupied") + Đã đặt: @_tables.Count(t => t.Status == "reserved") +
+
@_tables.Count phòng
+
+
+ @foreach (var room in _tables) + { + var bgColor = room.Status switch { "available" => "rgba(139,92,246,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" }; + var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" }; + var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" }; + var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status }; + var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) }; +
+
+
+ + Phòng @room.TableNumber +
+ @roomType.Item1 +
+
@(room.Zone ?? "Chung") • @room.Capacity chỗ
+
@ShopHelpers.FormatVND(roomType.Item3)/giờ
+
+ + @statusText +
+ @if (room.SessionId.HasValue) + { + var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow); + var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours)); + var bill = hours * roomType.Item3; +
+
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
+
+ @hours giờ + @ShopHelpers.FormatVND(bill) +
+
+ } +
+ } +
+ } +} +else if (SubSection == "zones") +{ + var tableZones = _tables.GroupBy(t => t.Zone ?? "Chung").Select(g => new { Name = g.Key, Count = g.Count() }).ToList(); + var allZones = tableZones.Select(z => z.Name).Union(_customZones).Distinct().OrderBy(z => z).ToList(); + var zoneGroups = allZones.Select((z, i) => new { Name = z, Count = tableZones.FirstOrDefault(tz => tz.Name == z)?.Count ?? 0, Color = _zoneColors[i % _zoneColors.Length], Icon = _zoneIcons[i % _zoneIcons.Length] }).ToList(); +
+
+

Quản lý khu vực

+ +
+
+ @if (_showZoneForm) + { +
+
@(_editingZoneOriginalName != null ? "Đổi tên khu vực" : "Thêm khu vực mới")
+
+ + + +
+ @if (_zoneFormMessage != null) {
@_zoneFormMessage
} +
+ } + @if (!zoneGroups.Any()) + { + @RenderEmpty("map-pin", "#F59E0B", "Chưa có khu vực nào", "Nhấn 'Thêm khu vực' ở trên để tạo khu vực đầu tiên", "", "", "") + } + else + { +
+ @foreach (var zone in zoneGroups) + { + var rgbVal = zone.Color switch { "#3B82F6" => "59,130,246", "#A855F7" => "168,85,247", "#22C55E" => "34,197,94", "#F59E0B" => "245,158,11", "#EC4899" => "236,72,153", _ => "99,102,241" }; +
+
+
+ +
+
+
@zone.Name
+
@zone.Count bàn
+
+
+
+ Đang hoạt động +
+
+ +
+
+ } +
+ } +
+
+} + +@code { + [Parameter] public Guid ShopId { get; set; } + [Parameter] public string SubSection { get; set; } = "tables"; + + // Tables state + private List _tables = new(); + // Table form state + private bool _showTableForm; + private Guid? _editingTableId; + private string _newTableNumber = ""; + private int _newTableCapacity = 4; + private string _newTableZone = ""; + private string? _tableFormMessage; + private bool _tableFormSuccess; + // Zone form state + private bool _showZoneForm; + private string _newZoneName = ""; + private string? _editingZoneOriginalName; + private string? _zoneFormMessage; + private bool _zoneFormSuccess; + private readonly List _customZones = new(); + private static readonly string[] _zoneColors = { "#3B82F6", "#A855F7", "#22C55E", "#F59E0B", "#EC4899", "#6366F1" }; + private static readonly string[] _zoneIcons = { "building", "crown", "trees", "wine", "coffee", "map-pin" }; + private List AllZoneNames => _tables.Select(t => t.Zone ?? "Chung").Distinct() + .Union(_customZones).Distinct().OrderBy(z => z).ToList(); + + protected override async Task OnInitializedAsync() + { + if (ShopId != Guid.Empty) + _tables = await DataService.GetTablesAsync(ShopId); + } + + // ═══ TABLE CRUD ═══ + private void EditTable(PosDataService.TableInfo table) + { + _editingTableId = table.Id; + _newTableNumber = table.TableNumber; + _newTableCapacity = table.Capacity; + _newTableZone = table.Zone ?? ""; + _tableFormMessage = null; + _showTableForm = true; + } + + private async Task AddTable() + { + _tableFormMessage = null; + if (string.IsNullOrWhiteSpace(_newTableNumber) || ShopId == Guid.Empty) + { + _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return; + } + try + { + await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone)); + _tableFormMessage = $"Đã thêm bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; + _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; + if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId); + } + catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; } + } + + private async Task SaveTable() + { + _tableFormMessage = null; + if (string.IsNullOrWhiteSpace(_newTableNumber) || ShopId == Guid.Empty || !_editingTableId.HasValue) + { + _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return; + } + try + { + await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone)); + _tableFormMessage = $"Đã cập nhật bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; + _editingTableId = null; + if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId); + } + catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; } + } + + private async Task DeleteTableItem(Guid id) + { + try + { + await DataService.DeleteTableAsync(id); + if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId); + } + catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa bàn: {ex.Message}"); } + } + + // ═══ ZONE MANAGEMENT ═══ + private async Task SaveZone() + { + _zoneFormMessage = null; + if (string.IsNullOrWhiteSpace(_newZoneName) || ShopId == Guid.Empty) + { + _zoneFormMessage = "Vui lòng nhập tên khu vực."; _zoneFormSuccess = false; return; + } + if (_editingZoneOriginalName != null) + { + // Rename: update all tables in old zone to new zone name + var tablesInZone = _tables.Where(t => (t.Zone ?? "Chung") == _editingZoneOriginalName).ToList(); + try + { + foreach (var t in tablesInZone) + await DataService.UpdateTableAsync(t.Id, new PosDataService.CreateTableRequest(ShopId, t.TableNumber, t.Capacity, _newZoneName.Trim())); + _tables = await DataService.GetTablesAsync(ShopId); + _zoneFormMessage = $"Đã đổi tên '{_editingZoneOriginalName}' → '{_newZoneName.Trim()}'"; _zoneFormSuccess = true; + _editingZoneOriginalName = null; _newZoneName = ""; _showZoneForm = false; + } + catch (Exception ex) { _zoneFormMessage = $"Lỗi: {ex.Message}"; _zoneFormSuccess = false; } + } + else + { + var zoneName = _newZoneName.Trim(); + if (!_customZones.Contains(zoneName) && !_tables.Any(t => (t.Zone ?? "Chung") == zoneName)) + _customZones.Add(zoneName); + _zoneFormMessage = $"Đã thêm khu vực '{zoneName}'."; _zoneFormSuccess = true; + _newTableZone = zoneName; + _newZoneName = ""; + _showZoneForm = false; + } + } + + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder => + { +
+
+ +
+

@title

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + } +
+ }; +}