From 15b17f54ca6b343207435bb848f4b1bfebfebbd1 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Tue, 3 Mar 2026 20:34:56 +0700 Subject: [PATCH] feat(web-client-tpos): add staff update/delete and inventory update CRUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BFF Endpoints: - PUT /api/bff/staff/{id} — update employee code, role, phone, email - DELETE /api/bff/staff/{id} — soft-delete (set status=Terminated) - PUT /api/bff/inventory/{id} — update quantity and reorder level - Fix CreateStaff: Forbid→Unauthorized, add created_at to INSERT PosDataService Methods: - UpdateStaffAsync(Guid, CreateStaffRequest) - DeleteStaffAsync(Guid) - UpdateInventoryAsync(Guid, UpdateInventoryRequest) ShopPage.razor UI: - Staff table: new Hành động column with edit/delete buttons - Staff form: switches between Thêm/Chỉnh sửa mode - EditStaff, SaveStaffEdit, DeleteStaffMember methods --- .../Pages/Admin/Shop/ShopPage.razor | 53 +++++++++++++- .../Services/PosDataService.cs | 23 ++++++ .../Controllers/BffDataController.cs | 73 ++++++++++++++++++- 3 files changed, 143 insertions(+), 6 deletions(-) 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 97b69439..d7cce1b8 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 @@ -345,7 +345,7 @@ case "staff":

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

- @@ -353,7 +353,7 @@ @if (_showStaffForm) {
-

Thêm nhân viên mới

+

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

@@ -370,7 +370,7 @@
- +
@if (!string.IsNullOrEmpty(_staffFormMessage)) @@ -397,6 +397,7 @@ Vai trò Trạng thái SĐT + Hành động @foreach (var s in _staff) { @@ -405,6 +406,12 @@ @(s.Role ?? "—") @(s.Status ?? "—") @(s.Phone ?? s.Email ?? "—") + +
+ + +
+ } @@ -1527,6 +1534,7 @@ private bool _formSuccess; // Staff form state private bool _showStaffForm; + private Guid? _editingStaffId; private string _newStaffCode = ""; private string _newStaffRole = "Cashier"; private string _newStaffPhone = ""; @@ -1811,6 +1819,45 @@ 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 ?? ""; + _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 + { + await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest( + _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole)); + _staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true; + _editingStaffId = null; + _staff = await DataService.GetStaffAsync(); + } + catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; } + } + + private async Task DeleteStaffMember(Guid staffId) + { + try + { + await DataService.DeleteStaffAsync(staffId); + _staff = await DataService.GetStaffAsync(); + } + 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 { 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 52136a3b..bab71daf 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 @@ -142,6 +142,29 @@ public class PosDataService return resp.IsSuccessStatusCode; } + public async Task UpdateStaffAsync(Guid staffId, CreateStaffRequest req) + { + AttachToken(); + var resp = await _http.PutAsJsonAsync($"api/bff/staff/{staffId}", req, _jsonOptions); + return resp.IsSuccessStatusCode; + } + + public async Task DeleteStaffAsync(Guid staffId) + { + AttachToken(); + var resp = await _http.DeleteAsync($"api/bff/staff/{staffId}"); + return resp.IsSuccessStatusCode; + } + + public record UpdateInventoryRequest(int Quantity, int ReorderLevel); + + public async Task UpdateInventoryAsync(Guid inventoryId, UpdateInventoryRequest req) + { + AttachToken(); + var resp = await _http.PutAsJsonAsync($"api/bff/inventory/{inventoryId}", req, _jsonOptions); + return resp.IsSuccessStatusCode; + } + // ═══ STAFF ROLES & SCHEDULES ═══ public record StaffRoleInfo(int Id, string Name); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs index 8ec53cfe..cc6512d7 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs @@ -464,7 +464,7 @@ public class BffDataController : ControllerBase { var merchantId = await GetCurrentMerchantIdAsync(); if (merchantId == null || merchantId.Value != req.MerchantId) - return Forbid(); // EN: Cannot create staff for another merchant + return Unauthorized(); // EN: Cannot create staff for another merchant var id = Guid.NewGuid(); await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); @@ -478,12 +478,78 @@ public class BffDataController : ControllerBase if (statusId == 0) statusId = 1; await conn.ExecuteAsync( - @"INSERT INTO merchant_staff (id, merchant_id, employee_code, phone, email, role_id, status_id, joined_at) - VALUES (@Id, @MerchantId, @EmployeeCode, @Phone, @Email, @RoleId, @StatusId, NOW())", + @"INSERT INTO merchant_staff (id, merchant_id, employee_code, phone, email, role_id, status_id, joined_at, created_at) + VALUES (@Id, @MerchantId, @EmployeeCode, @Phone, @Email, @RoleId, @StatusId, NOW(), NOW())", new { Id = id, req.MerchantId, req.EmployeeCode, req.Phone, req.Email, RoleId = roleId, StatusId = statusId }); return CreatedAtAction(nameof(GetStaff), new { }, new { id }); } + /// + /// EN: Update a staff member — validates merchant ownership. + /// VI: Cập nhật nhân viên — kiểm tra quyền sở hữu merchant. + /// + [HttpPut("staff/{staffId:guid}")] + public async Task UpdateStaff(Guid staffId, [FromBody] CreateStaffRequest req) + { + var merchantId = await GetCurrentMerchantIdAsync(); + if (merchantId == null || merchantId.Value != req.MerchantId) + return Unauthorized(); + + await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); + var roleId = await conn.QueryFirstOrDefaultAsync( + "SELECT id FROM staff_roles WHERE name = @Role", new { req.Role }); + if (roleId == 0) roleId = 1; + + var rows = await conn.ExecuteAsync( + @"UPDATE merchant_staff SET employee_code = @EmployeeCode, phone = @Phone, + email = @Email, role_id = @RoleId + WHERE id = @Id AND merchant_id = @MerchantId", + new { Id = staffId, req.MerchantId, req.EmployeeCode, req.Phone, req.Email, RoleId = roleId }); + return rows > 0 ? Ok(new { id = staffId }) : NotFound(); + } + + /// + /// EN: Terminate (soft-delete) a staff member. + /// VI: Chấm dứt (xóa mềm) nhân viên. + /// + [HttpDelete("staff/{staffId:guid}")] + public async Task DeleteStaff(Guid staffId) + { + var merchantId = await GetCurrentMerchantIdAsync(); + if (merchantId == null) return Unauthorized(); + + await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); + // EN: Set status to Terminated + record termination date + var terminatedStatusId = await conn.QueryFirstOrDefaultAsync( + "SELECT id FROM staff_statuses WHERE name = 'Terminated'"); + if (terminatedStatusId == 0) terminatedStatusId = 3; + + await conn.ExecuteAsync( + @"UPDATE merchant_staff SET status_id = @StatusId, terminated_at = NOW() + WHERE id = @Id AND merchant_id = @MerchantId", + new { Id = staffId, StatusId = terminatedStatusId, MerchantId = merchantId.Value }); + return NoContent(); + } + + /// + /// EN: Update inventory quantity for a specific item. + /// VI: Cập nhật số lượng tồn kho cho mặt hàng. + /// + [HttpPut("inventory/{inventoryId:guid}")] + public async Task UpdateInventory(Guid inventoryId, [FromBody] UpdateInventoryRequest req) + { + var merchantId = await GetCurrentMerchantIdAsync(); + if (merchantId == null) return Unauthorized(); + + var myShopIds = await GetMyShopIdsAsync(merchantId.Value); + await using var conn = new NpgsqlConnection(ConnStr("inventory_service")); + var rows = await conn.ExecuteAsync( + @"UPDATE inventory_items SET quantity = @Quantity, reorder_level = @ReorderLevel, updated_at = NOW() + WHERE id = @Id AND shop_id = ANY(@ShopIds)", + new { Id = inventoryId, req.Quantity, req.ReorderLevel, ShopIds = myShopIds.ToArray() }); + return rows > 0 ? Ok(new { id = inventoryId }) : NotFound(); + } + // ═══ STAFF ROLES ═══ [HttpGet("staff/roles")] public async Task GetStaffRoles() @@ -1034,4 +1100,5 @@ public class BffDataController : ControllerBase public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role); public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List Items); public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice); + public record UpdateInventoryRequest(int Quantity, int ReorderLevel); }