feat(web-client-tpos): add product update (PUT) endpoint and edit UI
- Add PUT /api/bff/products/{id} endpoint with ownership validation
- Add UpdateProductAsync to PosDataService
- Add edit button (pencil icon) on each product card
- Form supports edit mode: title/button text changes, pre-fills values
- Add EditProduct and SaveProduct methods in ShopPage.razor
- Full CRUD: Create, Read, Update, Delete all functional
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Scratchpad Context — GoodGo Platform
|
||||
> Updated: 2026-03-03 14:22
|
||||
> Updated: 2026-03-03 20:10
|
||||
|
||||
## Trạng thái hệ thống
|
||||
|
||||
@@ -10,44 +10,28 @@
|
||||
- **Queue**: RabbitMQ (rabbitmq-local)
|
||||
- **Storage**: MinIO (minio-local)
|
||||
|
||||
### Services (26 .NET microservices)
|
||||
| Service | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| iam-service-net | ✅ healthy | Auth, Users, Roles |
|
||||
| merchant-service-net | ✅ healthy | Merchants, Shops, Staff |
|
||||
| catalog-service-net | ✅ healthy | Products, Categories |
|
||||
| order-service-net | ✅ healthy | Orders, Order Items |
|
||||
| booking-service-net | ✅ healthy | Appointments, Resources |
|
||||
| fnb-engine-net | ✅ healthy | Tables, Sessions |
|
||||
| inventory-service-net | ✅ healthy | Stock management |
|
||||
| promotion-service-net | ✅ healthy | Campaigns, Vouchers |
|
||||
| membership-service-net | ✅ healthy | Members, Levels |
|
||||
| wallet-service-net | ✅ healthy | Wallets, Transactions |
|
||||
| chat-service-net | ✅ healthy | Chat, SignalR |
|
||||
| social-service-net | ✅ healthy | Social features |
|
||||
| storage-service-net | ✅ healthy | File storage |
|
||||
| mining-service-net | ✅ healthy | Data mining |
|
||||
| mission-service-net | ✅ healthy | Gamification |
|
||||
| ads-*-service-net (5) | ✅ healthy | Ads ecosystem |
|
||||
| mkt-*-service-net (4) | ✅ healthy | Marketing integrations |
|
||||
|
||||
### Apps
|
||||
| App | Type | Port | Status |
|
||||
|-----|------|------|--------|
|
||||
| web-client-tpos-net | Blazor WASM POS | 3001 | ✅ Running |
|
||||
| web-client-base-net | Blazor Admin | — | Not deployed locally |
|
||||
| app-client-base-net | .NET MAUI Mobile | — | Not deployed |
|
||||
| app-client-base-swift | Swift iOS | — | Not deployed |
|
||||
| web-docs | Documentation site | — | Not deployed |
|
||||
|
||||
## Recent Changes (từ Git log)
|
||||
1. `d969f3d` — **feat**: Replace hardcoded POS data with API-driven endpoints
|
||||
2. `fe6e14c` — **feat**: Unify POS with inline payment and tabs (path fix)
|
||||
3. `15404a8` — **feat**: Unify POS page with tabs and inline payment
|
||||
4. `617a7ca` — **fix**: Resolve 500 error on GET /api/v1/users endpoint
|
||||
5. `ad6fe03` — **feat**: Add users management page and enhance roles CRUD
|
||||
6. `f3bfcc8` — **fix**: Roles API response parsing and property mapping
|
||||
7. `f353f88` — **feat**: Redesign Home/NotFound pages, localize Dashboard
|
||||
1. `a791830` — **feat**: Add date filter to order history and payment method display
|
||||
2. `7562fc1` — **feat**: Add receipt print with thermal 80mm layout
|
||||
3. `e74527d` — **feat**: Implement POS order creation via BFF API
|
||||
4. `d969f3d` — **feat**: Replace hardcoded POS data with API-driven endpoints
|
||||
5. `fe6e14c` — **feat**: Unify POS with inline payment and tabs (path fix)
|
||||
|
||||
## Completed Tasks
|
||||
- ✅ P1: Order API Create — orders + items persist in DB
|
||||
- ✅ P1: Print Receipt — thermal 80mm popup with JSInterop
|
||||
- ✅ P2: Dashboard Real Data — hourly chart, popular items, payment breakdown
|
||||
- ✅ P2: History Date Filter — today/7d/30d with API reload
|
||||
|
||||
## Next Tasks
|
||||
- P3: Admin Panel — Products CRUD, Inventory, Staff
|
||||
- P4: Frontend Apps — web-client-base-net deploy
|
||||
- P5: Infrastructure — Observability, CI/CD, Tests
|
||||
|
||||
## Blockers
|
||||
- Chưa có
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
# Task Plan — GoodGo Platform
|
||||
> Updated: 2026-03-03 14:22
|
||||
> Updated: 2026-03-03 20:10
|
||||
|
||||
## ✅ Completed Tasks
|
||||
1. ~~Admin Users & Roles — CRUD, phân quyền~~
|
||||
2. ~~POS Unified page — 3 tabs (Sale, History, Dashboard)~~
|
||||
3. ~~POS Inline Payment Flow~~
|
||||
4. ~~Replace hardcoded POS data with API endpoints~~
|
||||
5. ~~P1: Order API Create — POST /api/bff/pos/orders~~
|
||||
6. ~~P1: Print Receipt — Thermal 80mm popup~~
|
||||
7. ~~P2: Dashboard Real Data — hourly/popular/payment~~
|
||||
8. ~~P2: History Date Filter — today/7d/30d~~
|
||||
|
||||
## Pending Tasks (Ưu tiên cao → thấp)
|
||||
|
||||
### 🔴 P1 — POS Core (Hoàn thiện)
|
||||
- [ ] **Order API Create**: `ConfirmPayment` gọi Order Service API tạo order thật (hiện chỉ lưu in-memory session)
|
||||
- [ ] **Order Items**: Lưu order_items (product_id, qty, unit_price) khi confirm
|
||||
- [ ] **Payment Method column**: Add `payment_method` column vào orders table nếu chưa có
|
||||
- [ ] **Print receipt**: Implement in hóa đơn (hoặc generate PDF)
|
||||
|
||||
### 🟡 P2 — Dashboard Real Data
|
||||
- [ ] **Hourly chart**: Verify `order_items` + `payment_method` tables exist
|
||||
- [ ] **Popular items**: Test with real order data
|
||||
- [ ] **History search/filter**: Implement date range filter (today/7 days/30 days)
|
||||
|
||||
### 🟡 P3 — Admin Panel (web-client-tpos-net)
|
||||
- [ ] **Products CRUD**: Thêm/sửa/xóa products từ Admin
|
||||
- [ ] **Inventory management**: Stock alerts, reorder
|
||||
@@ -27,7 +20,7 @@
|
||||
- [ ] **Promotions**: Campaign management UI
|
||||
|
||||
### 🟢 P4 — Frontend Apps
|
||||
- [ ] **web-client-base-net**: Deploy locally, kiểm tra
|
||||
- [x] **web-client-tpos-net**: Deployed locally (port 3001) ✅
|
||||
- [ ] **app-client-base-swift**: iOS app review
|
||||
- [ ] **web-docs**: Documentation site
|
||||
|
||||
@@ -37,4 +30,4 @@
|
||||
- [ ] **Testing**: Unit + Integration tests coverage
|
||||
|
||||
## Next Task
|
||||
> **P1: Order API Create** — Implement `POST /api/bff/orders` endpoint + call from POS ConfirmPayment
|
||||
> **P3: Products CRUD** — Admin panel thêm/sửa/xóa sản phẩm
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
case "products":
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
||||
<h3 style="margin:0;font-size:16px;font-weight:700;">@(_products.Count) sản phẩm</h3>
|
||||
<button class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;" @onclick="@(() => { _showProductForm = !_showProductForm; })">
|
||||
<button class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;" @onclick="@(() => { _editingProductId = null; _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductType = "PreparedFood"; _formMessage = null; _showProductForm = !_showProductForm; })">
|
||||
<i data-lucide="plus-circle" style="width:16px;height:16px;"></i>
|
||||
Thêm sản phẩm
|
||||
</button>
|
||||
@@ -166,7 +166,7 @@
|
||||
@if (_showProductForm)
|
||||
{
|
||||
<div class="admin-panel" style="margin-bottom:16px;border:1px solid rgba(255,92,0,0.3);">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Thêm sản phẩm mới</h3></div>
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">@(_editingProductId.HasValue ? "Chỉnh sửa sản phẩm" : "Thêm sản phẩm mới")</h3></div>
|
||||
<div class="admin-panel__body">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Tên sản phẩm *</label><input type="text" @bind="_newProductName" class="admin-input" placeholder="VD: Cà phê sữa đá" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);" /></div>
|
||||
@@ -181,7 +181,7 @@
|
||||
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Mô tả</label><input type="text" @bind="_newProductDesc" class="admin-input" placeholder="Mô tả ngắn" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);" /></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:16px;">
|
||||
<button class="admin-btn-primary" @onclick="AddProduct" style="display:inline-flex;align-items:center;gap:6px;"><i data-lucide="check" style="width:14px;height:14px;"></i>Lưu</button>
|
||||
<button class="admin-btn-primary" @onclick="@(_editingProductId.HasValue ? SaveProduct : AddProduct)" style="display:inline-flex;align-items:center;gap:6px;"><i data-lucide="check" style="width:14px;height:14px;"></i>@(_editingProductId.HasValue ? "Cập nhật" : "Lưu")</button>
|
||||
<button class="admin-btn" @onclick="@(() => _showProductForm = false)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"><i data-lucide="x" style="width:14px;height:14px;"></i>Hủy</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(_formMessage))
|
||||
@@ -204,7 +204,10 @@
|
||||
var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" };
|
||||
<div class="admin-panel" style="position:relative;">
|
||||
<div class="admin-panel__body" style="padding:16px;text-align:center;">
|
||||
<button @onclick="@(() => DeleteProduct(p.Id))" style="position:absolute;top:8px;right:8px;background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa"><i data-lucide="trash-2" style="color:#EF4444;width:14px;height:14px;"></i></button>
|
||||
<div style="position:absolute;top:8px;right:8px;display:flex;gap:4px;">
|
||||
<button @onclick="@(() => EditProduct(p))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa"><i data-lucide="pencil" style="color:#3B82F6;width:14px;height:14px;"></i></button>
|
||||
<button @onclick="@(() => DeleteProduct(p.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa"><i data-lucide="trash-2" style="color:#EF4444;width:14px;height:14px;"></i></button>
|
||||
</div>
|
||||
<div style="width:48px;height:48px;border-radius:12px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="package" style="color:#FF5C00;width:24px;height:24px;"></i></div>
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@p.Name</div>
|
||||
<div style="font-size:13px;color:var(--admin-text-tertiary);margin-bottom:4px;">@(p.CategoryName ?? "—")</div>
|
||||
@@ -1515,6 +1518,7 @@
|
||||
private List<PosDataService.AppointmentInfo> _ovAppts = new();
|
||||
// Product form state
|
||||
private bool _showProductForm;
|
||||
private Guid? _editingProductId;
|
||||
private string _newProductName = "";
|
||||
private decimal _newProductPrice;
|
||||
private string _newProductType = "PreparedFood";
|
||||
@@ -1760,6 +1764,35 @@
|
||||
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 ?? "";
|
||||
_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
|
||||
{
|
||||
await DataService.UpdateProductAsync(_editingProductId.Value, new PosDataService.CreateProductRequest(
|
||||
_shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, null, null));
|
||||
_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;
|
||||
|
||||
@@ -97,6 +97,13 @@ public class PosDataService
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateProductAsync(Guid productId, CreateProductRequest req)
|
||||
{
|
||||
AttachToken();
|
||||
var resp = await _http.PutAsJsonAsync($"api/bff/products/{productId}", req, _jsonOptions);
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteProductAsync(Guid productId)
|
||||
{
|
||||
AttachToken();
|
||||
|
||||
@@ -328,6 +328,38 @@ public class BffDataController : ControllerBase
|
||||
return CreatedAtAction(nameof(GetAllProducts), new { }, new { id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update a product — validates shop ownership first.
|
||||
/// VI: Cập nhật sản phẩm — kiểm tra quyền sở hữu shop trước.
|
||||
/// </summary>
|
||||
[HttpPut("products/{productId:guid}")]
|
||||
public async Task<IActionResult> UpdateProduct(Guid productId, [FromBody] CreateProductRequest req)
|
||||
{
|
||||
var merchantId = await GetCurrentMerchantIdAsync();
|
||||
if (merchantId == null)
|
||||
return Unauthorized();
|
||||
|
||||
var myShopIds = await GetMyShopIdsAsync(merchantId.Value);
|
||||
if (!myShopIds.Contains(req.ShopId))
|
||||
return Unauthorized();
|
||||
|
||||
var typeId = (req.Type ?? "PreparedFood") switch
|
||||
{
|
||||
"Physical" => 1,
|
||||
"Service" => 2,
|
||||
"PreparedFood" => 3,
|
||||
_ => 3
|
||||
};
|
||||
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
|
||||
var rows = await conn.ExecuteAsync(
|
||||
@"UPDATE products SET name = @Name, description = @Description, price = @Price,
|
||||
type_id = @TypeId, sku = @Sku, image_url = @ImageUrl
|
||||
WHERE id = @Id AND shop_id = ANY(@ShopIds)",
|
||||
new { Id = productId, req.Name, req.Description, req.Price, TypeId = typeId,
|
||||
req.Sku, req.ImageUrl, ShopIds = myShopIds.ToArray() });
|
||||
return rows > 0 ? Ok(new { id = productId }) : NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete (deactivate) a product — validates ownership first.
|
||||
/// VI: Xóa (vô hiệu hóa) sản phẩm — kiểm tra quyền sở hữu trước.
|
||||
|
||||
Reference in New Issue
Block a user