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