Commit Graph

308 Commits

Author SHA1 Message Date
f216169039 [CLAUDE] FE-Admin+Domain+Infra+App: Workflows tab → sidebar menu items
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m37s
User request: 7 tab trong /system/workflows thành menu items riêng.

Domain:
- MenuKeys.WorkflowTypeLeaf(code) helper — `Wf_<TypeCode>` pattern

Infrastructure (DbInitializer):
- Seed 7 leaves dưới Workflows group (order 95..101), label matches
  ContractType (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ / Mua bán /
  Nguyên tắc NCC / Nguyên tắc Dịch vụ). Idempotent.

Application (GetMyMenuTreeQuery):
- Generalized inherit-perm logic: descendants of Contracts AND Workflows
  inherit parent CanRead flag. Single Workflows.Read grant → all 7
  Wf_* leaves visible; no per-leaf permission rows needed.

FE Layout (admin):
- resolvePath: Wf_<Code> → /system/workflows/<code>. Ct_* still hidden
  on admin side.

FE App.tsx:
- New route /system/workflows/:typeCode?

FE WorkflowsPage:
- Removed horizontal tab bar; type selection now comes từ URL param.
- Landing view (no param): 3-col grid card per type với active version
  badge — so admin có visual overview khi click top-level Workflows
  group without selecting a type.
- TYPE_CODE_TO_INT map drives URL→int conversion.

Result: click `Quy trình HĐ > HĐ Mua bán` trong sidebar → opens
/system/workflows/MuaBan directly với designer scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:49:42 +07:00
29dbac2051 [CLAUDE] Docs: STATUS update cho versioned workflow + nested menu user
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m38s
2026-04-22 09:37:53 +07:00
355bbe3a61 [CLAUDE] FE-Admin: Dialog size xl → lg (fix TS2322 CI build)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
2026-04-22 09:31:10 +07:00
e7e5f2d066 [CLAUDE] Domain+Infra+App+Api+FE-Admin: versioned workflow per ContractType
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m32s
User yêu cầu: mỗi loại HĐ có quy trình riêng với admin add roles + users
vào từng bước. Khi tạo version mới → HĐ tương lai chạy theo, HĐ cũ giữ
version cũ.

Domain:
- WorkflowDefinition (Code + Version + ContractType + IsActive + Steps)
- WorkflowStep (Order + Phase + Name + SlaDays + Approvers)
- WorkflowStepApprover (Kind: Role/User + AssignmentValue)
- Contract.WorkflowDefinitionId — pinned at creation
- WorkflowPolicyRegistry.FromDefinition() — build runtime policy từ DB

Infrastructure:
- EF config + migration AddVersionedWorkflows (3 table mới)
- DbInitializer.SeedWorkflowDefinitionsAsync: v01 per 7 ContractType,
  steps sinh từ hardcoded WorkflowPolicies (Role approvers).
- ContractWorkflowService.TransitionAsync: load pinned WorkflowDefinition
  → FromDefinition(), fallback cho HĐ cũ không có pin.

Application:
- CreateContractCommand pin WorkflowDefinitionId = active version cho type
- ContractFeatures.Get(id): load pinned def cho workflow summary
- WorkflowAdminFeatures: GetWorkflowAdminOverviewQuery (7 types + active
  + history + ContractsUsingCount), CreateWorkflowDefinitionCommand
  (validate payload, auto-increment version, deactivate old).

Api:
- GET /api/workflows trả overview
- POST /api/workflows tạo version mới (deactivate old)

FE /system/workflows:
- Tabs per 7 ContractType, mỗi tab hiện active version + lịch sử
- DefinitionCard: steps với badge role/user + SLA + archived indicator
  hiện "N HĐ còn chạy" cho version cũ
- WorkflowDesigner modal: form code/name/desc + danh sách steps
  (phase/name/SLA) + approvers (+ Role hoặc + User). Drop step ok.
  Clone từ version hiện tại để tạo v02 có điểm start sensible.
- Amber banner: HĐ cũ không bị ảnh hưởng khi tạo version mới

Invariants được giữ:
- Unique (Code, Version) index
- Chỉ 1 version IsActive per ContractType tại 1 thời điểm
- Set default sẽ auto xóa override → respect legacy override table
- Role-kind approvers drive transition guards; User-kind fallback
  DeptManager role cho v1 (user-level targeting = iteration 2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:57:41 +07:00
5e0f3801a1 [CLAUDE] Move nested-type menu → fe-user; Admin workflow config page
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m41s
User clarified: menu loại HĐ 3-level (Danh sách/Thao tác/Duyệt) thuộc
fe-user. Admin có page riêng để config quy trình per loại HĐ.

fe-admin Layout:
- filterForAdmin() drops Ct_* entries (hide nested type menu).
- Admin sidebar giờ về lại đơn giản: Dashboard / Master / Hợp đồng
  (leaf) / Forms / Reports / System.

fe-user Layout:
- Dynamic menu tree từ /menus/me (thay fixed USER_MENU hardcoded).
- Recursive MenuNodeRenderer (top-level expanded, nested collapsed).
- resolvePath user-specific: Ct_*_List → /my-contracts?type=X,
  Ct_*_Create → /contracts/new?type=X, Ct_*_Pending → /inbox?type=X.
- filterForUser drops admin-only entries (Master/System/Forms/Reports).
- Static USER_FIXED_TOP prepends "Hộp thư" leaf → /inbox.
- MyContractsPage + InboxPage đọc ?type=X param, filter client-side.

Workflow config (Admin side):
- Domain: WorkflowTypeAssignment entity (ContractType → PolicyName
  override). Registry.ForContractWithOverrides() prefer DB override
  else default.
- Infrastructure: EF config + migration AddWorkflowTypeAssignments,
  unique index trên ContractType. ContractWorkflowService load
  overrides dict mỗi transition. ContractFeatures load overrides khi
  build WorkflowSummaryDto.
- Application: GetWorkflowAdminOverviewQuery returns 7 types × current
  policy + available policies. SetWorkflowAssignmentCommand validate
  policy name tồn tại; nếu = default thì delete override (no stale row).
- Api: GET /api/workflows + PUT /api/workflows/{contractType}
  với policy "Workflows.Read" + "Workflows.Update".
- Menu: new key `Workflows` dưới System, label "Quy trình HĐ".
- FE /system/workflows: 7 card per type, dropdown Standard/SkipCcm +
  'Đã override' badge khi khác default, phase sequence timeline,
  explanation banner ở top. Iteration 2 note: admin-authored custom
  policies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:41:05 +07:00
48e91fe7ca [CLAUDE] Domain+Infra+App+FE-Admin: per-ContractType nested sidebar menu
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
User request: mỗi loại HĐ có menu riêng với 3 action Danh sách /
Thao tác / Duyệt.

Sidebar giờ 3-level under "Hợp đồng":
  Hợp đồng (group, expandable)
  ├── HĐ Thầu phụ (sub-group)
  │   ├── Danh sách → /contracts?type=1
  │   ├── Thao tác  → /contracts/new?type=1
  │   └── Duyệt      → /contracts?type=1&pendingMe=1
  ├── HĐ Giao khoán (sub-group)
  ├── HĐ NCC / Dịch vụ / Mua bán / Nguyên tắc NCC / Nguyên tắc DV
  └── ... (7 types × 4 = 28 new menu items)

BE:
- MenuKeys.cs: ContractTypeCodes array + helpers ContractTypeGroup/
  List/Create/Pending → key format Ct_<TypeCode>[_<Action>]
- DbInitializer.SeedMenuTreeAsync: loop seeds 28 entries under Contracts
- GetMyMenuTreeQuery.BuildChildren: descendants of `Contracts` inherit
  parent permission (avoid adding 28 rows to Permissions table per role)

FE:
- Layout.tsx recursive: MenuNodeRenderer dispatches group vs leaf by
  depth; nested groups collapsed by default (top-level expanded).
  Deeper levels get smaller padding/text + left border guide.
- Pattern-based resolvePath: Ct_<Type>_<Action> → URL with query.
- Contract type code → int map (matches Domain ContractType enum).
- ContractsListPage reads ?type + ?pendingMe, filters client-side.
  Header title + description reflect active filter. "← Tất cả loại"
  quick-reset button.
- ContractCreatePage new cho admin (copy từ fe-user), pre-select type
  từ ?type URL param.
- App.tsx route /contracts/new → ContractCreatePage.

Pure navigation UX; no new permissions needed. Admin + any role with
Contracts.Read see full menu; leaves click-through to filtered views.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:25:00 +07:00
fb3a410a1b [CLAUDE] Docs: STATUS update cho seed master data + MyDashboard
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m46s
2026-04-21 22:12:08 +07:00
6197c841bb [CLAUDE] App+Infra+FE-Admin: seed master data + MyDashboard widgets
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
Task 1 — Seed master data unblock UAT/demo:
- DbInitializer.SeedDepartmentsAsync: 9 departments từ QT-TP-NCC.docx
  (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) — reference data không phải demo.
- DbInitializer.SeedDemoMasterDataAsync: 5 demo suppliers (NCC VLXD, NTP
  Xây dựng, TĐ Hoàng Nam, DV Clean, CĐT Vingroup — covers cả 5
  SupplierType) + 3 demo projects (FLOCK01, SkyGarden, Industrial).
  Chỉ seed nếu tables empty — respect admin's real data khi họ add.

Task 2 — Roles CRUD đã có sẵn trong UsersPage (Shield icon button mở
dialog gán 12 roles từ AppRoles.cs). Skip.

Task 3 — MyDashboard role-specific widgets:
- GetMyDashboardQuery (Reports): returns DraftsInProgress (tôi là
  Drafter + phase soạn thảo), PendingMyApproval (phase eligible role
  tôi + không phải tôi drafter), DueSoon 24h, Overdue, DraftsTotalValue.
- Endpoint GET /api/reports/my-dashboard.
- FE MyDashboardRow ở đầu DashboardPage: 4 card hover → navigate.
  Admin ẩn row nếu tất cả = 0 (ERP noise reduction).
  'Đang soạn thảo' + 'Chờ tôi duyệt' clickable → /contracts?filter=...
  (filter param để wire lần sau; row hiện chưa implement).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:06:28 +07:00
4690cc3a81 [CLAUDE] Docs: STATUS update — PDF + workflow policy + form builder iter2 E2E verified
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m38s
2026-04-21 21:54:21 +07:00
cae4d84830 [CLAUDE] Domain+Infra+App+FE: dynamic workflow policy per ContractType
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s
Đọc QT-TP-NCC.docx: quy trình 9 bước chỉ áp dụng cho Thầu phụ/NCC/Tổ đội.
Dịch vụ/Mua bán/Nguyên tắc bypass CCM. Thay hardcoded dict bằng policy
registry.

Domain — WorkflowPolicy.cs:
- Record WorkflowPolicy { Name, Description, Transitions, PhaseSla,
  ActivePhases } — pure data, testable.
- WorkflowPolicies.Standard: 9-phase full (Thầu phụ/Giao khoán/NCC)
- WorkflowPolicies.SkipCcm: 7-phase (Dịch vụ/Mua bán/Nguyên tắc)
- WorkflowPolicyRegistry.For(type) map ContractType → policy
- WorkflowPolicyRegistry.ForContract(c) override nếu BypassProcurement
  AndCCM=true (instance-level escape hatch)

Infrastructure — ContractWorkflowService:
- Xóa hardcoded Transitions/PhaseSla dicts → load từ policy.ForContract
- TransitionAsync: validate qua policy.Transitions thay vì dict local
- Error message include policy.Name để debug dễ hơn
- GetPhaseSla trả SLA từ Standard policy (fallback — SLA hiện tại giống
  nhau giữa 2 policy)

Application — ContractDetailDto:
- Field mới `Workflow: WorkflowSummaryDto { PolicyName, Description,
  ActivePhases, NextPhases }` — FE dùng để render nút chuyển phase
  dynamic + timeline card.
- BuildWorkflowSummary helper trong ContractFeatures.

FE (both apps):
- Type WorkflowSummary + ContractDetail.workflow
- ContractDetailPage xóa hardcoded NEXT_PHASES — dùng
  c.workflow.nextPhases từ BE (single source of truth)
- WorkflowSummaryCard: timeline của ActivePhases với check/current/
  future states + policy name/description ở header
- Card hiển thị trong sidebar, phía trên "Lịch sử duyệt"

Docs:
- gotchas.md #21 marked RESOLVED (NEXT_PHASES sync không còn cần)

Foundation: sau này admin có thể edit policy qua UI khi chuyển sang DB-
backed policy — nhưng API contract (WorkflowSummaryDto) đã stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:31 +07:00
e45909712b [CLAUDE] App+Infra+FE-Admin: DynamicForm + .doc/.xls auto-convert on upload
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m17s
Tier 3 iter2 — form builder UI dùng FieldSpec thay raw JSON textarea.

FE:
- DynamicForm component — parse FieldSpec JSON (record of FieldDef với
  label/type/required/placeholder/hint/options) và render inputs
  dynamic theo type: text/textarea/number/date/currency/select.
- FormsPage render dialog thêm toggle Form ↔ JSON (segmented control).
  Mặc định Form mode khi template có FieldSpec, JSON mode khi không.
  Khi mở dialog cho row khác, reset formValues + chọn đúng default mode.
- parseFieldSpec helper trả { spec, error } — UI báo lỗi nếu JSON
  không parse được, fallback JSON textarea.

BE — generalize converter thành IDocumentConverter:
- IPdfConverter → IDocumentConverter (ConvertAsync(bytes, src, tgt, ct))
  — đủ gánh cả pdf, docx, xlsx targets.
- LibreOfficeDocumentConverter — 1 shell-out pattern cho mọi conversion
  (docx→pdf, doc→docx, xls→xlsx, xlsx→pdf), target arg truyền vào
  --convert-to.
- ExportTemplatePdfCommand update dùng "pdf" target.

Auto-convert .doc/.xls trên upload:
- Validator accept thêm .doc/.xls (thêm note "sẽ tự convert").
- UploadContractTemplateCommandHandler: nếu ext là doc/xls → read stream
  → converter.ConvertAsync → lưu file .docx/.xlsx thay vì format gốc.
  File rendering pipeline (DocxRenderer/XlsxRenderer) chỉ support docx/
  xlsx — convert đảm bảo consistent.
- Display FileName preserve original name nhưng đổi extension.

Unblock 3 file .doc legacy template — admin giờ upload .doc bình thường,
system tự convert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:35:05 +07:00
6bbd894d96 [CLAUDE] App+Infra+Api+FE-Admin: PDF export (LibreOffice headless)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m33s
Pipeline: template.docx → FormRenderer fill placeholders → LibreOffice
soffice --headless --convert-to pdf → PDF byte[] → File() stream to
browser.

Clean-arch split:
- Application: IPdfConverter abstraction (swap to QuestPDF/Aspose later
  without touching caller).
- Infrastructure: LibreOfficePdfConverter — shells out to soffice.exe
  path from config (Pdf:SofficePath, default
  `C:\Program Files\LibreOffice\program\soffice.exe` on Windows).
  Per-request temp workDir để tránh filename collision + -env:
  UserInstallation isolate mỗi conversion (chống "soffice already
  running" khi concurrent). Timeout 60s (configurable). Best-effort
  cleanup. Kill entire process tree nếu timeout.
- Application: ExportTemplatePdfCommand — reuses existing FormRenderer
  + pipes bytes through IPdfConverter. Same data dict signature as
  Render để UI code share.
- Api: POST /api/forms/templates/{id}/export-pdf (same JSON body as
  /render, returns PDF stream).

FE:
- useExport hook chung cho 2 endpoints (DRY render + export-pdf mutations)
- Render dialog thêm nút "Tải PDF" (outline variant) cạnh "Tải file gốc".
  Disabled khi mutation khác đang chạy.
- Hướng dẫn dialog nâng cấp: "file gốc để edit Word/Excel, PDF để
  in/gửi không chỉnh sửa được".

Ops: scripts/install-libreoffice.ps1 — silent MSI install 25.8.6 cho
VPS (đã chạy trên prod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:28:31 +07:00
acc134a2fd [CLAUDE] Docs: STATUS update cho Form template builder + Gitea 500 fix
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 2m23s
2026-04-21 21:21:46 +07:00
166d26c1d8 [CLAUDE] App+Api+FE-Admin: Form template builder (upload + edit + delete)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m44s
Admin giờ có thể quản lý template HĐ hoàn toàn qua UI — không cần dev
đụng vào file system hay seed data.

BE (FormFeatures.cs + FormsController.cs):
- UploadContractTemplateCommand (multipart): validate FormCode
  (regex [A-Za-z0-9._-]+, unique), file <= 10MB, ext .docx/.xlsx,
  FieldSpec phải là JSON hợp lệ hoặc null. Ghi file vào
  wwwroot/templates/{formCode}_{guid:N}.{ext} để tránh collision
  + path traversal.
- UpdateContractTemplateCommand: sửa metadata + FieldSpec + IsActive
  (không đụng file — chỉ DB).
- DeleteContractTemplateCommand: soft delete qua IsActive=false
  (historical contracts ref template này vẫn resolve).
- Endpoints: POST /api/forms/templates (multipart),
  PUT /api/forms/templates/{id}, DELETE /api/forms/templates/{id}.
  RequestSizeLimit 12MB (validator caps 10MB).

FE (FormsPage.tsx admin):
- PageHeader action button "Upload template" mở dialog mới
- Row actions: Download (render existing), Pencil (edit), Trash (xóa
  confirm) thay vì chỉ có 1 nút Render — row hover reveals clearly
- Upload dialog: file picker với file: pseudo-element brand styled,
  FormCode (required, font-mono), Tên, Loại HĐ select, Mô tả,
  FieldSpec JSON textarea với placeholder example
- Edit dialog: same fields minus file (FormCode disabled, edit chỉ
  cập nhật metadata), có checkbox Kích hoạt
- Shared form submit handler — same dialog cho upload (__new) + edit

Foundation sẵn cho form builder thật (render UI từ FieldSpec JSON
đang là text field — iteration sau sẽ parse + render form dynamic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:15:35 +07:00
c52186bed0 [CLAUDE] Docs: gotcha #25 — Install-WindowsFeature Web-WebSockets locks webSocket section in applicationHost
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m24s
2026-04-21 21:10:43 +07:00
93e107208c [CLAUDE] Docs: STATUS update cho SignalR realtime deploy
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
2026-04-21 21:03:33 +07:00
ea9ab5e352 [CLAUDE] App+Infra+Api+FE: SignalR realtime notifications E2E
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
Clean-arch split:
- Application: IRealtimeNotifier (PushToUserAsync, abstraction)
- Api: NotificationHub (/hubs/notifications, [Authorize]) +
  SignalRNotifier impl với IHubContext<NotificationHub>, uses
  Clients.User(userId) (default provider resolves NameIdentifier="sub")
- Infrastructure: NotificationPushInterceptor — SaveChangesInterceptor
  capture Notification entities state=Added trong SavingChanges,
  push qua IRealtimeNotifier trong SavedChanges sau khi commit thành
  công. Zero caller changes — handlers chỉ cần db.Add(Notification).
  Attached vào ApplicationDbContext cùng với AuditingInterceptor.

Auth:
- JWT config thêm OnMessageReceived event: read ?access_token= từ
  query string khi path = /hubs/* (WebSockets không set headers).
- SignalRNotifier singleton (stateless, chỉ delegate IHubContext).

FE (both apps):
- @microsoft/signalr 8.0.7 vào package.json.
- lib/realtime.ts: singleton connection với lazy start + automatic
  reconnect [0,2s,5s,10s,15s] + accessTokenFactory lấy từ localStorage.
- NotificationBell: useEffect subscribe 'notification-created' khi
  isAuthenticated. On push: invalidate query + toast.message. Fallback
  polling giảm từ 30s → 60s (realtime cover gap).
- AuthContext.logout: dynamic import stopConnection() — avoid leaking
  auth'd socket across users.

Result: ERP-grade feel. Contract transition → Drafter nhận toast ngay
trong vòng 100-300ms (same-origin WebSocket), không cần F5 hay polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:56:37 +07:00
2a851caa92 [CLAUDE] Docs: STATUS update cho Attachment upload + brand + content polish
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s
2026-04-21 20:49:45 +07:00
dc3f09b8d4 [CLAUDE] FE: drop unused Button import (fixes TS6133 CI error)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m38s
2026-04-21 20:42:48 +07:00
c8d0070770 [CLAUDE] App+Infra+Api+FE: Attachment upload E2E
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m40s
Foundation file-storage:
- IFileStorage interface (Application) — SaveAsync/OpenReadAsync/
  DeleteAsync/Exists. Future swap cho S3/Azure Blob không đổi caller.
- LocalFileStorage (Infrastructure) — resolve Uploads:RootPath từ
  config, path-traversal guard (resolved full path phải stay in root),
  tự tạo directory khi save.
- DI: singleton (stateless).
- Config: dev "uploads", prod "C:\inetpub\solution-erp\uploads".

CQRS:
- UploadContractAttachmentCommand: validate size <=20MB + MIME whitelist
  (pdf, doc/docx, xls/xlsx, png/jpg/jpeg/webp). Sanitize filename
  (strip path components + invalid FS chars + leading dots). Storage
  path: contracts/{contractId}/{attId}_{safeFileName}.
- DownloadContractAttachmentQuery: trả Stream + FileName + ContentType.
- DeleteContractAttachmentCommand: best-effort file delete sau DB remove
  (orphan cleanup job có thể sweep sau).

Api:
- POST /api/contracts/{id}/attachments — multipart/form-data, field
  'file' + form fields 'purpose' + 'note'. RequestSizeLimit 25MB
  (validator enforces 20MB).
- GET /api/contracts/{id}/attachments/{attId}/download — File() stream.
- DELETE /api/contracts/{id}/attachments/{attId}.

FE ContractAttachmentsSection (both apps, identical):
- Drag-drop zone với dragging highlight (brand-500 border + brand-50 bg)
- Purpose selector (DraftExport / ScannedSigned / SealedCopy / Other)
- List có icon per MIME (FileText/Image/File), filename, metadata
  (purpose · size · createdAt), download button (fetch blob + trigger
  browser save với auth header), delete button (confirm dialog)
- Empty state hint về use-case ("bản scan HĐ đã ký ở phase In ký…")

Integrated vào cả 2 ContractDetailPage — ngay dưới phần comments,
trước sidebar lịch sử duyệt.

Unblock E2E workflow: users giờ có thể upload bản scan ký (DangInKy),
scan đóng dấu (DangDongDau) — phase transitions có bằng chứng thật.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:37:35 +07:00
346bd5d644 [CLAUDE] FE: content polish — typography + PageHeader + Button/Input/Table
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m54s
Typography base (both apps):
- 14px base, line-height 1.55, letter-spacing -0.003em — Be Vietnam
  Pro render chắc hơn, dấu tiếng Việt rõ hơn
- h1/h2 tracking tighter (-0.018em), h1 weight 700, h2 weight 600
- Tabular numbers trong <table> (tự động align cột số)

PageHeader:
- Title text-[22px] thay vì text-xl — hierarchy mạnh hơn
- Border-bottom + pb-5 thay flat layout — content-area rõ vùng
- Description leading-relaxed + slate-500 — dễ đọc hơn

Button:
- shadow-sm + color-tinted shadow (brand/20, red/20) cho primary/
  danger — có chiều sâu
- active:translate-y-[0.5px] micro-press feedback
- Ring offset 2 (thay 1) + offset-white — focus ring tách rõ

Input/Select/Textarea:
- h-9 thay h-10 — phù hợp dense table layouts
- shadow-[inset_0_1px_0_...] — inset highlight tinh tế
- Focus: border-brand-500 + ring-brand-500/20 — 2 lớp chỉ báo
- Disabled: bg-slate-50 + opacity-70 — rõ disabled state

DataTable:
- rounded-xl + shadow-sm + border-200/80 — card feel nhẹ nhàng hơn
- Header: UPPERCASE text-[11px] tracking-wider — ERP enterprise look
- Row hover: bg-brand-50/40 (thay slate-50) — brand-tinted hover
- Padding tăng từ px-3 py-2 → px-4 py-2.5 — breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:16:53 +07:00
bf1fbe3870 [CLAUDE] FE: favicon dùng chữ 'S' thật từ logo Solutions
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m4s
Pipeline:
- Pixel-crop chữ 'S' script từ logo.png (x=0..86, y=0..100 — trước
  underline + tagline)
- Scale lên 2x thành 172x200 trên canvas 256x256 transparent,
  center có padding ~12%
- Save thành mark.png (6.8KB, transparent bg)
- Embed base64 vào favicon.svg với nền trắng rounded-r-48 (contrast
  với chữ S xanh brand) — scale mọi size
- index.html thêm apple-touch-icon + alternate PNG cho browsers
  không support SVG favicon

Kết quả: favicon giờ là glyph 'S' script thật của Solutions, không
phải font-rendered text nữa. Contrast trắng-xanh dễ nhận ra ở size nhỏ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:10:05 +07:00
4abb5596d5 [CLAUDE] FE-Admin+FE-User: brand identity từ Solutions logo
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m54s
Lấy logo gốc từ template docx (SOL-CCM-FO-002.05) và brand color
exact pixel-sampled #1F7DC1 từ chữ "Solutions".

Thay đổi:
- logo.png (407x145, từ header docx) đặt vào /public cả 2 app
- favicon.svg: "S" trắng trên nền vuông brand blue bo góc
- index.css: palette brand-50..900 generate quanh #1F7DC1 + accent
  red-500/600 cho ® mark + font Be Vietnam Pro (Google Fonts,
  designed cho tiếng Việt, diacritics đẹp) với fallback Inter
  + JetBrains Mono cho font-mono + tùy chỉnh scrollbar
- Layout sidebar: logo.png 32px + "Admin"/"ERP" subtitle (thay
  text "SOLUTION ERP" đơn điệu)
- LoginPage: gradient background brand-50 + 2 decorative orbs
  blur, rounded-2xl card + backdrop-blur, big logo 56px + subtitle
  tracking-[0.2em]
- index.html: lang="vi", title "Solutions ERP · Admin" / "Solutions
  ERP", theme-color #1F7DC1 cho mobile address bar, preconnect
  fonts.gstatic.com để load Google Fonts nhanh hơn

Tất cả màu hardcoded trong component đã dùng `brand-600` → tự
map sang palette mới, không cần đổi logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:57:45 +07:00
c73e3f904b [CLAUDE] FE: login error message hướng dẫn diagnose khi Network Error
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m55s
2026-04-21 15:46:04 +07:00
02b8d4be52 [CLAUDE] Docs: STATUS + session log cho prod go-live + ERP shell + Notifications
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m45s
2026-04-21 15:35:45 +07:00
397eb367e5 [CLAUDE] Scripts: SPA web.config thêm HTTP→HTTPS redirect (fix admin login Network Error)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
2026-04-21 15:33:01 +07:00
49c0ddc8f4 [CLAUDE] App+Domain+Infra+Api+FE: Notifications module end-to-end
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
Domain:
- Notification entity + NotificationType enum (stable ints)
- Nullable RefId cho correlation (contract, user, ...)

Infrastructure:
- NotificationConfiguration: bảng Notifications, index theo (UserId, ReadAt)
- NotificationService: ghi vào DbContext, không SaveChanges (để caller quyết
  định unit-of-work — đảm bảo atomic với domain mutation)
- EF migration AddNotifications

Application:
- INotificationService (Notify + NotifyMany)
- CQRS: ListMyNotifications / GetMyUnreadCount / MarkRead / MarkAllRead

Api:
- NotificationsController: GET /api/notifications + unread-count + mark-read

Integration:
- ContractWorkflowService emit notification tới Drafter khi HĐ chuyển phase
  (skip nếu actor chính là Drafter). Title + type theo phase đích:
  DaPhatHanh → ContractPublished, TuChoi → ContractRejected, khác →
  ContractPhaseTransition.

FE:
- Both NotificationBell (admin + user) dùng /api/notifications thật
  (thay cho derived-from-inbox MVP trước đó). 30s refetch, click mark-read,
  'Đọc hết' bulk action.

Foundation sẵn cho SignalR push + email outbox sau này — chỉ cần mở rộng
NotificationService mà không đổi caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:24:09 +07:00
6c0e20649a [CLAUDE] FE-Admin: PermissionsPage improved (search + stats + column bulk toggle + empty state)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
2026-04-21 15:17:58 +07:00
2b6f91c2b2 [CLAUDE] FE: TopBar + NotificationBell + UserMenu — ERP shell foundation
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Kiến trúc Layout giờ tách thành [sidebar] [topbar + content], foundation
để scale thêm module trong tương lai (HR, Accounting, Inventory...).

TopBar: title placeholder + NotificationBell + UserMenu (initials avatar
+ role badges + logout). UserMenu thay cho bottom-of-sidebar (cleaner).

NotificationBell:
- fe-admin: cảnh báo SLA (HĐ quá/sắp quá hạn, 24h window)
- fe-user: hộp thư chờ xử lý (items trong /contracts/inbox)
- refetchInterval: 60s
- Placeholder cho SignalR/email notifications thật sẽ thay bằng
  /api/notifications endpoint ở Tier 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:16:15 +07:00
2e43799046 [CLAUDE] FE: EmptyState component + MyContracts CTA empty state
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
2026-04-21 15:13:43 +07:00
c1c23619de [CLAUDE] FE: DataTable skeleton rows khi loading (thay 'Đang tải…' text đơn giản)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m36s
2026-04-21 15:06:40 +07:00
0e5b5cd670 [CLAUDE] FE-User: stat cards trên Inbox (total, sắp hạn, quá hạn, tổng giá trị)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
2026-04-21 15:05:42 +07:00
290936a0ca [CLAUDE] CICD+FE-Admin+FE-User: deploy pool-state guard + SlaTimer component
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
CICD: check app pool state before Stop-WebAppPool (idempotent).

FE: new SlaTimer component with color-coded countdown (emerald/amber/red)
and progress bar. Two variants:
- inline: used in list tables (Inbox, Contracts list x2, MyContracts)
- full: used in ContractDetail card with progress bar + deadline timestamp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:04:44 +07:00
b40da1e726 [CLAUDE] CICD: read appsettings template from source workspace (not publish output)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 2m22s
2026-04-21 14:58:51 +07:00
5709092e08 [CLAUDE] CICD: fresh node_modules per build (Vite 8 rolldown native binding platform resolve)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 2m38s
2026-04-21 14:53:16 +07:00
519ba85f22 [CLAUDE] CICD: use npm install (not ci) to resolve Vite 8 rolldown native binding on Windows
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m0s
2026-04-21 14:46:37 +07:00
489a0054fa [CLAUDE] CICD: drop GITHUB_PATH step (UTF-16 NUL issue) - PATH already set via NSSM
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m12s
2026-04-21 14:40:14 +07:00
10ae51902b [CLAUDE] CICD: re-trigger after task 32 stuck (runner restart race)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 10s
2026-04-21 14:37:40 +07:00
57a027501a [CLAUDE] CICD: use powershell (5.1) instead of pwsh (7) - not installed on VPS
Some checks are pending
Deploy SOLUTION_ERP / build-deploy (push) Waiting to run
2026-04-21 14:31:35 +07:00
5df883de00 [CLAUDE] CICD: re-trigger workflow after runner PATH fix
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 13s
2026-04-21 14:27:23 +07:00
ccfcfb4907 [CLAUDE] CICD: rewrite workflow for local deploy on self-hosted runner
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 5s
VPS runner has only windows-latest label (no ubuntu) and lives on the
same VPS as IIS, so WinRM + multi-job artifact handoff is unnecessary.

Changes:
- Single build-deploy job on windows-latest self-hosted
- Uses pre-installed Node 20 and .NET 10 SDK (avoids 500MB setup-*
  downloads per run)
- Deploys directly to C:\inetpub\solution-erp\ — no WinRM
- appsettings.Production.json rendered from template via secrets
  (ConvertFrom-Json + ConvertTo-Json keeps JSON structure intact)
- Fixed site name mismatch: SolutionErp-Api (was SolutionErpApi)
- Removed IIS_HOST/USER/PASSWORD secrets — no longer needed
- Added smoke test step hitting https://api.huypham.vn/health/live

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:25:02 +07:00
45452765e3 [CLAUDE] Scripts: fix setup-ssl.ps1 --installationsiteid flag
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 8s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
win-acme 2.2.9 with --target manual + --installation iis requires
--installationsiteid (not --siteid). --siteid only applies to the
iis target plugin, not the iis installation plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:19:26 +07:00
169e268b28 [CLAUDE] Scripts: rewrite 4 deploy PS1 ASCII-only for PS 5.1 compat
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 9s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
PowerShell 5.1 reads .ps1 files as locale codepage (not UTF-8 no BOM),
which corrupts multi-byte Vietnamese chars and breaks parsing. Rewrote
setup-iis-sites.ps1, setup-ssl.ps1, setup-gitea-runner.ps1, deploy-all.ps1
as ASCII-only. Also renamed $Host param to $HostName in Ensure-Site to
avoid collision with PowerShell built-in $Host automatic variable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:17:36 +07:00
85acf750b3 [CLAUDE] Scripts: update cho SQLEXPRESS instance + Invoke-Sqlcmd (no sqlcmd.exe binary needed)
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 9s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
- setup-sql-db.ps1: Server=.\SQLEXPRESS default, dung Invoke-Sqlcmd (SqlServer PS module) voi credential + TrustServerCertificate
- appsettings.Production.json.example: Server=.\SQLEXPRESS (voi escaped backslash JSON)
- DB_CONNECTION Gitea secret da update (qua API)

VPS Windows Server 2022 minimal, co VIETREPORT da chay 4 site, SQL instance SQLEXPRESS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:10:08 +07:00
1b7d8872ab [CLAUDE] Scripts: deploy-all.ps1 one-command gop 4 step
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 9s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
- Pre-check prerequisites (admin, sqlcmd, WebAdministration, .NET 10)
- Chay theo thu tu SQL → IIS → appsettings.Production.json (auto write tu template + ACL restrict) → SSL (voi confirm) → Runner
- Idempotent: chay lai khong pha
- -SkipSsl / -SkipRunner flag cho debug
- Summary voi next steps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:49:55 +07:00
b1a4571c86 [CLAUDE] VPS setup scripts + SSL + runner + FE prod config + master runbook
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 9s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
Scripts moi (PowerShell admin trên VPS Windows Server):
- setup-sql-db.ps1: tao DB SolutionErp + grant db_owner cho vrapp (user shared voi VIETREPORT). Idempotent.
- setup-iis-sites.ps1: app pool SolutionErp-Api (NoManagedCode + AlwaysRunning + no idle) + 3 site (SolutionErp-Api/Admin/User) voi host header, C:\inetpub\solution-erp\{api,fe-admin,fe-user,logs,uploads}. Placeholder index.html + SPA web.config voi URL rewrite fallback + security headers. Firewall rule. ACL grant AppPool identity Modify. Naming prefix SolutionErp-* tranh conflict VIETREPORT.
- setup-ssl.ps1: download win-acme v2.2.9 → issue cert Let's Encrypt 3 domain (api/admin/user.huypham.vn) qua HTTP-01 challenge + auto install IIS binding + HTTP→HTTPS redirect + scheduled task 90d renew.
- setup-gitea-runner.ps1: download act_runner.exe → register voi Gitea git.baocaogiaoduc.vn, install Windows service, labels windows-latest,self-hosted,windows,x64 (cho phep share voi VIETREPORT).

FE production config:
- fe-admin/.env.production + fe-user/.env.production: VITE_API_BASE_URL=https://api.huypham.vn
- fe-admin/src/lib/api.ts + fe-user/src/lib/api.ts: BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
  - Dev: empty prefix → /api qua Vite proxy :5443
  - Prod: https://api.huypham.vn/api (cross-origin CORS da config AllowedOrigins)

Docs:
- docs/guides/vps-setup.md MOI (master runbook): prereq, 4 script chay theo thu tu, set 5 Gitea secrets, first deploy, appsettings.Production.json pattern (file hoac user-secrets), smoke test 3 curl, post go-live checklist (doi admin password, rotate secrets chat-exposed, backup schedule, disable Swagger prod, monitor logs), table co-existence VIETREPORT
- CLAUDE.md root: add vps-setup.md reference

Gitea repo da setup (extern):
- https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp (private)
- Secrets set via API: IIS_HOST=103.124.94.38, IIS_USER=Administrator, DB_CONNECTION (voi vrapp password), JWT_SECRET placeholder
- CON THIEU: IIS_PASSWORD (Windows admin — user cung cap), JWT_SECRET real value (64-char tu vps-jwt-key.txt — user update qua Gitea UI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:37:34 +07:00
fba0754110 [CLAUDE] Config: appsettings.Production.json.example voi domain huypham.vn + policy 12-char
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 8s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled
- Template commit (not actual Production.json — bi gitignore de tranh leak secret)
- ConnectionStrings: User Id=vrapp, Password placeholder
- AllowedOrigins: https://admin.huypham.vn + https://user.huypham.vn
- Identity.Password.RequiredLength = 12 prod (dev still 8)
- Identity.Lockout: 15min, 5 fail max
- Khi deploy: cp appsettings.Production.json.example appsettings.Production.json tren server va set secret thuc te (Jwt.Secret, ConnectionStrings.Default password)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:27:57 +07:00
1b5ef2ed51 [CLAUDE] Phase5.1/3.2: IDOR filter + SLA auto-approve job + admin password warning
IDOR filter ContractsController:
- ListContractsQueryHandler + ICurrentUser: non-admin chi thay HD minh la Drafter hoac role eligible phase hien tai
- GetContractQueryHandler + ICurrentUser: throw ForbiddenException neu truy cap HD khong lien quan
- GetEligiblePhases() internal static trong ListContractsQueryHandler — mirror GetMyInboxQueryHandler.PhaseActorRoles (Drafter/DeptManager → DangSoanThao/DangDamPhan/DangInKy, ProjectManager+PRO+CCM+FIN+ACT+EQU → DangGopY, CostControl → DangKiemTraCCM, Director+AuthorizedSigner → DangTrinhKy, HrAdmin → DangDongDau)

SLA Expiry BackgroundService (Phase 3 iteration 2 partial):
- Infrastructure/HostedServices/SlaExpiryJob MOI: BackgroundService moi 15 phut (delay 30s startup)
- Query Contracts WHERE SlaDeadline < UtcNow AND Phase NOT IN (DaPhatHanh, TuChoi)
- Map phase → next (happy path). Goi IContractWorkflowService.TransitionAsync voi actorUserId=null + Decision=AutoApprove + comment 'AUTO: het SLA phase X (Nh qua han)'
- Try-catch tung contract, 1 fail khong block batch
- Log structured: 'SlaExpiryJob: auto-approved contract {Id} {From} → {To}'
- Package Microsoft.Extensions.Hosting added to Infrastructure
- DI register AddHostedService<SlaExpiryJob>

Admin password warning (Phase 5.1):
- DbInitializer.WarnDefaultAdminPasswordAsync: check CheckPasswordAsync voi AdminPassword default → log WRN '⚠️  Admin user vẫn dùng password mặc định. ĐỔI NGAY trong production!'
- Chain vao InitializeAsync sau cac seed

E2E verified:
- Admin GET /contracts → total 1 (see all)
- Drafter GET /contracts → total 0 (IDOR filter, chua tao HD nao)
- API startup log: '⚠️  Admin user admin@solutionerp.local vẫn dùng password mặc định'
- Build + TS check → pass

Docs:
- STATUS.md: Phase 5.1 hau nhu xong (IDOR + admin warning + SLA job tick), cumulative BE 3900 LOC
- migration-todos.md: tick Phase 5.1 IDOR + admin warning, Phase 3 iter 2 SlaExpiryJob + E2E non-admin + admin warning
- session log 2026-04-21-1730-idor-sla-job.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:15:14 +07:00
11e61c9c39 [CLAUDE] Phase5.1: Security headers + account lockout + Users management
Security hardening:
- Api/Middleware/SecurityHeadersMiddleware MOI: remove server fingerprint (Server, X-Powered-By, ...), add X-Content-Type-Options:nosniff, X-Frame-Options:DENY, Referrer-Policy:strict-origin-when-cross-origin, Permissions-Policy (disable geolocation/mic/cam/payment), X-Permitted-Cross-Domain-Policies:none, CSP (default-src 'self' + img data: + style inline for Tailwind + frame-ancestors 'none'). Skip CSP tren /swagger (dung inline script).
- Program.cs wire UseMiddleware SecurityHeadersMiddleware first in pipeline
- Infrastructure/DependencyInjection Identity options:
  - Password.RequiredLength config-driven (Identity:Password:RequiredLength, default 8 dev, override 12+ prod)
  - Lockout: DefaultLockoutTimeSpan (15min), MaxFailedAccessAttempts (5), AllowedForNewUsers=true — all config-driven
- LoginCommandHandler: IsLockedOutAsync check truoc → throw voi deadline message, AccessFailedAsync khi sai password, ResetAccessFailedCountAsync khi login thanh cong

Users management:
- Application/Users/UserFeatures.cs: 8 CQRS (ListUsersQuery paging+search, GetUserQuery, CreateUserCommand + Validator, UpdateUserCommand voi self-disable protection, AssignRolesCommand voi self-demote protection (khong tu go Admin), ResetPasswordCommand (invalidate refresh token + unlock), UnlockUserCommand)
- UserDto: Id, Email, FullName, IsActive, IsLocked (computed tu LockoutEnd), CreatedAt, Roles
- Api/Controllers/UsersController: 7 endpoint (Users.Read/Create/Update policies):
  - GET / (list paged), GET /{id}, POST /, PUT /{id}, PUT /{id}/roles, POST /{id}/reset-password, POST /{id}/unlock
- using alias ValidationException = Application.Common.Exceptions.ValidationException (fix ambiguity voi FluentValidation)

Frontend fe-admin:
- types/users.ts MOI: User type + AVAILABLE_ROLES 12 role (match BE AppRoles.cs) + RoleLabel Vietnamese
- pages/system/UsersPage.tsx MOI:
  - DataTable columns: Email (mono), FullName, Roles (badge chips voi Vietnamese label), IsActive (CheckCircle/XCircle), IsLocked (KeyRound red), CreatedAt
  - Actions per row (PermissionGuard Users.Update wrap): Gan role (Shield icon → Dialog grid 12 checkbox), Reset password (KeyRound → Dialog voi warning user se bi logout), Unlock (Unlock icon, chi hien khi isLocked), Toggle active (XCircle/CheckCircle)
  - Create user dialog: email + fullName + password (min 8) + grid 12 role checkbox
- Route /system/users vao App.tsx

E2E verified:
- Security headers present tren moi response (check qua curl -I)
- POST /api/users voi roles: [Drafter] → 201 + id
- GET /api/users → paged voi 2 user (admin + new test.drafter)
- TS check fe-admin → pass
- dotnet build → 0 errors

Docs:
- docs/STATUS.md: Phase 5.1 xong, cumulative BE 3700 LOC, 42 endpoints, 17 FE pages
- docs/HANDOFF.md: phase table update row Phase 5.1, last updated timestamp
- docs/changelog/migration-todos.md: tick 6 items Phase 5.1 + 4 items remaining (IDOR, deps scan, admin warning, Roles CRUD)
- docs/changelog/sessions/2026-04-21-1630-phase5-1-security-users.md: session log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
2026-04-21 13:06:46 +07:00
46a2cab788 [CLAUDE] Docs: tick Phase 5 Prep items in migration-todos
Follow-up: migration-todos Phase 5 section update bi miss trong commit truoc (Edit bi block boi system reminder). Apply lai:
- Tick 14 items Prep xong
- Split 'Deploy that (can Gitea URL)' va 'Phase 5.1 Security hardening'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:58:08 +07:00