89 lines
15 KiB
Markdown
89 lines
15 KiB
Markdown
# Asset & License Management — Dev Notes
|
||
|
||
Web app nội bộ quản lý tài sản IT + license cho Ubisoft VN local IT. Luồng chính: Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire.
|
||
|
||
Business context, data model, luồng trạng thái, case đặc biệt: xem `../processes/asset-license-management.md` — đó là nguồn brainstorm chính, đừng duplicate nội dung đó ở đây, chỉ link tới.
|
||
|
||
## Stack (đã chốt)
|
||
|
||
- Next.js (TypeScript) full-stack
|
||
- DB: Postgres (đã chốt 2026-07-29, xem lý do trong `../processes/asset-license-management.md`)
|
||
- DB access: **Prisma** (ORM + migrations, đã chốt 2026-07-30 — lý do trong `../processes/asset-license-management.md`). Không viết raw SQL cho app logic nữa — ngoại lệ duy nhất là hạ tầng test (`tests/setup.ts` dùng raw `pg` `Client` để tạo/xoá throwaway database, đó là test infra chứ không phải data access layer).
|
||
- Validation: **Zod** (đã chốt 2026-07-30, đi cùng quyết định Prisma)
|
||
- UI/CSS: **Tailwind CSS v4 + shadcn/ui (CLI `@latest`, hiện tại v4.16.0)** — nâng cấp từ Tailwind v3 + `[email protected]` ngày 2026-07-30, xem lý do quyết định trong `../processes/asset-license-management.md`. Không còn `tailwind.config.ts` — theme khai báo thẳng trong CSS qua `@theme inline { ... }` (`src/app/globals.css`), `postcss.config.js` chỉ còn plugin `@tailwindcss/postcss` (không cần `autoprefixer` riêng nữa). shadcn config: style `new-york-v4`, base color `neutral`, CSS variables bật, `tailwind.config: ""` (`components.json`) — đây là style/preset cổ điển (Radix), **không phải** 8 preset thương hiệu hoá mới của CLI hiện tại (`nova/vega/maia/lyra/mira/luma/sera/rhea`, chọn qua `-p`), vốn ép dùng theme "Nova/Vega..." + tuỳ chọn thư viện primitive khác (`--base base` = Base UI, `--base aria` = React Aria) — không dùng các preset đó cho project này. Component dùng `radix-ui` (package hợp nhất, không phải `@radix-ui/react-slot` riêng lẻ nữa) và `tw-animate-css` (thay `tailwindcss-animate`). `globals.css` còn `@import "shadcn/tailwind.css"` — 1 file CSS runtime do chính package `shadcn` (npm) publish kèm nhiều utility (data-state variants, scroll-fade, shimmer...), là dependency thật (không phải chỉ dev tool) vì cần resolve lúc build. Khi thêm component mới: `npx shadcn@latest add <component>` (đã test non-interactive OK với `components.json` này qua flag `-y --overwrite`).
|
||
- Self-hosted (theo nguyên tắc chung của workspace — ưu tiên self-host hơn cloud, xem `../processes/new-office-fitout.md`)
|
||
|
||
## Coding conventions
|
||
|
||
Rút ra từ pattern thật đã dùng cho `Request` (xem `src/lib/requests.ts`, `src/app/api/requests/route.ts`, `prisma/schema.prisma`) — theo đúng pattern này cho các entity tiếp theo, đừng bịa pattern mới:
|
||
|
||
- **1 file schema Prisma** (`prisma/schema.prisma`) cho tất cả model — không tách nhiều file. Field trong model dùng camelCase (`employeeId`), map sang cột DB snake_case qua `@map("employee_id")`; tên bảng map qua `@@map("requests")` (snake_case, số nhiều).
|
||
- **1 file `src/lib/<entity>.ts` mỗi entity**, chứa cả 3 thứ trong cùng file: Zod schema + hàm validate trả về `ValidationResult` (`{ok:true,value}|{ok:false,error}`, không throw), các hàm CRUD gọi Prisma Client, và 1 hàm `toRecord()` map từ shape Prisma (camelCase) sang shape public API (snake_case — vd `employee_id`, `created_at`). Ranh giới cố ý: nội bộ TS/Prisma dùng camelCase, API trả ra ngoài dùng snake_case.
|
||
- **API route** (`src/app/api/<entity>/route.ts`) giữ mỏng: parse JSON trong try/catch (400 nếu invalid JSON), check `typeof body === "object" && body !== null` (400 nếu không), gọi hàm validate của lib, gọi hàm CRUD, trả `NextResponse.json(...)` với status code đúng (400 validation lỗi, 201 tạo mới, 200 list). Không viết business logic trong route handler.
|
||
- **Prisma Client**: dùng singleton ở `src/lib/prisma.ts` (global-guard chống hot-reload spawn nhiều instance) — import `prisma` từ đó, không tự `new PrismaClient()` ở nơi khác.
|
||
- **Enum có tập giá trị cố định gắn với domain** (vd `type: asset|license`) → Prisma `enum`, không phải `String` + check tay.
|
||
- **Màu/theme Tailwind**: từ khi lên Tailwind v4 (2026-07-30), theme khai báo thẳng trong `src/app/globals.css` qua `@theme inline { --color-*: var(--*); ... }` — không còn `tailwind.config.ts`. Biến CSS ở `:root`/`.dark` đã là giá trị `oklch(...)` đầy đủ, `@theme inline` chỉ alias thẳng qua `var(--x)`, **không bọc thêm** `hsl(...)`/`rgb(...)` (bài học từ bug `hsl(oklch(...))` hồi còn Tailwind v3 + `tailwind.config.ts`, đã fix rồi bỏ luôn file đó khi lên v4).
|
||
- **Test**: 1 file `tests/<entity>.test.ts` mirror theo lib file, chạy trên Postgres thật qua DB tạm per-test-file (xem `tests/setup.ts`) — không mock DB. `beforeEach` dọn bằng `prisma.<model>.deleteMany()`.
|
||
- Schema mới/đổi schema → tạo migration Prisma thật (`prisma migrate dev --name <mô_tả>`), không sửa tay SQL.
|
||
|
||
## Môi trường
|
||
|
||
Project này được dùng từ **nhiều máy khác nhau** (máy Windows công ty, VM Linux ở nhà qua Proxmox) — **luôn kiểm tra host hiện tại trước khi giả định cách chạy npm**, đừng áp cứng 1 cách cho mọi nơi.
|
||
|
||
- **Máy Windows công ty** (`C:\Users\thoangminh\Desktop\ubisoft\asset-license-mgmt`): **không có Node.js/npm cài native**. Mọi `npm install`/`npm test`/`tsc` phải chạy qua Docker (`node:20` container, xem cú pháp bên dưới). Docker Desktop đã confirm hoạt động được trên máy này (dùng để chạy Xibo containers, xem `../xibo/`).
|
||
- **VM Linux ở nhà (Proxmox)**: nếu VM có Node.js cài native (kiểm tra bằng `node -v`/`which node` trước), chạy `npm install`/`npm test`/`tsc` **trực tiếp**, không cần wrap qua Docker — chỉ Postgres mới cần Docker (`docker compose up -d db`), vì đó là quyết định kiến trúc (Postgres qua container), không phải vì thiếu Node. Nếu VM đó cũng không có Node, dùng lại cách wrap Docker như máy Windows.
|
||
- DB luôn là Postgres qua `docker-compose.yml` (service `db`, network `asset-license-mgmt-net`, image `postgres:16-alpine`) — **cái này giống nhau ở mọi máy**, không phụ thuộc host có Node native hay không. Trước khi `npm test`, phải start Postgres: `docker compose up -d db` (từ thư mục project).
|
||
- Nếu phải wrap qua Docker (máy không có Node native): chạy npm trong container gắn cùng network để resolve được hostname `db`:
|
||
`docker run --rm --network asset-license-mgmt-net -v "<project dir>:/app" -w /app -e DATABASE_URL="postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt" node:20 sh -c "npm install && npm test"`
|
||
- Nếu Node native: set `DATABASE_URL` (xem `.env`/`.env.example`) rồi chạy thẳng `npm install && npm test` — Postgres vẫn phải resolve qua `db` nếu chạy trong Docker network, hoặc `localhost`/port map nếu compose có publish port (hiện `docker-compose.yml` **không** publish port ra host, cần thêm port mapping hoặc chạy npm trong cùng Docker network nếu muốn gọi từ ngoài container).
|
||
- Test tự tạo/xoá một database Postgres tạm (`test_<uuid>`) riêng cho mỗi lần chạy file test (xem `tests/setup.ts`), không đụng vào database dev chính.
|
||
- `DATABASE_URL` mẫu nằm ở `.env.example` — copy thành `.env` (đã gitignore) nếu cần chỉnh local, riêng cho từng máy.
|
||
- `npm install` tự chạy `prisma generate` qua hook `postinstall` (xem `package.json`) — không cần gọi tay trừ khi thay đổi `schema.prisma` mà không chạy `npm install` lại.
|
||
- `pg` vẫn còn trong `devDependencies` — **chỉ** dùng cho hạ tầng test (`tests/setup.ts` tạo/xoá throwaway DB), không phải data access layer của app (đó là Prisma). Đừng nhầm là app vẫn dùng raw SQL.
|
||
|
||
### Lưu ý về việc gọi custom agent (feature-implementer/test-runner/code-reviewer)
|
||
|
||
Các agent định nghĩa ở `.claude/agents/` trong thư mục **này** (`asset-license-mgmt/`) chỉ được Claude Code nhận diện làm `subagent_type` nếu phiên Claude Code được mở với working directory **là chính thư mục này** (hoặc thư mục con của nó) — Claude Code quét `.claude/agents/` từ cwd đi lên tới repo root, **không quét xuống** thư mục con. Nếu mở Claude Code từ `ubisoft/` (thư mục cha, gộp nhiều project), 3 agent này sẽ báo "not found". Cách đúng: `cd` vào `asset-license-mgmt/` rồi mới chạy `claude` khi làm việc trên project này.
|
||
|
||
## Multi-agent dev workflow
|
||
|
||
Mục tiêu: không để một agent vừa viết code vừa tự chấm bài mình — tách vai trò để bug bị bắt lại thay vì được tự "cho qua".
|
||
|
||
Với mỗi task cụ thể (vd "thêm API tạo Request", "màn hình bàn giao asset"):
|
||
|
||
1. **`feature-implementer`** — nhận đúng 1 task, viết code + test đi kèm trong cùng lượt. Không tự tuyên bố "đã pass" — chỉ báo cáo đã đổi gì, giả định gì, và cái gì cố tình để ngoài scope.
|
||
2. **`test-runner`** — spawn **riêng**, không phải cùng agent vừa code. Chạy toàn bộ test suite + lint + type-check, báo lỗi cụ thể kèm file:line. Không tự sửa code.
|
||
3. Nếu fail: đưa report của test-runner làm input cho một lượt `feature-implementer` mới để fix. Lặp lại bước 1–2 tới khi xanh hết.
|
||
4. **`code-reviewer`** (optional, dùng cho thay đổi lớn/rủi ro — vd đổi state machine của Request, logic seat-count license) — review diff cuối cùng trước khi commit, tìm bug logic/security, không tự sửa.
|
||
|
||
### Quy tắc cứng
|
||
- Implementer và test-runner luôn là 2 lượt spawn riêng biệt cho cùng 1 task — không gộp.
|
||
- test-runner không được tự sửa file, kể cả khi lỗi trông "hiển nhiên".
|
||
- Nếu implementer thấy task mơ hồ, chọn giả định nhỏ nhất hợp lý và ghi rõ trong report thay vì dừng lại hỏi — nhưng nếu ảnh hưởng luồng trạng thái Request/quyền approval thì phải hỏi trước khi code.
|
||
|
||
## Trạng thái hiện tại
|
||
|
||
- **Task 1 xong**: đã bootstrap Next.js (TS, App Router) project thật, entity `Request` (chỉ state `Draft`) với `POST/GET /api/requests`.
|
||
- **Migrate SQLite → Postgres xong**: CONFIRMED pass qua test-runner độc lập.
|
||
- **ESLint setup xong**: CONFIRMED pass sạch qua test-runner độc lập.
|
||
- **Retrofit `Request` sang Prisma + Zod xong (2026-07-30)**: `prisma/schema.prisma` (model `Request`, enum `RequestType`), `src/lib/prisma.ts` (singleton client), `src/lib/requests.ts` (Zod schema thay hàm validate tay, Prisma Client thay raw SQL), `scripts/migrate.ts` (gọi `prisma migrate deploy`), `tests/setup.ts`/`tests/requests.test.ts` (schema áp qua `prisma migrate deploy`, dọn dữ liệu qua `prisma.request.deleteMany()`). `src/lib/db.ts` (raw pg Pool cũ) đã xoá. **CONFIRMED qua test-runner độc lập**: 6/6 test pass trên Postgres thật, type-check sạch, lint sạch. Migration SQL đối chiếu khớp 1:1 với schema cũ (chỉ khác `type` từ `TEXT+CHECK` sang Postgres enum — tương đương semantics).
|
||
- `npm audit`: 1 "critical" nằm ở `vitest` (devDependency, CVE cần `vitest --ui` đang chạy — project không dùng UI mode nên không khai thác được trong cách dùng hiện tại). Rủi ro thực tế thấp, chưa cần fix gấp — xem "Việc còn dở".
|
||
- **Tailwind + shadcn/ui setup xong (2026-07-30, ban đầu trên v3)**: `postcss.config.js`, `src/app/globals.css`, `components.json`, `src/lib/utils.ts` (`cn()`), `src/components/ui/button.tsx` (component mẫu để xác nhận pipeline chạy được). Có 1 bug phát hiện qua verify độc lập (màu token bị bọc `hsl()` chồng lên giá trị `oklch()` sẵn có → CSS không hợp lệ, silent fail lúc render dù build/lint/tsc đều xanh) — đã fix và CONFIRMED qua verify độc lập lần 2.
|
||
- **Nâng cấp lên Tailwind v4 + shadcn CLI `@latest` xong (2026-07-30)**: bỏ `tailwind.config.ts` (theme giờ khai trong `globals.css` qua `@theme inline`), đổi `postcss.config.js` sang `@tailwindcss/postcss`, đổi dependency (`radix-ui` thay `@radix-ui/react-slot`, `tw-animate-css` thay `tailwindcss-animate`, thêm `shadcn` làm runtime dependency vì `globals.css` import `shadcn/tailwind.css`), `button.tsx` viết lại theo registry `new-york-v4` hiện tại. **CONFIRMED qua test-runner độc lập**: install/build/tsc/lint sạch, 6/6 test Request vẫn pass (không liên quan tới UI), CSS đã compile xác nhận màu sạch (không còn kiểu bug `hsl(oklch(...))`), không còn gói v3 sót lại.
|
||
- **Rủi ro tồn đọng (dormant, chưa gãy)**: `button.tsx` mới bỏ `React.forwardRef` (convention của registry hiện tại là cho React 19), nhưng project đang pin **React 18.3.1** — nếu sau này có chỗ truyền `ref` vào `<Button>`, ref sẽ không được forward đúng cách. Hiện tại không chỗ nào trong code truyền `ref` nên chưa phát sinh lỗi — nhưng cần nhớ khi build UI thật, tránh `<Button ref={...}>` hoặc phải tự thêm lại `forwardRef` nếu cần.
|
||
|
||
## Việc còn dở
|
||
|
||
- Bump `vitest` lên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — hiện không khai thác được vì project không chạy `vitest --ui`, nhưng nên dọn khi có breaking-change window) — không khẩn cấp.
|
||
- Rà lại advisory riêng của `next` (prod dependency, có vài high/moderate — vd SSRF qua WebSocket upgrade, middleware bypass) khi có dịp, tách biệt khỏi đợt retrofit Prisma/Zod này.
|
||
- Test coverage gaps (chưa block gì, chỉ là follow-up): chưa test malformed JSON body, chưa test JSON body không phải object (array/string).
|
||
- Chưa có git repo cho project này — chưa có version history/rollback cho bất kỳ thay đổi nào ở trên. Nên `git init` sớm trước khi làm thêm entity mới.
|
||
- `Button` (shadcn) không `forwardRef`, React đang pin 18 — dormant risk, xem note ở "Trạng thái hiện tại".
|
||
- Các entity còn lại: `Employee`, `AssetItem`, `License`, `PurchaseOrder`, `Receipt`, `Assignment`
|
||
- Các state tiếp theo của Request: Pending Approval → Approved → Ordered → Received → Delivered → Closed, và nhánh Rejected/Cancelled
|
||
- UI (hiện chỉ có placeholder home page)
|
||
- Auth/phân quyền (ai được approve request, ai được xác nhận nhận hàng/bàn giao)
|
||
|
||
---
|
||
Cập nhật lần cuối: 2026-07-30
|