User request: 'cho hop dong dua cac thong tin lich su dieu chinh sang
duoi lich su duyet nhen'.
ContractDetailContent (Panel 2): xoa section 'Lich su dieu chinh' (cot
3/10 grid 7/3) → Chi tiet HD gio full-width. Remove import History +
ContractChangelogsTab.
WorkflowHistoryPanel (Panel 3): them section Lich su dieu chinh duoi
Lich su duyet. Import History icon + ContractChangelogsTab. Reuse same
component, chi doi vi tri render.
Mirror fe-admin + fe-user.
User request: 'cho tat ca cai nay the hien tren dung 1 man hinh nhe, cai
duyet va lich su thi dua sang panel 3'.
Panel 2 (PeDetailTabs): truoc 5 tab (Info/NCC/Items/Approvals/History).
Sau bo tabs, flat render 3 section stack doc voi divider + title uppercase:
Thong tin → NCC tham gia (N) → Hang muc + Bao gia (N)
Panel 3 (PeWorkflowPanel): truoc chi workflow timeline + transition btn.
Sau them 2 section ben duoi:
Workflow timeline → Lich su duyet (PeApprovalsSection) → Lich su thay doi
(PeHistorySection)
Export PeApprovalsSection + PeHistorySection tu PeDetailTabs — reuse
ApprovalsTab + HistoryTab logic cu, wrap them <h3> section title.
Dong bo ca fe-admin + fe-user (copy identical file).
Bug: click leaf 'Duyet' (/purchase-evaluations?type=2&pendingMe=1) khien
leaf 'Danh sach' (/purchase-evaluations?type=2) cung highlight cung luc.
Nguyen nhan: NavLink 'end' prop chi match pathname. 2 leaf cung pathname
/purchase-evaluations → ca 2 active.
Fix: custom isActive voi queryMatches helper — compare query string dang
key-value set (thu tu param khong quan trong). 2 leaf chi active khi
pathname + query dung khop.
Dong bo ca fe-admin + fe-user. Anh huong tat ca menu leaf co ?query=
variants: Ct_* (Danh sach /contracts?type=N vs Duyet /contracts?type=N&
pendingMe=1), Pe_* (tuong tu /purchase-evaluations), admin workflow leaf
Wf_* + PeWf_* (khong dinh vi path khong query params).
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ì 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>