- Next.js 14 (TS, App Router) monolith - Request entity (Draft state) via Prisma + Postgres, validated with Zod - ESLint configured - Tailwind v4 + shadcn/ui (new-york-v4 style) - Vitest test suite (6 tests, runs against throwaway Postgres DB per file) - Multi-agent dev workflow docs in CLAUDE.md and .claude/agents/
13 KiB
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.tsdùng rawpgClientđể 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òntailwind.config.ts— theme khai báo thẳng trong CSS qua@theme inline { ... }(src/app/globals.css),postcss.config.jschỉ còn plugin@tailwindcss/postcss(không cầnautoprefixerriêng nữa). shadcn config: stylenew-york-v4, base colorneutral, 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ùngradix-ui(package hợp nhất, không phải@radix-ui/react-slotriêng lẻ nữa) vàtw-animate-css(thaytailwindcss-animate).globals.csscòn@import "shadcn/tailwind.css"— 1 file CSS runtime do chính packageshadcn(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ớicomponents.jsonnà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>.tsmỗ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àmtoRecord()map từ shape Prisma (camelCase) sang shape public API (snake_case — vdemployee_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), checktypeof 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) — importprismatừ đó, 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) → Prismaenum, không phảiString+ 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.cssqua@theme inline { --color-*: var(--*); ... }— không còntailwind.config.ts. Biến CSS ở:root/.darkđã là giá trịoklch(...)đầy đủ,@theme inlinechỉ alias thẳng quavar(--x), không bọc thêmhsl(...)/rgb(...)(bài học từ bughsl(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.tsmirror theo lib file, chạy trên Postgres thật qua DB tạm per-test-file (xemtests/setup.ts) — không mock DB.beforeEachdọn bằngprisma.<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
- Host Windows này không có Node.js/npm cài native. Mọi
npm install/npm test/tscphải chạy qua Docker. - DB là Postgres (không còn SQLite — xem quyết định trong
../processes/asset-license-management.md), chạy quadocker-compose.yml(servicedb, networkasset-license-mgmt-net, imagepostgres:16-alpine). Trước khinpm test, phải start Postgres:docker compose up -d db(từ thư mục project) — khởi động Postgres local dev.- 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" - Test tự tạo/xoá một database Postgres tạm (
test_<uuid>) riêng cho mỗi lần chạy file test (xemtests/setup.ts), không đụng vào database dev chính.
DATABASE_URLmẫu nằm ở.env.example— copy thành.env(đã gitignore) nếu cần chỉnh local.
- Docker Desktop đã confirm hoạt động được trên máy này (dùng để chạy Xibo containers, xem
../xibo/). npm installtự chạyprisma generatequa hookpostinstall(xempackage.json) — không cần gọi tay trừ khi thay đổischema.prismamà không chạynpm installlại.pgvẫn còn trongdevDependencies— chỉ dùng cho hạ tầng test (tests/setup.tstạ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"):
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.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.- Nếu fail: đưa report của test-runner làm input cho một lượt
feature-implementermới để fix. Lặp lại bước 1–2 tới khi xanh hết. 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ỉ stateDraft) vớiPOST/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
Requestsang Prisma + Zod xong (2026-07-30):prisma/schema.prisma(modelRequest, enumRequestType),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ọiprisma migrate deploy),tests/setup.ts/tests/requests.test.ts(schema áp quaprisma migrate deploy, dọn dữ liệu quaprisma.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áctypetừTEXT+CHECKsang Postgres enum — tương đương semantics). npm audit: 1 "critical" nằm ởvitest(devDependency, CVE cầnvitest --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ọchsl()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
@latestxong (2026-07-30): bỏtailwind.config.ts(theme giờ khai trongglobals.cssqua@theme inline), đổipostcss.config.jssang@tailwindcss/postcss, đổi dependency (radix-uithay@radix-ui/react-slot,tw-animate-cssthaytailwindcss-animate, thêmshadcnlàm runtime dependency vìglobals.cssimportshadcn/tailwind.css),button.tsxviết lại theo registrynew-york-v4hiệ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 bughsl(oklch(...))), không còn gói v3 sót lại.- Rủi ro tồn đọng (dormant, chưa gãy):
button.tsxmớ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ềnrefvào<Button>, ref sẽ không được forward đúng cách. Hiện tại không chỗ nào trong code truyềnrefnê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ạiforwardRefnếu cần.
- Rủi ro tồn đọng (dormant, chưa gãy):
Việc còn dở
- Bump
vitestlên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — hiện không khai thác được vì project không chạyvitest --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 initsớm trước khi làm thêm entity mới. Button(shadcn) khôngforwardRef, 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