Records two pending business decisions in docs/asset-license-management.md: AssetCategory/AssetModel/ConsumableItem classification (already decided 2026-08-02, carried forward from local WIP), and a new spec for Snipe-IT-style per-category custom fields (AssetFieldDefinition/AssetFieldValue, EAV-based). Custom fields spec is blocked on stakeholder answers to open questions (required flag, editing definitions after assets exist, whether it applies to ConsumableItem) before pm can chunk implementation tasks. Co-Authored-By: Claude Sonnet 5 <[email protected]>
154 lines
23 KiB
Markdown
154 lines
23 KiB
Markdown
# Quản lý Tài sản IT & License
|
||
|
||
## Trạng thái hiện tại
|
||
|
||
- Chưa có hệ thống tập trung — request xử lý rời rạc qua email/docx (vd `Firefly_License_Request.docx`, `Eric Pelatan.docx` ở gốc workspace `ubisoft/`, ngoài repo này). Không tracking được ai đang giữ gì, license nào sắp hết hạn/hết seat, tài sản nào sắp hết bảo hành.
|
||
- Đã chốt: kiến trúc monolith Next.js, không tách backend/frontend ở giai đoạn này (xem trigger để tách ở dưới).
|
||
- **Đã chốt: Postgres** (2026-07-29) — lý do: đảm bảo scale sau này + Power BI connect native, và bạn đã có kinh nghiệm dùng Postgres. Migrate task 1 (entity Request, state Draft) từ SQLite sang Postgres đã xong, CONFIRMED pass.
|
||
- **Đã chốt: Prisma (ORM) + Zod (validation)** (2026-07-30) — lý do Prisma over Drizzle cho solo dev: Prisma Studio (xem/sửa data qua GUI, khỏi viết psql query khi không có teammate hỏi), migration workflow tự động (`prisma migrate dev`), docs/community lớn hơn hẳn khi cần tự tra cứu một mình. Đánh đổi: nặng hơn Drizzle (query engine binary riêng, cần bước generate client), "magic" hơn raw SQL. Zod đi kèm để tận dụng type inference thay vì viết tay interface + hàm validate riêng.
|
||
- **Retrofit `Request` sang Prisma+Zod xong (2026-07-30)** — CONFIRMED qua test-runner độc lập: 6 test pass trên Postgres thật (qua `prisma migrate deploy`), type-check sạch, lint sạch. `type` chuyển từ `TEXT + CHECK` sang Postgres enum (`RequestType`) — tương đương semantics, không phải divergence. `id`/`createdAt` giữ nguyên hành vi cũ. Chi tiết đầy đủ ở `../CLAUDE.md`.
|
||
- Project scaffold (multi-agent dev workflow) đặt ngay tại repo này — xem `../CLAUDE.md`.
|
||
- File này trước đây nằm ở `ubisoft/processes/asset-license-management.md` (workspace chung, không phải git repo) — đã move vào đây (2026-07-30) vì nó gắn chặt với app này và cần update song song với code. `ubisoft/processes/README.md` vẫn giữ 1 dòng trỏ tới đây cho ai tìm từ đó.
|
||
|
||
## Mục tiêu
|
||
|
||
Theo dõi toàn bộ vòng đời tài sản/license: **Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire**, và trả lời nhanh được:
|
||
- Ai đang giữ máy/license nào?
|
||
- License nào sắp hết hạn hoặc hết seat?
|
||
- Tài sản nào sắp hết bảo hành?
|
||
- Chi phí/license theo phòng ban là bao nhiêu?
|
||
|
||
## Data model (sơ bộ)
|
||
|
||
| Entity | Ý nghĩa |
|
||
|---|---|
|
||
| `Employee` | id, tên, phòng ban, manager |
|
||
| `Vendor` | nhà cung cấp |
|
||
| `AssetItem` | hardware **high-value** (serial hóa bắt buộc) — serial number, model (FK → `AssetModel`, bắt buộc — không còn string tự do), ngày mua, bảo hành, trạng thái (in stock / assigned / repair / retired). Category không còn gán trực tiếp trên `AssetItem` — suy ra qua `AssetModel.categoryId`. Xem "Phân loại tài sản" bên dưới |
|
||
| `License` | phần mềm — tên, vendor, tổng seat, seat đã dùng, ngày hết hạn, loại (perpetual/subscription) |
|
||
| `Request` | người yêu cầu, loại (asset/license), lý do, trạng thái — **chỉ áp dụng cho asset high-value/license**, không dùng cho consumable (xem "Phân loại tài sản" bên dưới) |
|
||
| `PurchaseOrder` | gộp nhiều Request đã duyệt, vendor, ngày đặt, ngày dự kiến nhận |
|
||
| `Receipt` | nhận hàng — đối chiếu PO, ngày nhận, người nhận |
|
||
| `Assignment` | gán AssetItem (high-value)/License cho 1 Employee cụ thể — ngày giao, ngày trả, biên bản ký nhận. **Không áp dụng cho consumable** (consumable track theo tồn kho, không gắn 1 người cụ thể) |
|
||
| `Request.servicenow_ticket` | field text, nhập tay số ticket ServiceNow liên quan (không tích hợp API ServiceNow ngay — volume thấp, nhập tay đủ dùng; để ngỏ cho auto-sync sau nếu cần) |
|
||
| `AssetCategory` | id, tên (vd "Laptop", "Cáp HDMI"), nhóm giá trị cố định (`high_value` \| `low_value`) — danh mục category cụ thể do IT admin tự định nghĩa, không hardcode. Xem "Phân loại tài sản" bên dưới |
|
||
| `AssetModel` | id, category (FK → `AssetCategory`, bắt buộc, phải thuộc nhóm `high_value`), `manufacturer` (vd "Dell"), `name` (vd "Latitude 5420") — tách riêng 2 field, không gộp chung 1 string, để filter/report theo hãng. Nằm giữa `AssetCategory` và `AssetItem` (1 model có nhiều `AssetItem` serial khác nhau). Xem "Phân loại tài sản" bên dưới |
|
||
| `ConsumableItem` | phụ kiện **low-value/consumable** — tên, category (bắt buộc, phải thuộc nhóm `low_value`, trỏ thẳng `AssetCategory` — **không** qua `AssetModel`, khái niệm "model" chỉ áp dụng cho asset serial hóa), số lượng tồn kho (`quantityOnHand`). **KHÔNG serial hóa, KHÔNG gắn Assignment cá nhân** — chỉ track theo số lượng. Xem "Phân loại tài sản" bên dưới |
|
||
| `AssetFieldDefinition` | định nghĩa 1 custom field gắn với 1 `AssetCategory` — `fieldKey`, `label`, `fieldType` (`text\|number\|date\|boolean\|dropdown`), `options` (chỉ dùng khi `fieldType = dropdown`). Xem "Custom fields theo Category" bên dưới |
|
||
| `AssetFieldValue` | giá trị thực tế của 1 `AssetFieldDefinition` trên 1 `AssetItem` cụ thể — 4 cột kiểu riêng (`valueText`/`valueNumber`/`valueDate`/`valueBoolean`), chỉ cột khớp `fieldType` được set. Xem "Custom fields theo Category" bên dưới |
|
||
|
||
## Luồng trạng thái Request
|
||
|
||
```
|
||
Draft → Pending Approval → Approved → Ordered → Received → Delivered → Closed
|
||
↓
|
||
Rejected / Cancelled
|
||
```
|
||
|
||
## Phân loại tài sản: High-value vs Low-value/Consumable (chốt 2026-08-02)
|
||
|
||
**Quyết định đã chốt với stakeholder:** tài sản được phân nhóm theo **category cố định**, KHÔNG theo ngưỡng giá tiền. Mỗi `AssetCategory` thuộc đúng 1 trong 2 nhóm giá trị (`high_value` hoặc `low_value`), category cụ thể do IT admin tự định nghĩa/mở rộng — danh sách dưới đây chỉ là **ví dụ minh hoạ, không phải danh sách đóng**:
|
||
|
||
- Nhóm `high_value` → đi qua luồng `AssetItem` hiện có nguyên vẹn (serial hóa bắt buộc, track cá nhân qua `Assignment`, đi qua state machine đầy đủ của `Request`). Ví dụ category: Laptop, Màn hình, Máy in, Điện thoại công ty.
|
||
- Nhóm `low_value` (consumable) → entity mới `ConsumableItem`, **không serial hóa**, chỉ track theo số lượng tồn kho (`quantityOnHand`), **không gắn `Assignment`** cho 1 người cụ thể. Ví dụ category: Cáp (HDMI/USB-C/LAN), Chuột, Bàn phím, Túi đựng laptop, Sạc dự phòng.
|
||
|
||
**Cập nhật 2026-08-02 (2): thêm phân cấp `AssetModel` giữa `AssetCategory` và `AssetItem`.** Lý do: nhiều model khác nhau trong cùng 1 category (vd category "Laptop" có nhiều model "Dell Latitude 5420", "MacBook Pro 14"...), cần report/thống kê theo model (vd "đang có bao nhiêu Dell Latitude 5420"), tránh lỗi gõ tay khi model là string tự do như trước. Phân cấp: `AssetCategory` ("Laptop") → `AssetModel` ("Dell Latitude 5420") → từng `AssetItem` (1 serial cụ thể thuộc model đó). `AssetModel.manufacturer`/`AssetModel.name` tách riêng (không gộp 1 string) để filter/report riêng theo hãng. Khái niệm "model" chỉ có ý nghĩa cho asset serial hóa (`high_value`) — `ConsumableItem` không có model, vẫn trỏ thẳng `AssetCategory` (`low_value`) như cũ.
|
||
|
||
**Ràng buộc dữ liệu:**
|
||
- `AssetModel.categoryId` bắt buộc, phải trỏ tới 1 `AssetCategory` có `valueGroup = high_value`.
|
||
- `AssetItem.modelId` bắt buộc, phải trỏ tới 1 `AssetModel` có thật (thay cho field `model` string tự do trước đây — đã đổi, không cần chiến lược backfill vì DB chưa có `AssetItem` thật). `AssetItem` không còn giữ `categoryId` trực tiếp — category được suy ra qua `AssetItem.modelId → AssetModel.categoryId` (tránh 2 nguồn sự thật có thể lệch nhau).
|
||
- `ConsumableItem.categoryId` bắt buộc, phải trỏ tới 1 `AssetCategory` có `valueGroup = low_value` — trỏ thẳng, không qua `AssetModel`.
|
||
- `Request` (state machine đầy đủ) tiếp tục chỉ áp dụng cho `type: asset|license` như hiện tại — **không** mở rộng `RequestType` thêm `consumable`.
|
||
|
||
**Luồng riêng cho consumable ("xin + nhận" đơn giản hoá):** stakeholder đã xác nhận muốn 1 luồng gọn hơn state machine đầy đủ của `Request`, nhưng **chưa chốt chi tiết state/approval/xử lý hết hàng** — xem danh sách câu hỏi mở trong spec BA (2026-08-02, lưu ở lịch sử trao đổi/PR liên quan). Không build luồng này (entity `ConsumableRequest` hoặc tương đương) tới khi có câu trả lời — chỉ mới build phần catalog (`AssetCategory`, `ConsumableItem`).
|
||
|
||
## Custom fields theo Category (Additional Options) (spec BA 2026-08-02)
|
||
|
||
**Yêu cầu gốc từ stakeholder** (lấy cảm hứng từ module "Additional Options" của Snipe-IT): mỗi `AssetCategory` cho phép admin tự định nghĩa thêm các field tuỳ biến; khi tạo mới 1 `AssetItem`, tuỳ theo category của asset đó, form hiện thêm 1 section "Additional Options" chứa đúng các field đã định nghĩa cho category đó.
|
||
|
||
**Gắn vào `AssetCategory`, không phải `AssetModel`** — theo đúng cách stakeholder mô tả ("với mỗi category tương ứng... custom thêm các field"). Mọi `AssetModel`/`AssetItem` thuộc cùng 1 category dùng chung 1 bộ field definition.
|
||
|
||
**Lưu trữ: chuẩn hoá quan hệ (EAV), không dùng cột JSONB** — nhất quán với toàn bộ schema hiện tại (mọi entity khác đều dùng cột có kiểu/enum rõ ràng, chưa có tiền lệ JSON nào), và tránh việc các Postgres view phục vụ Power BI (đã chốt ở "Quyết định kiến trúc") phải `->>`/cast JSON cho từng field.
|
||
|
||
- `AssetFieldDefinition`: `id`, `categoryId` (FK → `AssetCategory`, bắt buộc), `fieldKey` (unique **trong phạm vi 1 category**, không unique toàn cục — 2 category khác nhau có thể cùng có field `color`), `label` (tên hiển thị), `fieldType` (enum `text|number|date|boolean|dropdown`), `options` (mảng string, **bắt buộc và không rỗng/không trùng lặp nếu `fieldType = dropdown`**, bỏ trống với các type khác).
|
||
- `AssetFieldValue`: `id`, `assetItemId` (FK → `AssetItem`), `fieldDefinitionId` (FK → `AssetFieldDefinition`), 4 cột giá trị kiểu riêng — `valueText`/`valueNumber`/`valueDate`/`valueBoolean` — **chỉ cột khớp với `fieldDefinitionId.fieldType` được set, các cột còn lại null**. Giữ giá trị đúng kiểu Postgres native (không stringly-typed) để filter/report được, cùng nguyên tắc với enum/date ở các entity khác.
|
||
- MVP chỉ áp dụng cho `AssetItem` (high-value, serial hóa) — **chưa áp dụng cho `ConsumableItem`** (xem câu hỏi mở #3 bên dưới, chưa chốt).
|
||
- MVP: **mọi custom field đều optional**, chưa có khái niệm `required` (xem câu hỏi mở #1).
|
||
- MVP: **chưa có khả năng sửa/xoá `AssetFieldDefinition`** sau khi tạo — đây cũng là capability sửa/xoá đầu tiên trong toàn app (mọi entity hiện tại đều mới chỉ có tạo mới + đọc, chưa có `PUT`/`PATCH`/xoá nào), nên đây không phải gap riêng của feature này mà là giới hạn chung hiện tại của app (xem câu hỏi mở #2).
|
||
|
||
**Câu hỏi mở — cần stakeholder trả lời, không tự đoán:**
|
||
1. Field có thể đánh dấu `required` không? Nếu có, chặn cứng hay chỉ cảnh báo lúc tạo `AssetItem`?
|
||
2. Khi category đã có `AssetItem` thật dùng field definition của nó — cho sửa/xoá definition không? Nếu xoá 1 field đã có giá trị: giữ nguyên giá trị cũ (chỉ ẩn khỏi form tạo mới), xoá cứng luôn cả giá trị liên quan, hay soft-delete/archive?
|
||
3. Custom field chỉ áp dụng cho `AssetItem` (high-value) hay áp dụng cả `ConsumableItem` (low-value)? Snipe-IT gốc chỉ áp dụng fieldset cho hardware serial hóa, nhưng domain doc này chưa xác nhận có áp dụng tương tự cho Ubisoft VN không.
|
||
4. Có giới hạn số lượng custom field tối đa trên 1 category không?
|
||
5. Giá trị custom field có cần search/filter được trong report (Power BI view) hoặc filter danh sách asset trong app không?
|
||
|
||
Chưa build gì cho phần này tới khi có câu trả lời cho ít nhất câu #1–#3 (câu #4–#5 có thể để mặc định hợp lý — không giới hạn số field, chưa cần search — nếu stakeholder không có ý kiến, nhưng vẫn nên hỏi trước khi `pm` chia task).
|
||
|
||
## Module tương lai: Rack/Slot management + IoT (chưa build, chỉ note kiến trúc)
|
||
|
||
Mục tiêu xa hơn: biết chính xác asset nằm ở ô/slot nào trên tủ rack, và hướng dẫn người lấy đúng ô bằng đèn (pick-to-light).
|
||
|
||
**Data model thêm:**
|
||
- `Rack` (id, tên, vị trí — map với `rack-1/2/3` đã có ảnh ở `../../server-racks/`, thư mục ngoài repo này trong workspace `ubisoft/`)
|
||
- `RackSlot` (rack_id, vị trí U, `sensor_status` [empty/occupied — ghi bởi cảm biến vật lý], `assigned_asset_id` [nullable — ghi khi người dùng quét QR gán asset vào slot])
|
||
- Mismatch giữa `sensor_status` và `assigned_asset_id` (vd sensor báo occupied nhưng không có asset nào được assign) = tín hiệu cần kiểm tra thủ công (phát hiện thất thoát/quên cập nhật)
|
||
|
||
**Kiến trúc:** tách stack IoT riêng (broker MQTT hoặc tận dụng Home Assistant đã định dùng cho KNX ở văn phòng mới, xem `../../processes/new-office-fitout.md`) khỏi app chính — lý do là giao thức (MQTT/CoAP), data shape (time-series), và network/bảo mật (nên tách VLAN như camera/access control) đều khác hẳn CRUD web app. Tích hợp qua **1 bridge job** đọc/ghi trạng thái vào Postgres của app chính (`RackSlot.sensor_status`) — app chính không cần biết MQTT/HASS, Postgres vẫn là nguồn sự thật duy nhất.
|
||
|
||
**Luồng pick-to-light (tương lai):**
|
||
1. Request duyệt → nhận hàng → asset gán vào 1 `RackSlot` cụ thể → trạng thái Request thêm state mới "Ready for Pickup"
|
||
2. Người tới quét số ticket (ServiceNow, nhập tay ở trên) tại trạm quét QR gắn cạnh tủ rack → gọi API tra `RackSlot` tương ứng
|
||
3. Trạm quét = **consumer độc lập thứ 2** của app (ngoài web frontend) → endpoint này cần thiết kế như API thật (auth riêng cho thiết bị, không dựa session web) — đây là trigger hợp lệ đầu tiên để có 1 API boundary rõ ràng, dù chưa cần tách hẳn backend
|
||
4. App → bridge → publish lệnh bật đèn đúng slot qua IoT stack
|
||
5. Người lấy xong → xác nhận (quét lại hoặc sensor tự phát hiện chuyển empty) → tắt đèn, đóng Assignment/Request
|
||
|
||
QR gán asset vào slot (bước 1, thủ công hàng ngày) **không phải IoT** — chỉ là quét mã bằng camera trình duyệt/máy quét cầm tay, ghi thẳng qua API hiện có của app chính, không cần MQTT.
|
||
|
||
## Case đặc biệt cần xử lý
|
||
|
||
- Nhân viên nghỉ việc → checklist thu hồi asset + license (offboarding)
|
||
- License seat-based dùng chung (vd Adobe CC theo pool chung của phòng ban)
|
||
- Tài sản luân chuyển giữa phòng ban / chi nhánh (không phải "delivery" mới, mà là re-assign)
|
||
- Asset hỏng → gửi RMA/sửa chữa → cần state "in repair" tách khỏi "assigned"
|
||
|
||
## Quyết định kiến trúc
|
||
|
||
- Quy mô hiện tại <100 người, IT team nhỏ → **web app tự build gọn nhẹ, monolith** (Next.js full-stack, frontend + API routes chung 1 app) — self-hosted, tương tự pattern app "IT Knowledge Base + Ticket Analyzer" đã build cho `it-support.md` (workspace ngoài repo này), theo nguyên tắc chung của workspace là ưu tiên self-host hơn cloud (xem `../../processes/new-office-fitout.md`). **Không tách backend/frontend riêng ở giai đoạn này** — xem lý do & trigger để tách ở dưới.
|
||
- DB: **Postgres, đã chốt** (2026-07-29). Lý do: đảm bảo scale sau này, Power BI connect native/ổn định tới Postgres (còn SQLite cần ODBC driver gượng ép + không hợp để BI query đồng thời lúc app đang ghi), và đã có kinh nghiệm sẵn với Postgres. Cách lộ dữ liệu cho Power BI: tạo Postgres views riêng cho report (vd `v_requests_report`, `v_license_utilization`) + 1 read-only DB user trỏ Power BI vào, **không cần expose API riêng cho việc này**.
|
||
- Cron job (vd gửi email cảnh báo license sắp hết hạn hàng ngày) **không cần tách backend riêng** — làm 1 script độc lập (`scripts/send-license-expiry-emails.ts`) query thẳng Postgres, lịch chạy qua Windows Task Scheduler (đã có tiền lệ ở `xibo-signage.md`, workspace ngoài repo này) hoặc container cron riêng trong Docker. Cần chốt sau: SMTP nào, ngưỡng cảnh báo bao nhiêu ngày, gửi cho ai.
|
||
- Cân nhắc thay thế cho MVP: Airtable/Google Form nếu muốn test quy trình trước khi build — nhưng approval workflow nhiều bước + audit trail sẽ gượng ép nếu để lâu dài trên low-code.
|
||
- Reverse proxy khi deploy: **Nginx Proxy Manager** (đã chốt 2026-07-30) — tự host, có UI quản lý + tự động Let's Encrypt. Chưa dựng ngay vì app còn ở local dev, chưa tới bước deploy production. Khi deploy, app này khả năng sẽ dùng chung 1 NPM instance với các service self-host khác trong workspace (vd Xibo) thay vì mỗi app 1 reverse proxy riêng — cần xác nhận lại lúc thực sự deploy.
|
||
- Postgres: **không expose ra internet qua domain** (đã chốt 2026-07-31) — dù kỹ thuật làm được giống các cloud DB (domain + TLS + password mạnh), việc đó bắt tự vận hành toàn bộ phần bảo mật mà cloud provider thường lo (firewall theo IP, TLS, fail2ban, vá lỗi định kỳ) trong khi DB chứa data công ty thật. Quyết định: **dev/test dùng Postgres local riêng từng máy** (Docker, không expose). Khi deploy production ở nhà (Proxmox), Postgres prod chạy **riêng biệt, không public** — nếu cần connect từ máy khác (vd máy công ty) thì qua **VPN/SSH tunnel**, không mở port DB ra ngoài.
|
||
- Docker-compose: **giữ chung 1 stack** (app + `db` cùng 1 `docker-compose.yml`) thay vì tách file riêng cho DB — đã note tên network/volume/container tường minh (`asset-license-mgmt-net`, `asset-license-mgmt-db-data`, `asset-license-mgmt-db`) sẵn từ đầu nên tách sau này (nếu có consumer thứ 2 độc lập cần DB — vd cron container, IoT bridge) vẫn rẻ: chỉ cần đổi network thành `external: true` ở cả 2 file, volume tự nhận lại theo tên, gần như không downtime.
|
||
|
||
### Khi nào nên tách backend riêng (chưa áp dụng, chỉ ghi lại trigger)
|
||
|
||
1. Có consumer thứ 2 thật sự cần gọi API độc lập ngoài web frontend (vd trạm quét QR pick-to-light — xem module Rack/Slot ở dưới; hoặc tích hợp ServiceNow sau này)
|
||
2. Có job cần chạy tách biệt khỏi request-response của web (cron email đã quyết là chưa cần tách, chỉ cần script + scheduler)
|
||
3. Cần hạ tầng/bảo mật khác biệt (vd module IoT rack — xem dưới, lý do khác hẳn: giao thức, data shape, network)
|
||
4. Team lớn tới mức có người riêng phụ trách backend
|
||
|
||
"Muốn tích hợp với hệ thống khác" **tự nó không phải lý do để tách** — ngược lại, tích hợp chặt (vd Rack/Slot dùng chung AssetItem) là lý do để **gộp chung** app/DB thay vì làm hệ thống riêng rồi nối lại qua API.
|
||
|
||
## Process / SOP
|
||
|
||
*(Chưa có — sẽ bổ sung khi bắt đầu implement: quy trình tạo request mới, checklist nhận hàng, checklist bàn giao có ký nhận, checklist offboarding thu hồi thiết bị)*
|
||
|
||
## Việc còn dở
|
||
|
||
- Thiết kế ERD chi tiết + wireframe màn hình
|
||
- Entity còn lại: `PurchaseOrder`, `Receipt`, `Assignment` (`Employee`, `AssetItem` đã xong 2026-07-31; `License` xong 2026-08-02, xem `../CLAUDE.md`)
|
||
- Consumable (2026-08-02): phần phân loại (`AssetCategory`, `ConsumableItem`) đã có spec đầy đủ (BA), sẵn sàng cho `pm` chia task. Luồng "xin + nhận" (`ConsumableRequest` hoặc tương đương) **BỊ CHẶN** — cần stakeholder trả lời các câu hỏi mở: (1) có cần approval không, ai duyệt; (2) ai được tạo request xin (tự xin cho mình hay xin hộ được); (3) có giới hạn số lượng tối đa/lần xin không; (4) hết hàng thì chặn hẳn / cho "nợ" chờ nhập thêm / fulfill một phần; (5) trừ tồn kho lúc tạo request hay lúc fulfill; (6) restock tồn kho qua luồng nào, ai được làm; (7) có cần ghi nhận người phát hàng/ký nhận không. Không tự đoán — xem chi tiết đầy đủ trong spec BA.
|
||
- Custom fields theo Category / "Additional Options" (2026-08-02, spec BA — xem "Custom fields theo Category" ở trên): thiết kế data model (`AssetFieldDefinition`/`AssetFieldValue`, EAV, gắn vào `AssetCategory`) đã rõ, nhưng **BỊ CHẶN** cho tới khi stakeholder trả lời ít nhất 3 câu hỏi mở: (1) field có bắt buộc (`required`) được không; (2) có cho sửa/xoá field definition sau khi category đã có asset dùng không, xử lý giá trị cũ ra sao; (3) áp dụng cho `ConsumableItem` không hay chỉ `AssetItem`. Chưa giao cho `pm`.
|
||
- Migrate 2 request đang có (`Firefly_License_Request.docx`, `Eric Pelatan.docx`, ở workspace `ubisoft/` ngoài repo này) vào hệ thống mới sau khi có DB
|
||
- (Xa hơn, chưa ưu tiên) Rack/Slot + IoT pick-to-light, cron email cảnh báo license hết hạn, Power BI report views
|
||
- Setup Gitea tự host ở nhà (2026-07-31) — dự định làm remote chính sau này, giữ GitHub làm backup off-site (đây là "sản phẩm dùng cả đời" của user, không nên chỉ có 1 bản duy nhất). Tạm thời vẫn push GitHub như hiện tại, chưa làm gấp.
|
||
|
||
## Ghi chú & bài học
|
||
|
||
- Migrate entity `Request` từ SQLite sang Postgres (2026-07-30) và setup ESLint đều đã **CONFIRMED xong** qua lượt verify độc lập (test-runner riêng, không phải agent vừa code tự chấm) — 6 test pass trên Postgres thật, lint sạch, type-check sạch. Chi tiết trạng thái nằm ở `../CLAUDE.md`, không duplicate ở đây.
|
||
|
||
---
|
||
Cập nhật lần cuối: 2026-08-02
|