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 574f276a..5cad18cc 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 @@ -204,7 +204,7 @@ - @@ -235,6 +235,15 @@ } +
+
+ @if (_productImagePreview != null) + { + Preview + } + +
+
@@ -264,7 +273,14 @@
-
+ @if (!string.IsNullOrWhiteSpace(p.ImageUrl)) + { + @p.Name + } + else + { +
+ }
@p.Name
@(p.CategoryName ?? "—")
@typeLabel @@ -334,7 +350,7 @@

Danh mục (@_categories.Count)

-
@@ -342,14 +358,20 @@ {

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

-
+
-
- - +
+
+ @if (_categoryImagePreview != null) { Preview } + +
+
+ + +
@if (_categoryFormMessage != null) {
@_categoryFormMessage
}
} @@ -735,7 +757,7 @@ case "staff":

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

- @@ -746,6 +768,8 @@

@(_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) { @@ -767,9 +813,7 @@ @if (_createStaffAccount) { -
-
-
+
} @@ -808,6 +852,7 @@
+ @@ -816,8 +861,10 @@ @foreach (var s in _staff) { + var staffDisplayName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : null; - + + @@ -837,6 +884,156 @@ // ═══ CUSTOMERS + MEMBERSHIP LEVELS ═══ case "customers": +
+ @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
+ } +
+ + +
+
+
+ } +
+
+
Nhân viên Mã NV Vai trò Trạng thái
@(s.EmployeeCode ?? s.Id.ToString()[..6])@(staffDisplayName ?? s.EmployeeCode ?? s.Id.ToString()[..6])@(s.EmployeeCode ?? "—") @(s.Role ?? "—") @(s.Status ?? "—") @(s.Phone ?? s.Email ?? "—")
+ + + + + + + + + @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

@@ -985,6 +1182,7 @@
} + } break; // ═══ TABLES (Restaurant) ═══ @@ -1562,8 +1760,22 @@

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

-
-
+
+ +
+
+ +
@@ -1594,8 +1806,10 @@ @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]); - @(s.EmployeeCode ?? s.StaffId.ToString()[..8]) + @schedStaffName @(s.Role ?? "—") @DayLabel(s.DayOfWeek) @s.StartTime @@ -1858,7 +2072,7 @@
- @foreach (var (day, code) in new[] { ("T2","Mon"),("T3","Tue"),("T4","Wed"),("T5","Thu"),("T6","Fri"),("T7","Sat"),("CN","Sun") }) + @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 JSON ─── *@ + @* ─── Features config toggles ─── *@
-

Cấu hình tính năng (JSON)

+

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

- -

Nhập JSON để bật/tắt tính năng cho cửa hàng này.

+
+ @{ 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 ─── *@ @@ -2457,10 +2683,43 @@ private PosDataService.ShopSettingsInfo? _shopSettings; private string _settingsOpenTime = ""; private string _settingsCloseTime = ""; - private string _settingsOpenDays = ""; - private string _settingsFeaturesConfig = "{}"; + 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 @@ -2642,10 +2901,18 @@ _shopSettings = await DataService.GetShopSettingsAsync(_shopGuid.Value); if (_shopSettings != null) { - _settingsOpenTime = _shopSettings.OpenTime ?? ""; - _settingsCloseTime = _shopSettings.CloseTime ?? ""; - _settingsOpenDays = _shopSettings.OpenDays ?? ""; - _settingsFeaturesConfig= string.IsNullOrWhiteSpace(_shopSettings.FeaturesConfig) ? "{}" : _shopSettings.FeaturesConfig; + _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 */ } @@ -2655,11 +2922,16 @@ { 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( - FeaturesConfig: _settingsFeaturesConfig, - OpenTime: string.IsNullOrWhiteSpace(_settingsOpenTime) ? null : _settingsOpenTime, - CloseTime: string.IsNullOrWhiteSpace(_settingsCloseTime) ? null : _settingsCloseTime, - OpenDays: _settingsOpenDays); + 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."; @@ -2668,10 +2940,8 @@ private void ToggleDay(string code) { - var days = _settingsOpenDays.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); - if (days.Contains(code)) days.Remove(code); - else days.Add(code); - _settingsOpenDays = string.Join(",", days); + if (_settingsOpenDays.Contains(code)) _settingsOpenDays.Remove(code); + else _settingsOpenDays.Add(code); StateHasChanged(); } @@ -2800,10 +3070,11 @@ 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, null, null, catId)); + _shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl, null, catId)); _formMessage = $"Đã thêm '{_newProductName}' thành công!"; _formSuccess = true; - _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductCategoryId = ""; + _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductCategoryId = ""; _productImageFile = null; _productImagePreview = null; _products = await DataService.GetAllProductsAsync(_shopGuid); } catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; } @@ -2827,6 +3098,8 @@ _newProductType = p.Type ?? "PreparedFood"; _newProductDesc = p.Description ?? ""; _newProductCategoryId = p.CategoryId?.ToString() ?? ""; + _productImageFile = null; + _productImagePreview = p.ImageUrl; _formMessage = null; _showProductForm = true; } @@ -2841,8 +3114,9 @@ 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, null, null, catId)); + _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); @@ -2863,12 +3137,13 @@ } try { + var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile); + var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile); if (_createStaffAccount) { - if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword) - || string.IsNullOrWhiteSpace(_newStaffFirstName) || string.IsNullOrWhiteSpace(_newStaffLastName)) + if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword)) { - _staffFormMessage = "Vui lòng nhập đầy đủ email, mật khẩu, họ và tên."; _staffFormSuccess = false; return; + _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)); @@ -2878,10 +3153,12 @@ else { await DataService.CreateStaffAsync(new PosDataService.CreateStaffRequest( - _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole)); + _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 = ""; _createStaffAccount = false; + _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _createStaffAccount = false; + _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; _staff = await DataService.GetStaffAsync(); } catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } @@ -2894,6 +3171,12 @@ _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; } @@ -2907,10 +3190,14 @@ } 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)); + _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.GetStaffAsync(); } catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } @@ -2937,7 +3224,8 @@ private async Task SaveCategory() { if (string.IsNullOrWhiteSpace(_newCategoryName)) { _categoryFormMessage = "Tên danh mục không được trống."; _categoryFormSuccess = false; return; } - var req = new PosDataService.AdminCreateCategoryRequest(_shopGuid ?? Guid.Empty, _newCategoryName, _newCategoryDesc, _newCategoryOrder); + 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); @@ -2945,9 +3233,9 @@ 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; _categories = await DataService.GetAllCategoriesAsync(_shopGuid); } + 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; _showCategoryForm = true; _categoryFormMessage = null; } + 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 ═══ @@ -3079,6 +3367,112 @@ _members = await DataService.GetMembersAsync(); } + // ═══ 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; _memberLevels = await DataService.GetMembershipLevelsAsync(); } + } + 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); + _memberLevels = await DataService.GetMembershipLevelsAsync(); + } + + // ═══ 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); + _members = await DataService.GetMembersAsync(); + } + 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) { diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index cdb4e8d4..93c251d8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -139,10 +139,11 @@ public class PosDataService { public string? Category => CategoryName; } - public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder); + public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder, string? ImageUrl = null); public record TableInfo(Guid Id, string TableNumber, int Capacity, string? Zone, string Status, Guid? SessionId, int? GuestCount, DateTime? StartedAt); public record AppointmentInfo(Guid Id, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid ServiceId, DateTime StartTime, DateTime EndTime, string Status, string? ResourceName); - public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName); + public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName, + string? FirstName = null, string? LastName = null, string? Address = null, string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null); public async Task> GetShopsAsync() => await GetListFromApiAsync("api/bff/shops"); @@ -172,7 +173,7 @@ public class PosDataService public record AdminProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description, string? ImageUrl, bool IsActive, string? Type, Guid ShopId, DateTime CreatedAt, string? CategoryName, Guid? CategoryId = null); public record AdminCategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder, - Guid ShopId, Guid? ParentId, bool IsActive); + Guid ShopId, Guid? ParentId, bool IsActive, string? ImageUrl = null); public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price, string? Type, string? Sku, string? ImageUrl, Guid? CategoryId = null); @@ -234,7 +235,9 @@ public class PosDataService // ═══ STAFF CREATE ═══ - public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role); + public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role, + string? FirstName = null, string? LastName = null, string? Address = null, + string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null); public async Task CreateStaffAsync(CreateStaffRequest req) { @@ -436,11 +439,95 @@ public class PosDataService // ═══ MEMBERSHIP LEVELS ═══ - public record LevelDefinitionInfo(Guid Id, int Level, string Name, int MinExp, int MaxExp, int MemberCount); + public record LevelDefinitionInfo(Guid Id, int LevelNumber, string Name, int RequiredExp, + string? Description = null, string? BadgeColor = null, bool IsActive = true, int MemberCount = 0, + int MinExp = 0, int MaxExp = 0) + { + public int Level => LevelNumber; + }; public async Task> GetMembershipLevelsAsync() => await GetListFromApiAsync("api/bff/membership/levels"); + // ═══ LEVEL DEFINITION CRUD ═══ + + public record CreateLevelRequest(int LevelNumber, string Name, int RequiredExp, string? Description, string? BadgeColor); + + public async Task<(bool Ok, string? Error)> CreateLevelAsync(CreateLevelRequest req) + { + AttachToken(); + var resp = await _http.PostAsJsonAsync("api/bff/membership/levels", req, _writeOptions); + if (resp.IsSuccessStatusCode) return (true, null); + return (false, await TryExtractError(resp)); + } + + public async Task<(bool Ok, string? Error)> UpdateLevelAsync(Guid levelId, CreateLevelRequest req) + { + AttachToken(); + var resp = await _http.PutAsJsonAsync($"api/bff/membership/levels/{levelId}", req, _writeOptions); + if (resp.IsSuccessStatusCode) return (true, null); + return (false, await TryExtractError(resp)); + } + + public async Task DeleteLevelAsync(Guid levelId) + { + AttachToken(); + var resp = await _http.DeleteAsync($"api/bff/membership/levels/{levelId}"); + return resp.IsSuccessStatusCode; + } + + // ═══ EXP MANAGEMENT ═══ + + public record AddExpRequest(int Points, int SourceId, string? ReferenceId); + public record AddExpResult(Guid MemberId, int PointsAdded, int CurrentExp, int TotalExpEarned, + int PreviousLevel, int CurrentLevel, bool LeveledUp); + public record MemberProgressInfo(Guid MemberId, int CurrentLevel, string? CurrentLevelName, + int CurrentExp, int TotalExpEarned, int ExpToNextLevel, int ProgressPercent, + int? NextLevel, string? NextLevelName, string? BadgeColor); + public record ExpTransactionInfo(Guid Id, int Points, string? Source, int SourceId, + string? ReferenceId, int LevelAtTime, DateTime CreatedAt); + + public async Task AddExperienceAsync(Guid memberId, AddExpRequest req) + { + AttachToken(); + var resp = await _http.PostAsJsonAsync($"api/bff/members/{memberId}/experience", req, _writeOptions); + if (resp.IsSuccessStatusCode) + return await resp.Content.ReadFromJsonAsync(_jsonOptions); + return null; + } + + public async Task GetMemberProgressAsync(Guid memberId) + => await GetObjectFromApiAsync($"api/bff/members/{memberId}/progress"); + + public async Task> GetExperienceHistoryAsync(Guid memberId) + => await GetListFromApiAsync($"api/bff/members/{memberId}/experience"); + + // ═══ FILE UPLOAD (Storage Service) ═══ + + public async Task UploadImageAsync(Stream fileStream, string fileName, string contentType) + { + AttachToken(); + using var content = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileStream); + streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + content.Add(streamContent, "file", fileName); + var resp = await _http.PostAsync("api/bff/files/upload?accessLevel=public", content); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + // EN: Try to extract download URL or file ID from response + if (doc.RootElement.TryGetProperty("downloadUrl", out var url)) + return url.GetString(); + if (doc.RootElement.TryGetProperty("data", out var data)) + { + if (data.TryGetProperty("downloadUrl", out var dUrl)) return dUrl.GetString(); + if (data.TryGetProperty("fileId", out var fId)) return $"api/bff/files/{fId.GetString()}/download"; + } + if (doc.RootElement.TryGetProperty("fileId", out var fileId)) + return $"api/bff/files/{fileId.GetString()}/download"; + return null; + } + // ═══ SHOP STATS (aggregated per-shop) ═══ public record ShopStatsInfo(Guid ShopId, int ProductCount, int OrderCount, int StaffCount, decimal Revenue); @@ -512,7 +599,7 @@ public class PosDataService // EN: Category create/update request DTO // VI: DTO tạo/cập nhật danh mục - public record AdminCreateCategoryRequest(Guid ShopId, string Name, string? Description, int DisplayOrder); + public record AdminCreateCategoryRequest(Guid ShopId, string Name, string? Description, int DisplayOrder, string? ImageUrl = null); public async Task CreateCategoryAsync(AdminCreateCategoryRequest req) { @@ -588,8 +675,24 @@ public class PosDataService // ═══ SHOP SETTINGS ═══ - public record ShopSettingsInfo(string? FeaturesConfig, string? OpenTime, string? CloseTime, string? OpenDays); - public record UpdateShopSettingsRequest(string? FeaturesConfig, string? OpenTime, string? CloseTime, string? OpenDays); + public record ShopFeaturesInfo + { + public bool HasInventory { get; init; } + public bool HasBooking { get; init; } + public bool HasTables { get; init; } + public bool HasKitchen { get; init; } + public bool HasShipping { get; init; } + public bool HasDelivery { get; init; } + } + public record ShopSettingsInfo + { + public Guid ShopId { get; init; } + public ShopFeaturesInfo? Features { get; init; } + public string? OpenTime { get; init; } + public string? CloseTime { get; init; } + public List? OpenDays { get; init; } + } + public record UpdateShopSettingsRequest(ShopFeaturesInfo? Features, string? OpenTime, string? CloseTime, List? OpenDays); public async Task GetShopSettingsAsync(Guid shopId) => await GetObjectFromApiAsync($"api/bff/shops/{shopId}/settings"); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/MembershipController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/MembershipController.cs index fc9b2fb0..c29f7cf4 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/MembershipController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/MembershipController.cs @@ -81,4 +81,56 @@ public class MembershipController : ControllerBase [HttpGet("membership/levels")] public Task GetMembershipLevels() => _membership.GetAsync("/api/v1/levels").ProxyAsync(); + + // ═══ LEVEL CRUD ═══ + + /// + /// EN: Create a membership level. + /// VI: Tạo cấp bậc membership. + /// + [HttpPost("membership/levels")] + public Task CreateLevel([FromBody] JsonElement body) => + _membership.PostAsJsonAsync("/api/v1/levels", body).ProxyAsync(); + + /// + /// EN: Update a membership level. + /// VI: Cập nhật cấp bậc membership. + /// + [HttpPut("membership/levels/{id:guid}")] + public Task UpdateLevel(Guid id, [FromBody] JsonElement body) => + _membership.PutAsJsonAsync($"/api/v1/levels/{id}", body).ProxyAsync(); + + /// + /// EN: Deactivate a membership level. + /// VI: Vô hiệu hóa cấp bậc membership. + /// + [HttpDelete("membership/levels/{id:guid}")] + public Task DeleteLevel(Guid id) => + _membership.DeleteAsync($"/api/v1/levels/{id}").ProxyAsync(); + + // ═══ EXPERIENCE MANAGEMENT ═══ + + /// + /// EN: Add experience points to a member. + /// VI: Cộng điểm kinh nghiệm cho thành viên. + /// + [HttpPost("members/{memberId:guid}/experience")] + public Task AddExperience(Guid memberId, [FromBody] JsonElement body) => + _membership.PostAsJsonAsync($"/api/v1/members/{memberId}/experience", body).ProxyAsync(); + + /// + /// EN: Get member level progress. + /// VI: Lấy tiến trình cấp bậc thành viên. + /// + [HttpGet("members/{memberId:guid}/progress")] + public Task GetMemberProgress(Guid memberId) => + _membership.GetAsync($"/api/v1/members/{memberId}/progress").ProxyAsync(); + + /// + /// EN: Get member experience history. + /// VI: Lấy lịch sử điểm kinh nghiệm thành viên. + /// + [HttpGet("members/{memberId:guid}/experience")] + public Task GetExperienceHistory(Guid memberId, [FromQuery] int pageSize = 20) => + _membership.GetAsync($"/api/v1/members/{memberId}/experience?pageSize={pageSize}").ProxyAsync(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs index b3334958..dae556dc 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs @@ -85,7 +85,9 @@ public class StaffController : ControllerBase { email = iamPayload.email, role = body.TryGetProperty("role", out var r) ? r.GetString() : "Cashier", - shopId = body.TryGetProperty("shopId", out var s) ? s.GetString() : null as string + shopId = body.TryGetProperty("shopId", out var s) ? s.GetString() : null as string, + firstName = iamPayload.firstName, + lastName = iamPayload.lastName }; return await _merchant.PostAsJsonAsync("/api/v1/merchants/me/staff/invite", invitePayload).ProxyAsync(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StorageController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StorageController.cs new file mode 100644 index 00000000..82ea0733 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StorageController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using WebClientTpos.Server.Infrastructure; + +namespace WebClientTpos.Server.Controllers; + +/// +/// EN: Storage controller — proxies to StorageService for file uploads/downloads (MinIO). +/// VI: Controller lưu trữ — proxy đến StorageService cho upload/download file (MinIO). +/// +[ApiController] +[Route("api/bff")] +public class StorageController : ControllerBase +{ + private readonly HttpClient _storage; + + public StorageController(IHttpClientFactory httpClientFactory) + { + _storage = httpClientFactory.CreateClient("StorageService"); + } + + /// + /// EN: Upload a file (image) to storage. + /// VI: Upload file (ảnh) lên storage. + /// + [HttpPost("files/upload")] + [RequestSizeLimit(10_485_760)] // 10MB + public async Task UploadFile(IFormFile file, [FromQuery] string? accessLevel = "public") + { + using var content = new MultipartFormDataContent(); + using var stream = file.OpenReadStream(); + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType); + content.Add(streamContent, "file", file.FileName); + + var response = await _storage.PostAsync($"/api/v1/files/upload?accessLevel={accessLevel}", content); + var body = await response.Content.ReadAsStringAsync(); + return new ContentResult + { + StatusCode = (int)response.StatusCode, + Content = body, + ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json" + }; + } + + /// + /// EN: Download a file by ID. + /// VI: Tải file theo ID. + /// + [HttpGet("files/{fileId:guid}/download")] + public async Task DownloadFile(Guid fileId) + { + var response = await _storage.GetAsync($"/api/v1/files/{fileId}/download"); + if (!response.IsSuccessStatusCode) + { + var errBody = await response.Content.ReadAsStringAsync(); + return new ContentResult + { + StatusCode = (int)response.StatusCode, + Content = errBody, + ContentType = "application/json" + }; + } + var fileStream = await response.Content.ReadAsStreamAsync(); + var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream"; + var fileName = response.Content.Headers.ContentDisposition?.FileName?.Trim('"') ?? "file"; + return File(fileStream, contentType, fileName); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs index a69298a6..f7b88e48 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs @@ -117,6 +117,7 @@ AddServiceClient("PromotionService", "PromotionService__BaseUrl", "http://loca AddServiceClient("BookingService", "BookingService__BaseUrl", "http://localhost:5020"); AddServiceClient("FnbEngine", "FnbEngine__BaseUrl", "http://localhost:5019"); AddServiceClient("IamService", "IamService__BaseUrl", "http://localhost:5001"); +AddServiceClient("StorageService", "StorageService__BaseUrl", "http://localhost:5002"); var app = builder.Build(); diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommand.cs index 709d00e5..b18bd79f 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommand.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommand.cs @@ -40,4 +40,10 @@ public record CreateCategoryCommand : IRequest /// VI: Thứ tự hiển thị. /// public int DisplayOrder { get; init; } + + /// + /// EN: Category image URL. + /// VI: URL hình ảnh danh mục. + /// + public string? ImageUrl { get; init; } } diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommandHandler.cs index 03bc124c..3613e6be 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommandHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateCategoryCommandHandler.cs @@ -31,8 +31,15 @@ public class CreateCategoryCommandHandler : IRequestHandler +/// EN: Command to delete a category. +/// VI: Command để xóa danh mục. +/// +public record DeleteCategoryCommand : IRequest +{ + /// + /// EN: Category ID to delete. + /// VI: ID danh mục cần xóa. + /// + public Guid CategoryId { get; init; } + + public DeleteCategoryCommand(Guid categoryId) + { + CategoryId = categoryId; + } +} diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteCategoryCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteCategoryCommandHandler.cs new file mode 100644 index 00000000..d713221d --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteCategoryCommandHandler.cs @@ -0,0 +1,45 @@ +// EN: Handler for DeleteCategoryCommand. +// VI: Handler cho DeleteCategoryCommand. + +using MediatR; +using Microsoft.EntityFrameworkCore; +using CatalogService.Infrastructure; + +namespace CatalogService.API.Application.Commands; + +/// +/// EN: Handler for deleting a category. +/// VI: Handler xóa danh mục. +/// +public class DeleteCategoryCommandHandler : IRequestHandler +{ + private readonly CatalogContext _context; + + public DeleteCategoryCommandHandler(CatalogContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task Handle(DeleteCategoryCommand request, CancellationToken cancellationToken) + { + // EN: Retrieve category + // VI: Lấy danh mục + var category = await _context.Categories + .FirstOrDefaultAsync(c => c.Id == request.CategoryId, cancellationToken); + + if (category == null) + { + return false; + } + + // EN: Deactivate category instead of hard delete + // VI: Vô hiệu hóa danh mục thay vì xóa cứng + category.Deactivate(); + + // EN: Save changes + // VI: Lưu thay đổi + await _context.SaveChangesAsync(cancellationToken); + + return true; + } +} diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommand.cs new file mode 100644 index 00000000..e3a9adaf --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommand.cs @@ -0,0 +1,12 @@ +// EN: Command to update an existing category. +// VI: Command để cập nhật danh mục hiện tại. + +using MediatR; + +namespace CatalogService.API.Application.Commands; + +/// +/// EN: Command to update category information. +/// VI: Command để cập nhật thông tin danh mục. +/// +public record UpdateCategoryCommand(Guid CategoryId, string Name, string? Description, int DisplayOrder, string? ImageUrl) : IRequest; diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommandHandler.cs new file mode 100644 index 00000000..fcef0460 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateCategoryCommandHandler.cs @@ -0,0 +1,52 @@ +// EN: Handler for UpdateCategoryCommand. +// VI: Handler cho UpdateCategoryCommand. + +using MediatR; +using Microsoft.EntityFrameworkCore; +using CatalogService.Infrastructure; + +namespace CatalogService.API.Application.Commands; + +/// +/// EN: Handler for updating a category. +/// VI: Handler cập nhật danh mục. +/// +public class UpdateCategoryCommandHandler : IRequestHandler +{ + private readonly CatalogContext _context; + + public UpdateCategoryCommandHandler(CatalogContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task Handle(UpdateCategoryCommand request, CancellationToken cancellationToken) + { + // EN: Retrieve category + // VI: Lấy danh mục + var category = await _context.Categories + .FirstOrDefaultAsync(c => c.Id == request.CategoryId, cancellationToken); + + if (category == null) + { + return false; + } + + // EN: Update basic information + // VI: Cập nhật thông tin cơ bản + category.UpdateInfo(request.Name, request.Description, request.DisplayOrder); + + // EN: Update image if provided + // VI: Cập nhật hình ảnh nếu được cung cấp + if (request.ImageUrl != null) + { + category.UpdateImage(request.ImageUrl); + } + + // EN: Save changes + // VI: Lưu thay đổi + await _context.SaveChangesAsync(cancellationToken); + + return true; + } +} diff --git a/services/catalog-service-net/src/CatalogService.API/Application/DTOs/CategoryDto.cs b/services/catalog-service-net/src/CatalogService.API/Application/DTOs/CategoryDto.cs index ed0143a2..9dfea0d4 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/DTOs/CategoryDto.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/DTOs/CategoryDto.cs @@ -45,6 +45,12 @@ public record CategoryDto /// public int DisplayOrder { get; init; } + /// + /// EN: Category image URL. + /// VI: URL hình ảnh danh mục. + /// + public string? ImageUrl { get; init; } + /// /// EN: Is category active. /// VI: Danh mục có đang hoạt động không. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetCategoriesQueryHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetCategoriesQueryHandler.cs index ba956f34..5c8693c1 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetCategoriesQueryHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetCategoriesQueryHandler.cs @@ -50,6 +50,7 @@ public class GetCategoriesQueryHandler : IRequestHandler + /// EN: Update an existing category. + /// VI: Cập nhật danh mục hiện tại. + /// + [HttpPut("{categoryId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateCategory( + Guid categoryId, + [FromBody] UpdateCategoryCommand command, + CancellationToken cancellationToken = default) + { + var result = await _mediator.Send(command with { CategoryId = categoryId }, cancellationToken); + return result ? Ok() : NotFound(); + } + + /// + /// EN: Delete (deactivate) a category. + /// VI: Xóa (vô hiệu hóa) danh mục. + /// + [HttpDelete("{categoryId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteCategory( + Guid categoryId, + CancellationToken cancellationToken = default) + { + var result = await _mediator.Send(new DeleteCategoryCommand(categoryId), cancellationToken); + return result ? Ok() : NotFound(); + } } diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs index 6ae47d39..9da32d9f 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs @@ -17,6 +17,7 @@ public class Category : Entity private string? _description; private Guid? _parentId; private int _displayOrder; + private string? _imageUrl; private bool _isActive; private DateTime _createdAt; private DateTime? _updatedAt; @@ -45,6 +46,12 @@ public class Category : Entity /// public Guid? ParentId => _parentId; + /// + /// EN: Category image URL. + /// VI: URL hình ảnh danh mục. + /// + public string? ImageUrl => _imageUrl; + /// /// EN: Display order. /// VI: Thứ tự hiển thị. @@ -86,7 +93,8 @@ public class Category : Entity string name, string? description = null, Guid? parentId = null, - int displayOrder = 0) + int displayOrder = 0, + string? imageUrl = null) { if (shopId == Guid.Empty) throw new DomainException("Shop ID cannot be empty"); @@ -99,6 +107,7 @@ public class Category : Entity _description = description?.Trim(); _parentId = parentId; _displayOrder = displayOrder; + _imageUrl = imageUrl; _isActive = true; _createdAt = DateTime.UtcNow; } @@ -118,6 +127,16 @@ public class Category : Entity _updatedAt = DateTime.UtcNow; } + /// + /// EN: Update category image. + /// VI: Cập nhật hình ảnh danh mục. + /// + public void UpdateImage(string? imageUrl) + { + _imageUrl = imageUrl; + _updatedAt = DateTime.UtcNow; + } + /// /// EN: Update parent category. /// VI: Cập nhật danh mục cha. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs index 4d4cc210..1eae2b19 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs @@ -43,6 +43,11 @@ public class CategoryEntityTypeConfiguration : IEntityTypeConfiguration c.ImageUrl) + .HasField("_imageUrl") + .HasColumnName("image_url") + .HasMaxLength(500); + builder.Property(c => c.DisplayOrder) .HasField("_displayOrder") .HasColumnName("display_order") diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs index 5aa35bf9..bdad8120 100644 --- a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs +++ b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs @@ -20,6 +20,8 @@ public record InviteStaffCommand : IRequest public string Role { get; init; } = "Cashier"; public Guid? ShopId { get; init; } public Guid? BranchId { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } } public record InviteStaffResult(Guid StaffId, string Email, string Status); @@ -70,7 +72,9 @@ public class InviteStaffCommandHandler : IRequestHandler public string? EmployeeCode { get; init; } public string? Phone { get; init; } public string? Role { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string? Address { get; init; } + public string? ProfilePhotoUrl { get; init; } + public string? DocumentFrontUrl { get; init; } + public string? DocumentBackUrl { get; init; } } /// @@ -141,7 +151,15 @@ public class UpdateStaffCommandHandler : IRequestHandler(s.RoleId).Name, Status = Enumeration.FromValue(s.StatusId).Name, Permissions = (int)s.Permissions, @@ -90,6 +96,12 @@ public record StaffDto public string Email { get; init; } = null!; public string? EmployeeCode { get; init; } public string? Phone { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string? Address { get; init; } + public string? ProfilePhotoUrl { get; init; } + public string? DocumentFrontUrl { get; init; } + public string? DocumentBackUrl { get; init; } public string Role { get; init; } = null!; public string Status { get; init; } = null!; public int Permissions { get; init; } diff --git a/services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs b/services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs index c6894892..47fceaca 100644 --- a/services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs +++ b/services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs @@ -55,7 +55,9 @@ public class StaffController : ControllerBase Email = request.Email, Role = request.Role, ShopId = request.ShopId, - BranchId = request.BranchId + BranchId = request.BranchId, + FirstName = request.FirstName, + LastName = request.LastName }; var result = await _mediator.Send(command); _logger.LogInformation("Staff invited: {Email}", request.Email); @@ -83,7 +85,13 @@ public class StaffController : ControllerBase StaffId = staffId, EmployeeCode = request.EmployeeCode, Phone = request.Phone, - Role = request.Role + Role = request.Role, + FirstName = request.FirstName, + LastName = request.LastName, + Address = request.Address, + ProfilePhotoUrl = request.ProfilePhotoUrl, + DocumentFrontUrl = request.DocumentFrontUrl, + DocumentBackUrl = request.DocumentBackUrl }; await _mediator.Send(command); return Ok(new { message = "Staff updated successfully" }); @@ -189,6 +197,8 @@ public record InviteStaffRequest public string Role { get; init; } = "Cashier"; public Guid? ShopId { get; init; } public Guid? BranchId { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } } public record UpdateStaffRequest @@ -196,6 +206,12 @@ public record UpdateStaffRequest public string? EmployeeCode { get; init; } public string? Phone { get; init; } public string? Role { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string? Address { get; init; } + public string? ProfilePhotoUrl { get; init; } + public string? DocumentFrontUrl { get; init; } + public string? DocumentBackUrl { get; init; } } public record AcceptInviteRequest diff --git a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/MerchantStaff.cs b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/MerchantStaff.cs index 5ed216e2..2d542557 100644 --- a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/MerchantStaff.cs +++ b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/MerchantStaff.cs @@ -31,6 +31,12 @@ public class MerchantStaff : Entity, IAggregateRoot private DateTime? _terminatedAt; private DateTime _createdAt; private DateTime? _updatedAt; + private string? _firstName; + private string? _lastName; + private string? _address; + private string? _profilePhotoUrl; + private string? _documentFrontUrl; + private string? _documentBackUrl; private readonly List _deviceTokens = new(); private readonly List _shopAssignments = new(); @@ -137,6 +143,13 @@ public class MerchantStaff : Entity, IAggregateRoot /// public DateTime? UpdatedAt => _updatedAt; + public string? FirstName => _firstName; + public string? LastName => _lastName; + public string? Address => _address; + public string? ProfilePhotoUrl => _profilePhotoUrl; + public string? DocumentFrontUrl => _documentFrontUrl; + public string? DocumentBackUrl => _documentBackUrl; + /// /// EN: Private constructor for EF Core. /// VI: Constructor private cho EF Core. @@ -153,7 +166,9 @@ public class MerchantStaff : Entity, IAggregateRoot Guid merchantId, string email, StaffRole role, - StaffPermissions permissions = StaffPermissions.None) + StaffPermissions permissions = StaffPermissions.None, + string? firstName = null, + string? lastName = null) { if (merchantId == Guid.Empty) throw new DomainException("Merchant ID cannot be empty"); @@ -172,6 +187,8 @@ public class MerchantStaff : Entity, IAggregateRoot _status = StaffStatus.Invited, StatusId = StaffStatus.Invited.Id, _permissions = permissions, + _firstName = firstName?.Trim(), + _lastName = lastName?.Trim(), _createdAt = DateTime.UtcNow }; @@ -203,10 +220,24 @@ public class MerchantStaff : Entity, IAggregateRoot /// EN: Update staff information. /// VI: Cập nhật thông tin nhân viên. /// - public void Update(string? employeeCode, string? phone) + public void Update( + string? employeeCode, + string? phone, + string? firstName = null, + string? lastName = null, + string? address = null, + string? profilePhotoUrl = null, + string? documentFrontUrl = null, + string? documentBackUrl = null) { _employeeCode = employeeCode?.Trim(); _phone = phone?.Trim(); + _firstName = firstName?.Trim(); + _lastName = lastName?.Trim(); + _address = address?.Trim(); + _profilePhotoUrl = profilePhotoUrl?.Trim(); + _documentFrontUrl = documentFrontUrl?.Trim(); + _documentBackUrl = documentBackUrl?.Trim(); _updatedAt = DateTime.UtcNow; } diff --git a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs index 8e3160d0..bc5556ee 100644 --- a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs +++ b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/MerchantStaffEntityTypeConfiguration.cs @@ -82,6 +82,36 @@ public class MerchantStaffEntityTypeConfiguration : IEntityTypeConfiguration s.FirstName) + .HasField("_firstName") + .HasColumnName("first_name") + .HasMaxLength(100); + + builder.Property(s => s.LastName) + .HasField("_lastName") + .HasColumnName("last_name") + .HasMaxLength(100); + + builder.Property(s => s.Address) + .HasField("_address") + .HasColumnName("address") + .HasMaxLength(500); + + builder.Property(s => s.ProfilePhotoUrl) + .HasField("_profilePhotoUrl") + .HasColumnName("profile_photo_url") + .HasMaxLength(500); + + builder.Property(s => s.DocumentFrontUrl) + .HasField("_documentFrontUrl") + .HasColumnName("document_front_url") + .HasMaxLength(500); + + builder.Property(s => s.DocumentBackUrl) + .HasField("_documentBackUrl") + .HasColumnName("document_back_url") + .HasMaxLength(500); + // EN: Configure navigation to device tokens // VI: Cấu hình navigation đến device tokens builder.HasMany(s => s.DeviceTokens)