BE:
- CreateContractFromEvaluationCommand: guard DaDuyet + SelectedSupplier
+ ContractId=null → tạo Contract draft mới với SupplierId/ProjectId/
DepartmentId kế thừa từ PE. GiaTri = sum(details.thanhTienNganSach).
DraftData = PE.PaymentTerms. Gen MaHopDong ngay + pin WorkflowDefinitionId
theo ContractType user chọn. Log Changelog cả 2 bảng (Contract +
PurchaseEvaluation), link 2 chiều PE.ContractId = contract.Id.
- ListApprovedPurchaseEvaluationsQuery: DaDuyet + ContractId=null cho
FE picker.
- 2 endpoint mới:
GET /api/purchase-evaluations/approved-pending-contract
POST /api/purchase-evaluations/{id}/create-contract
FE:
- PeDetailTabs InfoTab: nếu Phase=DaDuyet && !ContractId && SelectedSupplierId
→ banner emerald + button "Tạo HĐ từ phiếu" → CreateContractDialog
(pick ContractType dropdown 7 loại + TenHopDong + bypass CCM flag)
- Sau khi tạo → navigate /contracts/{newId}
- Mirror fe-user.
KHÔNG auto-map PE Details → Contract Details per-type (PE schema ≠ 7
ContractType details schemas — user điền lại sau). PE → Contract link
qua FK ContractId cho navigation + history.
User feedback: thay vì placeholder dashed nhỏ "Chi tiết sẽ hiện sau khi
tạo Header", show structure thật của Chi tiết section ngay từ đầu nhưng
disabled. User thấy trước layout columns + button add → trải nghiệm
liên tục, không bất ngờ khi switch sang edit mode.
## Component mới: ContractDetailsPreview
- Section title "Chi tiết ({TypeLabel})" + amber pill "🔒 Cần tạo Header trước"
- Table opacity-60 với:
- thead column headers per type (sync với HEADERS_BY_TYPE config)
- tbody empty state: Lock icon + "Tạo Header xong sẽ thêm được hạng mục"
- Disabled "+ Thêm dòng" button (cursor-not-allowed, slate-400 text)
## HEADERS_BY_TYPE config
7 type × column headers — duplicate nhỏ với ContractDetailsTab.tsx renderers
(acceptable: chỉ là labels visual, không logic).
## Reactive theo type
User đổi dropdown "Loại HĐ" → preview headers update tương ứng (state-driven).
## Build
- fe-user: tsc + vite pass (586ms)
- fe-admin: tsc + vite pass (709ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: hiển thị Edit + Xóa cho mọi row (kể cả Phase khác), nhưng
mờ và disabled khi không thao tác được — để user biết button TỒN TẠI mà
không bị bất ngờ phải hover row đúng phase mới thấy.
## Thay đổi
- Bỏ conditional render `{phase === DangSoanThao && ...}`
- Thêm canMutate = c.phase === DangSoanThao biến + className conditional:
- canMutate=true: text-slate-500 + hover brand/red + clickable
- canMutate=false: text-slate-300 + cursor-not-allowed + disabled
- Default opacity-60 (luôn visible nhẹ), group-hover:opacity-100 (rõ
khi hover)
- title tooltip thay đổi theo state — hint user lý do disable
- onClick guard early return nếu !canMutate (defense in depth)
Build: tsc + vite pass cả 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: 2 button trên row Panel 1 phải là Edit + Xóa, và CHỈ
hoạt động trong trạng thái nhập liệu/điều chỉnh (Phase = DangSoanThao).
## Thay đổi
- ✏ Pencil icon thay ExternalLink — hành động Edit (select Panel 2 form)
- 🗑 Trash2 — Xóa (giữ nguyên)
- Cả 2 button bọc trong `c.phase === DangSoanThao` conditional → Phase
khác (DangGopY, DangDamPhan, ...) → ẩn cả 2
- Lý do: BE chỉ cho update + delete khi Phase=DangSoanThao
(UpdateContractDraftCommand + DeleteContractCommand throw Conflict-
Exception nếu khác)
## UX
- Phase=DangSoanThao: hover row → 2 button fade in
- Phase khác: chỉ row click select Panel 2 (form sẽ render read-only +
banner amber "HĐ đã chuyển khỏi Đang soạn thảo")
Build: tsc + vite pass cả 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: thêm nút edit/action ở mỗi row trong list Panel 1 trang
Thao tác. Hiện absolute positioned ở góc phải-trên row, opacity-0 → 100
khi hover (group-hover). Sibling không nested để click không trigger
row select propagation.
## 2 button per row
- ⤴ ExternalLink → navigate /contracts/{id} (fullpage detail với
Workflow + History, khác Panel 2 chỉ có Edit form)
- 🗑 Trash2 → confirm() + DELETE /contracts/{id} (soft delete,
blocked sau DangInKy ở BE). Nếu xóa HĐ đang select → clear ?id=
## Implementation details
- pr-16 cho row button để chừa khoảng cho action group
- group-hover:opacity-100 transition (smooth fade in)
- Mutation invalidate ['my-contracts'] sau xóa thành công
- Toast success + getErrorMessage cho fail case (vd xóa HĐ đã qua DangInKy)
Build: tsc + vite pass cả 2 app (fe-user 515ms, fe-admin 937ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trang /contracts/new?type=X (menu "Thao tác") redesign từ single form
→ 2-panel: Panel 1 list HĐ theo type | Panel 2 Header form + Chi tiết.
## Layout
Panel 1 (320px) — flex column 3 vùng:
- Top: Search box (filter mã/tên/NCC client-side)
- Middle: List HĐ theo type (scroll, click row chọn)
- Bottom: + Thêm mới button (sticky, ring-brand khi active mode=new)
Panel 2 (flex) — 3 trạng thái theo URL:
- Empty state — chưa chọn HĐ và chưa bấm + Thêm mới
- ContractHeaderForm (mode=new) — form trống, sau Tạo HĐ draft
→ URL update ?id=newId chuyển edit mode
- ContractEditForm (id=abc) — form populated từ /contracts/{id}, +
section Chi tiết bên dưới (ContractDetailsTab reuse)
## URL state
- ?type=X → empty
- ?type=X&mode=new → form trống
- ?type=X&id=abc → edit form + Chi tiết
- ?type=X&q=keyword → search filter Panel 1
## Edit constraints
ContractEditForm respect UpdateContractDraftCommand limits:
- Editable khi Phase=DangSoanThao: Tên HĐ, Giá trị, Template, Nội dung
- Read-only luôn: Loại HĐ, NCC, Dự án, Bypass CCM (không đổi sau create
qua BE command hiện tại)
- Khi Phase != DangSoanThao: warning amber + tất cả input disabled,
nhưng Chi tiết section vẫn render để user xem (ContractDetailsTab tự
disable add/delete khi không phải draft)
## Components
ContractCreatePage.tsx (rewrite) — page entry
ContractHeaderForm — create mode (full fields editable)
ContractEditForm — edit mode (limited fields + Chi tiết section)
FormFields helper — shared form layout cho create
## Build verify
- fe-user: tsc + vite pass (374ms)
- fe-admin: tsc + vite pass (987ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: thay vì click tab để switch, hiển thị Chi tiết + Lịch sử
LUÔN ngay dưới Tổng quan content. Tỷ lệ cột 7 (Chi tiết) - 3 (Lịch sử
điều chỉnh).
## Thay đổi (apply 2 app)
ContractDetailContent.tsx:
- Bỏ TabsNav + tab state + TabButton helper
- Bỏ conditional render theo tab
- Tổng quan content (Info / Comments / Attachments) render flat đầu tiên
- Thêm grid lg:grid-cols-10 dưới cùng:
- lg:col-span-7 → ContractDetailsTab (line items)
- lg:col-span-3 → ContractChangelogsTab (timeline)
- Mobile (<lg): stack vertical 1 cột, Chi tiết trên, Lịch sử dưới
## Build verify
- fe-user: tsc + vite pass (17.23s)
- fe-admin: tsc + vite pass (8.12s)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Layout resolvePath map "Dashboard" key → "/inbox" cũ (coi inbox là
home), khiến menu "Tổng quan" và "Hộp thư" cùng navigate về /inbox →
user thấy interface giống nhau, không phân biệt được.
Fix:
- Tạo UserDashboardPage.tsx — overview cá nhân:
* Greeting với fullName
* 5-card "Của tôi" row (HĐ đang soạn / Chờ tôi duyệt / Sắp quá hạn /
Đã quá hạn / Tổng giá trị nháp) — dùng /api/reports/my-dashboard có sẵn
* Card click navigate vào page tương ứng (/my-contracts hoặc /inbox)
* Section HĐ gần đây — list 5 row với click → /my-contracts?id=X
- App.tsx: thêm route /dashboard + redirect "/" sang /dashboard
- Layout.tsx: Dashboard → /dashboard, logo link cũng chuyển về /dashboard
Build: tsc + vite pass (439ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: 7 group Ct_<Code> (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ
/ Mua bán / Nguyên tắc NCC / Nguyên tắc DV) trước đây expand tự do →
sidebar dài lê thê khi user mở nhiều. Mỗi group nên độc lập (accordion):
chỉ 1 group expand cùng lúc.
## Cách làm
### AccordionContext lifted to Layout
- Layout maintain `expandedCtCode: string | null` state
- React Context expose getter + setter cho MenuGroup
- MenuGroup detect key `Ct_<Code>` qua regex `/^Ct_([^_]+)$/`:
- Match → controlled mode: open = (expandedCtCode === code)
- Toggle = setExpandedCtCode(open ? null : code)
- Group khác (Hợp đồng top-level, Quy trình admin, ...) giữ behavior cũ
(independent local useState)
### Auto-expand theo URL ?type=
useEffect watch location.search:
- `/my-contracts?type=5` → INT_TO_TYPE_CODE[5] = "MuaBan" → expand HĐ Mua bán
- `/contracts/new?type=2` → expand HĐ Giao khoán
- `/inbox?type=3` → expand HĐ Nhà cung cấp
- URL không có ?type= → KHÔNG reset (giữ user-selected context)
### Visual: highlight active group
Ct_ group đang accordion-open: `bg-slate-50 text-slate-900` (subtle tint
để user biết group nào đang active trong 7 type).
## Build
fe-user: tsc -b + vite build pass (1888 modules, 1.08MB JS, 380ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Đọ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>
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>
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>
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>
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>