Types (fe-{admin,user}/src/types/purchaseEvaluation.ts):
- ApprovalWorkflowOptions type (6 boolean Allow* flag)
- WorkflowReturnMode const-object {OneLevel,OneStep,Assignee,Drafter}
- PeDetailBundle +workflowOptions field (null nếu V1 legacy)
PeWorkflowPanel.tsx F1 (mirror 2 app):
- State returnMode + returnTargetUserId thêm vào transition mutation payload
- Dialog Trả lại render radio list 1-4 mode enabled theo workflowOptions:
• Trả về 1 Cấp trước (lùi pointer trong cùng Bước, peer review)
• Trả về 1 Bước trước (Cấp cuối Bước trước nhận lại)
• Trả về Người chỉ định (pick từ dropdown NV đã ký levelOpinions)
• Trả về Người soạn thảo (default Drafter S17 fallback)
- Banner amber rounded box dưới radio list mô tả hành vi mode chọn
- onSuccess reset returnMode về Drafter + returnTargetUserId null
PeDetailTabs.tsx F2 (mirror 2 app):
- State skipToFinal + allowSkipToFinal (từ workflowOptions)
- submitForApproval mutationFn accept opts.skipToFinal → POST body
- Workspace action bar: thêm checkbox violet "Gửi thẳng Cấp cuối (skip trung gian)"
hiển thị conditional theo allowSkipToFinal + canSubmitForApproval
- Confirm dialog message dynamic: "Gửi thẳng" warning vs default tuần tự
- Button label dynamic: "Lưu & Gửi thẳng CẤP CUỐI →" vs "Lưu & Gửi Duyệt →"
PeDetailTabs.tsx F3 (mirror 2 app):
- useAuth import + compute approverEditMode (phase=ChoDuyet +
workflow.AllowApproverEditDetails + actor match currentApproval.approvers)
- itemsReadOnly = readOnly && !approverEditMode → ItemsTab nhận
- Banner violet "ⓘ Bạn được phép chỉnh sửa Hạng mục/NCC/Báo giá" khi
approverEditMode + readOnly (Duyệt menu) — UX nhắc về quyền extended
InfoTab + NccSelectorRow + BudgetFieldRow GIỮ strict isEditablePhase (KHÔNG
trong F3 scope — Header section + Section 3 winner KHÔNG cho Approver edit).
Verify:
- npm run build × 2 app pass (fe-user 7.52s, fe-admin 499ms cached)
- 0 TS6 err, warning chunk size pre-existing
- BE Chunk B đã accept skipToFinal + returnMode + returnTargetUserId trong
TransitionPurchaseEvaluationCommand → wire E2E complete
Pending Chunk E: Docs schema-diagram §14 update + STATUS + HANDOFF + session log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 10: chọn NCC từ dropdown master → auto-load các field
đã có sẵn (contactPerson/phone/email/note) vào form, đỡ phải nhập tay lại.
FE-only mirror fe-admin + fe-user.
AddSupplierDialog dropdown "NCC (master)" onChange:
- Lookup suppliers.data find(s => s.id === selectedId) → master row
- setForm prev → ghi đè 4 field:
* contactName ← picked.contactPerson ?? ''
* contactPhone ← picked.phone ?? ''
* contactEmail ← picked.email ?? ''
* note ← picked.note ?? ''
- KHÔNG đụng displayName / paymentTermText / thanhTien (manual cho user)
- Hint "✓ Đã tự điền từ Master — bạn có thể sửa lại nếu cần." text-[10px]
text-emerald-600 dưới dropdown khi đã chọn supplier
Mapping master Supplier → PE.Supplier fields (skip address vì không có
field tương ứng — có thể nhét vào note nếu user cần, manual).
User vẫn override các field auto-fill được sau đó (input bình thường).
Đổi supplier giữa lúc đã chỉnh tay → re-fill từ master mới (mặc định ghi đè).
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 6 screenshot: chế độ "Nhập tay (không link)" Section 2
b. Ngân sách vẫn còn input "Tên (vd Tạm tính T11/2025)" cùng số tiền. User
chỉ cần nhập số tiền — bỏ Tên + áp VND format consistent.
3 file × 2 app = 6 file FE update:
- PeDetailTabs.tsx BudgetFieldRow (Section 2 detail editor)
- PeWorkspaceCreateView.tsx (workspace mode "new")
- PeHeaderForm.tsx (Create/Edit header page)
Mỗi file:
- Drop Input "Tên ngân sách" UI khỏi manual mode (state field giữ '' để
backward compat — BE save luôn null)
- Manual mode UI giờ chỉ 1 input số tiền (max-w-xs):
* type="text" inputMode="numeric" + value={formatVndInput(amount)}
* onChange={parseVnd} strip non-digit → number
* Suffix "đ" tuyệt đối inset-y-0 right-3
* Hint "VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)"
- Helpers parseVnd + formatVndInput inline mỗi file (mirror PeDetailTabs)
PeDetailTabs BudgetFieldRow cleanup:
- Drop state manualName + setManualName
- Drop manualName từ dirty check
- Save payload: budgetManualName: null luôn (không phụ thuộc state)
- Hủy thay đổi: drop reset manualName line
Read-only display (legacy data) giữ ev.budgetManualName nếu data cũ có tên
(đoạn render khi !canEdit) — không xóa hiển thị, chỉ ẩn input UI.
BE schema KHÔNG đụng — endpoint PUT /pe/:id vẫn nhận budgetManualName field,
chỉ FE luôn gửi null.
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 3: "Tạm thời chỉ cần nhập số tiền vào là đc, không cần
3 cột có VAT / ko VAT / tổng. 2 cột kia ẩn đi, chỉ 1 cột nhập tiền duy nhất."
FE-only mirror fe-admin + fe-user:
NCC inline table HangMucCard — bỏ 2 th + 2 td:
Trước: NCC | Liên hệ | Điều khoản TT | File báo giá | ĐG chưa VAT | ĐG có VAT | Thành tiền | Action
Sau: NCC | Liên hệ | Điều khoản TT | File báo giá | Số tiền | Action
QuoteDialog — đơn giản hóa form:
Trước: 3 input (Đơn giá chưa VAT / ĐG có VAT / Thành tiền auto-calc) + Ghi chú
+ display khoiLuong info
Sau: 1 input "Số tiền" (autoFocus) — map thẳng vào thanhTien field
Schema BE giữ nguyên (bgVat / chuaVat / note vẫn POST):
- Row mới: bgVat=0, chuaVat=0, note=''
- Existing: giữ giá trị cũ
Bỏ prop khoiLuong (không dùng — không còn auto-calc thanhTien = chuaVat × khoiLuong)
Bỏ updateAndRecalc helper
KHÔNG đụng schema BE — endpoint POST /purchase-evaluations/{id}/quotes giữ
nguyên payload shape, chỉ FE rút gọn input mặt người dùng nhập.
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback Session 20 turn 2:
1. "Chỗ ý kiến vẫn hiển thị ô vuông như trước nhé" — revert visual về cards
grid-cols-2 mirror S19 (Chunk C cũ dùng vertical list inline không phải
ô vuông như trước).
2. "Số bước duyệt khác số người duyệt trong 1 bước, check lại" — counter cũ
`{opinions.length}/{totalApprovers}` sai semantic vì OR-of-N (mỗi Cấp chỉ
cần 1 NV ký, không cần ký tất cả NV). totalApprovers đếm tổng NV gây hiểu
lầm.
Fix (FE-only mirror fe-admin + fe-user):
- StepOpinionsBox body chuyển từ `space-y-2` (vertical list) sang
`grid grid-cols-1 md:grid-cols-2 gap-3` — mỗi opinion = 1 card đầy đủ
border-emerald-200 + bg-white + p-3 (mirror visual S19 LevelOpinionBox).
- StepOpinionEntry restore styling đầy đủ:
- Header: "Cấp N — Tên NV" font-semibold + admin override badge amber +
"✓ Đã duyệt" emerald rounded-full badge
- Body: comment text-sm
- Footer: signedAt border-t separator (như S19)
- Counter mới: `{signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers}
NV tham gia` — đếm Cấp distinct (Set unique levelOrder) thay vì count NV.
Tooltip giải thích "OR-of-N" cho user hiểu.
- KHÔNG đụng schema Mig 26 (vẫn UPSERT 1 row / Level qua Service).
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
- Test pass mặc định skip (Q4 UAT iteration)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure Section 5 (rename Section 4 sau Chunk B) "Ý kiến cấp duyệt".
User Session 20 Q3=a: gộp các comment đồng cấp cùng Phòng → 1 ô / bước
(dù bước có nhiều người), CHỈ hiển thị comment của NV đã duyệt.
Trước (Mig 26 S19 LevelOpinionsSectionV2):
forEach step → grid-cols-2 cho forEach Level × forEach Approver → 1 box / NV
Hiển thị cả NV chưa duyệt với placeholder "— chưa duyệt"
Sau (Chunk C):
forEach step → 1 StepOpinionsBox (đại diện Phòng)
Box body: filter opinions có stepOrder == step.order
→ sort theo levelOrder asc, signedAt asc
→ render StepOpinionEntry per signed opinion
NV chưa duyệt KHÔNG hiển thị
Header box: "Bước N — Tên · {dept badge} · X/Y đã duyệt"
FE (mirror fe-admin + fe-user):
- LevelOpinionsSectionV2 forEach step → StepOpinionsBox (replace grid-cols-2)
- StepOpinionsBox: header phòng + body list signed opinions
- StepOpinionEntry: tên NV + Cấp badge + Admin override badge nếu có
+ timestamp + comment
- Drop LevelOpinionBox function (per-NV pattern bỏ)
- KHÔNG đụng schema Mig 26 (PE Service ApproveV2Async UPSERT giữ 1 row /
Level — chỉ FE re-group render)
Verify:
- npm run build × fe-admin pass · fe-user pass
- Test pass mặc định skip (Phase 9 UAT iteration, Q4 user public luôn)
Pending Chunk D: Docs S20 changelog + STATUS + HANDOFF
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hai yêu cầu UAT 2026-05-08:
1. Bỏ "(clone)" auto-append khi clone version mới — version đã đủ phân biệt.
2. Thêm pin toggle để admin chọn workflows nào cho user pick lúc tạo phiếu.
Migration 25 AddIsUserSelectableToApprovalWorkflows:
- ALTER ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0
- UPDATE backfill SET IsUserSelectable=1 WHERE IsActive=1 (giữ behavior cũ
cho active versions, archived = false default — admin tự pin nếu cần)
BE:
- Domain ApprovalWorkflow +property IsUserSelectable
- DTO AwDefinitionDto +field
- CreateAwDefinitionCommandHandler set default true cho version mới
- New SetAwUserSelectableCommand + Handler
- API PATCH /api/approval-workflows-v2/{id}/user-selectable (Workflows.Create policy)
- DbInitializer SeedSampleApprovalWorkflowsV2Async set IsUserSelectable=true
FE Designer (fe-admin):
- DefinitionDto +isUserSelectable
- Badge amber "Pin Cho user chọn" khi true (cạnh Đang áp dụng/Archived)
- Button "Pin/PinOff Ghim cho user / Bỏ ghim" trong action group + mutation toggle
- Auto-fill name khi clone: bỏ "(clone)" suffix → giữ nguyên name
FE Workspace (fe-admin + fe-user):
- approvalWorkflows query filter w.isUserSelectable === true
- User dropdown chỉ thấy workflows admin đã pin
Verify: dotnet build pass · 81 test pass · npm build × 2 pass · Mig 25 apply LocalDB OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User UAT 2026-05-08: bỏ "trạng thái duyệt" (Cấp 1 → 2 → DaDuyet) +
bỏ thay đổi trước Trả lại lần đầu. Chỉ giữ:
- Workflow transition về TraLai (Reject)
- Workflow transition từ TraLai → ChoDuyet (Drafter gửi lại)
- Mọi sửa nội dung khi phaseAtChange = TraLai (giai đoạn chờ gửi lại)
Filter ở FE (PeDetailTabs HistoryTab). BE giữ audit data đầy đủ —
chỉ thay logic display, reversible. Mirror fe-admin + fe-user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "bỏ luôn cái quy trình phía trên đi nhé, vì nó là trạng
thái rồi (đã có badge), update cái flow quy trình mới vào bên panel 3
đang đến ai".
BE — ApprovalFlow DTO mới (full snapshot Bước → Cấp → NV với Status):
- PurchaseEvaluationApprovalFlowDto { CurrentStepIndex, CurrentLevelOrder,
Steps[] }
- PurchaseEvaluationApprovalFlowStepDto { Order, Name, DepartmentId/Name,
Status, Levels[] }
- PurchaseEvaluationApprovalFlowLevelDto { Order, Name, Approvers[], Status }
- Status: "Done" | "Current" | "Pending"
Handler GetById compute Status logic:
- Phase=DaDuyet → tất cả Steps/Levels "Done"
- Phase=Nháp/Trả lại/Từ chối → tất cả "Pending"
- Phase=ChoDuyet:
* Step.Index < currentIdx → all Levels "Done"
* Step.Index == currentIdx:
Level.Order < currentLevelOrder → "Done"
Level.Order == currentLevelOrder → "Current"
Level.Order > currentLevelOrder → "Pending"
* Step.Index > currentIdx → all "Pending"
- Load Approvers info (FullName + Email) qua UserManager batch query
FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeApprovalFlow + Step + Level + Status union
PeDetail.approvalFlow optional
- PeWorkflowPanel:
* BỎ phase cards section (4 ô Nháp/TraLai/ChoDuyet/DaDuyet) — đã
duplicate với status badge ở header
* Header mới: "Quy trình duyệt" + Code + Version + Name workflow pin
* Render Flow vertical: Bước (icon ✓/●/○) → border + bg theo status
+ dept badge → list Cấp (icon nhỏ) với label "đang chờ" / "đã
duyệt" + tên NV duyệt
* Phiếu V1 legacy (no flow): show note "dùng quy trình cũ — không
khả dụng chi tiết"
* Bỏ helper isPastPhase() (orphan sau khi xóa cards)
Verify: BE build 0 error · 2 FE builds OK.
Test eoffice:
1. Mở phiếu V2 đang ChoDuyet → thấy flow Bước 1 (Phòng A):
✓ Cấp 1 NV X (đã duyệt)
● Cấp 2 NV Y (đang chờ) ← highlight
○ Cấp 3 NV Z (chưa)
2. Phase=DaDuyet → all Steps/Levels green ✓
3. Phase=Nháp/TraLai → all greyed ○
4. V1 legacy → fallback note
User feedback: "Nếu không đúng bước duyệt thì nút duyệt cho Disable luôn cũng đc."
BE — DTO + Handler populate "Bước/Cấp đang chờ duyệt":
- Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs:
+PurchaseEvaluationApprovalLevelApproverDto { UserId, FullName, Email }
+PurchaseEvaluationCurrentApprovalDto { StepIndex, StepName,
StepDepartmentId/Name, LevelOrder, LevelName, Approvers[] }
PurchaseEvaluationDetailBundleDto +CurrentApproval? optional field
- Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs handler
GetById: khi pin V2 + Phase=ChoDuyet → load AW.Steps.Levels Include
3-level + group by Order = Cấp + resolve user names → populate
CurrentApproval. Null khi V1 legacy hoặc không phải ChoDuyet.
FE — types + PeWorkflowPanel (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeCurrentApproval + PeCurrentApprovalLevelApprover
+ PeDetail.currentApproval optional
- PeWorkflowPanel:
* Banner V2 hiển thị "Đang chờ Bước N (TênBước · Phòng X) — Cấp K"
+ list NV được duyệt + status emerald (đến lượt) / amber (không phải lượt)
* useAuth() để check currentUser.id ∈ approvers + Admin bypass
* Button "Duyệt forward" disabled khi V2 pin + actor không khớp.
Title tooltip "Cấp K chỉ {NV X / NV Y} mới duyệt được."
* Button "Trả lại" + "Từ chối" vẫn enabled (BE không gating 2 hành
động này theo Cấp — Approver có thể reject bất cứ lúc nào).
* Send-back logic update: target = DangSoanThao OR TraLai (V2 dùng TraLai)
- Admin role bypass mọi check.
Verify: 81 test pass · npm build × 2 OK · BE 0 error.
Test thử:
1. NV X (approver Cấp 1 V2) login → banner emerald "Đến lượt bạn duyệt"
+ nút "✓ Duyệt → ChoDuyet" enabled
2. NV Y (không phải approver) login → banner amber "Không phải lượt
bạn — chỉ NV X mới duyệt được" + nút Duyệt grey disabled, hover tooltip
3. Admin login → bypass, button enabled
User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".
BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
PE chạy logic cũ tới khi Session sau wire qua schema mới
BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
(DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
editable per Session 17 spec)
FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
- Replace field disabled "Loại quy trình" → Select bắt buộc
- useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
- Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
- List cả version active + archived (UAT cần test compare)
- Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
- canSubmit require approvalWorkflowId set
- POST payload include approvalWorkflowId
Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.
Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.
User báo button "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID".
Phân tích: button disabled khi `evaluation.workflow.nextPhases` không có
forward phase (chỉ TuChoi/TraLai). Hiện FE silent — không cách nào biết.
Improvement (cả 2 app, mirror):
- Compute `forwardPhase` once thay vì 2 lần (.find / .some).
- Add `submitDisabledReason` string giải thích reason:
* canEditPhase=false → "Phiếu đã ở phase X — chỉ Bản nháp / Trả lại
mới sửa + gửi được"
* readOnly → "Chế độ chỉ đọc"
* !forwardPhase → "Workflow không có phase tiếp theo từ X. Liên hệ
admin kiểm tra cấu hình quy trình"
- Button title attribute show reason (hover tooltip) hoặc forward phase
label khi enabled: "Gửi phiếu sang 'Chờ Purchasing'"
- Confirm dialog show forward phase explicit: 'Gửi phiếu vào quy trình
duyệt? Sẽ chuyển sang "Chờ Purchasing". Sau khi gửi sẽ KHÔNG sửa
được nữa (trừ khi approver Trả lại).'
Note "trùng ID" KHÔNG phải bug FE: PurchaseEvaluationWorkspacePage
URL state đúng (`+ Thêm mới` clear `id`, save set new). Mỗi PE row
unique GUID + MaPhieu. User feedback có thể due to button silent
disabled — tooltip giờ rõ reason.
Verify: npm build fe-admin + fe-user pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User chỉ thị thay 2-button hiện tại bằng 3 hành động rõ ràng:
- Duyệt = forward phase tiếp theo
- Trả lại = về DangSoanThao + Drafter sửa → workflow tự jump tới phase
đã reject (smart reject Mig 16 pattern + clear N-stage rows)
- Từ chối = phiếu khoá hoàn toàn (Phase=TuChoi → 17 handler Mig 16 lock
edit). Drafter phải tạo phiếu mới.
Domain (PurchaseEvaluationPolicy.cs):
- NccOnly + NccWithPlan: thêm (X → TuChoi) transition cho mọi phase
trung gian (ChoPurchasing/ChoCCM/ChoCEODuyetNCC/ChoDuAn/ChoCEODuyetPA)
với roles của phase đó. Trước đây chỉ DangSoanThao → TuChoi (Drafter).
- FromDefinition expand: mỗi step (trừ DangSoanThao) thêm
(step.Phase → TuChoi) với roles của step.
Service (PurchaseEvaluationWorkflowService.cs):
- Reject branch tách 2 case:
* target=TuChoi → giữ nguyên (KHÔNG override + KHÔNG set
RejectedFromPhase + KHÔNG clear N-stage rows). Phiếu khoá vĩnh viễn.
* target khác (thường DangSoanThao) → smart reject (set
RejectedFromPhase + force DangSoanThao + clear N-stage rows).
FE (PeWorkflowPanel.tsx, fe-admin + fe-user mirror):
- next.phases render 3 button rõ ràng:
* "✓ Duyệt → <label>" brand (forward)
* "← Trả lại (về Drafter sửa)" red (target=DangSoanThao + isSendBack)
* "✗ Hủy / Từ chối" red (target=TuChoi)
- Decision logic: target=TuChoi || isSendBack → Reject (2), else Approve (1)
- Dialog confirm:
* Title rõ theo loại hành động
* Cancel case: warning red "Phiếu sẽ bị khoá hoàn toàn"
* SendBack case: hint amber "Phiếu sẽ về Đang soạn thảo, Drafter sửa
rồi trình lại — workflow tự jump tới phase này"
Tests update + add 1 test mới:
- Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao →
Reject_To_DangSoanThao_Sets_RejectedFromPhase_TraLai (rename + change
target từ TuChoi → DangSoanThao để test Trả lại pattern)
- + Reject_To_TuChoi_Locks_Permanently_No_RejectedFromPhase (NEW test
Từ chối — phase=TuChoi + RejectedFromPhase null)
- NStage_Reject_Clears_InnerStep_Rows_At_Phase: target TuChoi →
DangSoanThao (test Trả lại + clear N-stage rows pattern)
Verify:
- dotnet build 0 error
- dotnet test 95 → **96 pass** (+1 test mới Từ chối)
- npm build fe-admin + fe-user pass
Pending Task 2: Sample data seed N-stage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback 2026-05-07 (annotation):
1. Bỏ checkbox "Chọn NCC này cho hạng mục" trong QuoteDialog (consolidate winner
selection chỉ ở Section 2.a NccSelectorRow — tránh 2 nơi pick winner).
2. Khi NCC là winner (selectedSupplierId === s.supplierId) → cell giá Section 4
matrix ăn theo màu xanh emerald (header + cells trong column).
3. Save có delay → hiện loading spinner / overlay để user biết đang xử lý.
Implementation:
~ QuoteDialog (× 2 app):
- Remove `isSelected` từ form state + UI checkbox
- Vẫn gửi `isSelected: existing?.isSelected ?? false` lên API (giữ nguyên
trạng thái cũ — không expose UI để tránh confusion)
- Disable Xóa/Hủy/Lưu khi `isSaving = mut.isPending || del.isPending`
- Button text: "Đang lưu báo giá…" / "Đang xóa…" thay "Lưu" / "Xóa"
- Full overlay loading: absolute z-10 + bg-white/70 backdrop-blur-sm + spinner
ring brand-600 + status text rõ ràng
~ ItemsTab matrix (× 2 app):
- Column header `<th>`: thêm `isWinner` check → bg-emerald-50 + text-emerald-700
+ prefix "✓ " trước tên NCC khi winner
- Cell `<td>`: thay `q?.isSelected` highlight → `isWinnerColumn` (entire
column ăn theo Section 2.a winner). Cells của winner column LUÔN xanh
bất kể quote đã nhập hay chưa (visual trace winner rõ ràng).
~ NccSelectorRow (× 2 app):
- Wrap Select trong `relative` div
- Thêm inline spinner + text "Đang chọn NCC + sync cột giá Section 4…"
khi setWinner.isPending — báo cho user biết delay đang xử lý
Verify: npm run build fe-admin + fe-user pass · 0 TS error.
UAT mode: skip dotnet test (FE-only), push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback 2026-05-07: bấm pencil cho phiếu khác KHÔNG sáng + KHÔNG vào edit
mode (do useState init mount-time only, ev.id thay đổi không re-trigger).
Cũng cần visual feedback "sáng lên" để user biết đang edit phiếu nào.
Implementation:
~ PeDetailTabs.tsx (× 2 app)
+ import useEffect
~ InfoTab: thêm useEffect watch [autoEdit, canEdit, ev.id, ev.tenGoiThau,
ev.diaDiem, ev.moTa, ev.paymentTerms]. Khi autoEdit && canEdit → setEditing(true)
+ sync values từ ev mới (tránh stale state khi switch giữa 2 phiếu khác id).
Note: Dự án disabled đã có sẵn (line 458 `<Input value={ev.projectName}
disabled className="bg-slate-100" />`) — verify hỏi user, KHÔNG thay đổi.
~ PeListPanel.tsx (× 2 app)
+ Prop `editingRowId?: string | null` — row đang edit (URL editHeader=1)
~ Pencil icon: thêm `isEditingThis = editable && editingRowId === p.id` state
→ bg-brand-100 + text-brand-700 + ring-brand-300 + shadow-sm khi active
→ tooltip đổi "✎ Đang sửa phiếu này — click để toggle / xem khác"
~ PurchaseEvaluationWorkspacePage.tsx (× 2 app)
+ Pass `editingRowId={autoEditHeader ? selectedId : null}` xuống PeListPanel
Verify: npm run build fe-admin + fe-user pass · 0 TS error · áp rule strict
verify khi add new prop chain + useEffect.
UAT mode: skip dotnet test (FE-only), push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run #125 + #126 fail (red ❌ Gitea Actions) do TS compile errors trong commit
`18ebfa1` + `0c5db13` không catch local (UAT skip-verify rule). Errors:
PeListPanel.tsx:41 — Property 'forcedPhase' does not exist (renamed to
editableOnly nhưng quên xóa khỏi destructuring args list)
PeListPanel.tsx:81,106 — Cannot find name 'editableOnly' (do destructuring
vẫn dùng forcedPhase cũ → editableOnly không được declare ở scope)
PeWorkspaceCreateView.tsx:20 — 'PurchaseEvaluationType' declared but never
read (sau khi đổi <Select> Loại quy trình → <Input disabled> chỉ dùng
PurchaseEvaluationTypeLabel, không cần enum value nữa)
Fix:
~ PeListPanel × 2 app: destructuring `forcedPhase,` → `editableOnly = false,`
~ PeWorkspaceCreateView × 2 app: bỏ `PurchaseEvaluationType` khỏi import
Verify: npm run build fe-admin + fe-user pass · 0 TS error · dotnet test 83
vẫn pass (Migration 17 + TraLai phase enum đã verify trước).
UAT mode rule: vẫn skip verify cho task FE-only nhỏ — nhưng phát hiện
multi-rename refactor + bỏ import nên check `npm run build` 1 lần trước commit.
TODO update memory feedback_uat_skip_verify.md thêm exception khi prop rename
hoặc remove unused import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback 2026-05-07: thêm 2 trạng thái meta hiển thị "Bản nháp" + "Đã
gửi duyệt". Bản nháp chỉ hiện ở Thao tác workspace, không hiện ở Duyệt menu.
Implementation:
~ types/purchaseEvaluation.ts
+ PeDisplayStatus enum (BanNhap / DaGuiDuyet / DaDuyet / TuChoi)
+ PeDisplayStatusLabel + PeDisplayStatusColor
+ getPeDisplayStatus(phase) helper:
DangSoanThao → BanNhap
DaDuyet → DaDuyet
TuChoi → TuChoi
else (any middle phase) → DaGuiDuyet
~ components/pe/PeListPanel.tsx
- Phase Select filter → Display status Select (4 option, "Đã gửi duyệt"
KHÔNG filter exact phase do multi-phase, để client-side TODO BE)
- Row badge dùng display status (gọn 4 màu)
+ Prop forcedPhase?: number — workspace dùng để khóa filter Bản nháp
(DangSoanThao). Khi forcedPhase set: ẩn Select, show "Lọc cố định: Bản
nháp" indicator.
~ components/pe/PeDetailTabs.tsx
- Header badge dùng display status meta + secondary text "(Phase chi tiết)"
nhỏ bên cạnh để approver/dev vẫn biết phase exact
~ pages/pe/PurchaseEvaluationsListPage.tsx
- Phase filter Select → display status options
- Row badge → display status
~ pages/pe/PurchaseEvaluationWorkspacePage.tsx
- PeListPanel forcedPhase={PurchaseEvaluationPhase.DangSoanThao}
→ workspace chỉ list Bản nháp (đúng UX user yêu cầu)
Workflow timeline Panel 3 + workflow service BE KHÔNG đổi (giữ phase chi tiết
DangSoanThao/ChoPurchasing/ChoCCM/etc cho approval logic).
Pe_*_Pending Duyệt: dùng /inbox endpoint vốn đã filter chỉ phiếu cần user duyệt
→ DangSoanThao auto-không xuất hiện (không có active approver). Nên Bản nháp
auto-hidden từ Duyệt menu, không cần filter thêm.
UAT mode: skip verify, push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback 2026-05-07 (annotation screenshot):
1. "Gán cứng Duyệt NCC hoặc Duyệt NCC và Giải pháp theo đúng Menu" — Loại quy
trình lock theo URL ?type=N (user vào menu nào → loại đó, không chọn lại).
2. "Chỗ này vẫn hiểu code sửa lại thành select" — Điều khoản thanh toán đổi từ
Textarea (JSON code-style placeholder) → Select preset options + "Khác".
Implementation:
~ PeWorkspaceCreateView.tsx (× 2 app)
- Loại quy trình: <Select> editable → <Input disabled> hiển thị
PurchaseEvaluationTypeLabel[type] với bg-slate-100. Label đổi sang
"Loại quy trình (theo menu — khóa)" rõ ý đồ.
- Điều khoản thanh toán: <Textarea> JSON → <Select> với 8 preset:
"100% sau khi nghiệm thu" / "Tạm ứng 30% / 70%" / "Tạm ứng 50% / 50%" /
"TGN-30 ngày" / "TGN-45" / "TGN-60" / "Tiến độ theo đợt" / "Bảo hành 5%"
+ last option "Khác (nhập tay)" → khi chọn show Input text custom.
- Bỏ import Textarea (không dùng nữa).
- paymentMode local state điều khiển select; form.paymentTerms vẫn save text.
UAT mode: skip verify, push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror commit `7dfeb1a` cho fe-user (rule §3.9 duplicate có chủ đích).
PurchaseEvaluationsListPage readOnly=true cho PeDetailTabs + readOnly={!pendingMe}
cho PeWorkflowPanel. PeWorkflowPanel thêm prop readOnly hide Chuyển tiếp.
UAT mode: skip verify, push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chunk 2/3 — mirror y hệt Chunk 1 sang fe-user (rule §3.9 duplicate có chủ đích).
Cùng BudgetFieldRow component + same imports + same FormRow replacement.
Verify: npm run build fe-user pass · 0 TS error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FE Workflow Panel hiển thị progress 2-cấp duyệt phòng ban (Migration 16):
- Section "Tiến trình duyệt 2-cấp phòng ban" trong PeWorkflowPanel
- Group rows by Phase × Department, show Stage Review NV + Confirm TPB
- Highlight amber "chờ TPB confirm" khi current phase có Review nhưng chưa Confirm
- Badge fuchsia "bypass" khi NV được CanBypassReview
- useQuery fetch endpoint GET /pe/{id}/department-approvals
- Invalidate query sau transition để refresh ngay
Type mới: ApprovalStage const + PeDepartmentApproval DTO trong types/purchaseEvaluation.ts.
User flow anh Kiệt test:
- phuong.nguyen (NV.PRO) Duyệt phase ChoPurchasing
→ row Review xuất hiện, panel hiển thị "⏳ chờ TPB confirm" (amber)
- tra.bui (TPB.PRO, DeptManager) Duyệt
→ row Confirm xuất hiện (emerald) + phase chuyển sang ChoCCM
2 file đồng bộ giữa fe-admin + fe-user (rule §3.9 duplicate có chủ đích).
Build: cả 2 FE pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match form chính thức 4 section đánh số:
1. Thông tin gói thầu — chỉ a. Tên gói thầu + b. Dự án (Địa điểm + Mô tả compact bên dưới nếu có).
2. Chọn NCC / TP — đúng a/b/c/d:
a. NCC/TP được chọn — selectedSupplierName badge xanh
b. Ngân sách — link Budget với mã + tên + tổng
c. Giá chào thầu — tự compute = sum quotes của winner supplier (filter quotes.purchaseEvaluationSupplierId === winnerRowId)
d. Bản so sánh — embed GeneralAttachmentsSection (attachments không gắn supplier-row, purpose=ComparisonTable)
+ ĐKTT + HĐ kế thừa link bonus
+ Banner emerald 'Tạo HĐ từ phiếu' khi DaDuyet + chưa có Contract
3. NCC/TP tham gia — section riêng giữ table 5 cột (NCC/Liên hệ/ĐKTT/File/Action — nhiều info hơn spec table 3 cột, useful cho UX web).
4. Hạng mục + Báo giá — matrix với cột 'NS link · Δ' + footer aggregate (giữ nguyên).
Side change:
- FormRow helper mới (label 176px + value flex) thay cho dl grid 2-col cũ — match style form giấy
- Drop Field helper cũ (now unused)
- InfoTab signature đổi: bỏ readOnly param (chỉ display, action move sang ChonNccSection)
TS build pass cả 2 app. Mirror fe-user identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PART A: Section 'Bang so sanh' (file tong ho so so sanh)
User request: 'theo them cho thong tin ve Bang so sanh, cho dinh kem
file so sanh tong len'.
BE:
- PurchaseEvaluationAttachmentPurpose.ComparisonTable = 4 (new enum value)
Backend validator IsInEnum pass, khong can migration (int column).
FE types (2 app):
- PeAttachmentPurpose.ComparisonTable + Label '4: Bang so sanh'.
FE PeDetailTabs:
- Them section thu 4 'Bang so sanh (file tong)' sau 'Hang muc + Bao gia'.
- Component GeneralAttachmentsSection: upload KHONG truyen supplierRowId
(BE luu NULL) → purpose=ComparisonTable default. Filter attachments
co supplierRowId===null de render.
- Card layout khac SupplierAttachmentsCell: full-width card + brand color
+ purpose chip + date. Upload button to hon ([+ Tai len bang so sanh]).
- readOnly hide upload + delete, giu download.
PART B: Demo email rebrand @solutionerp.local → @solutions.com.vn
User request: 'tao nguoi dung demo theo email cua ben nay'.
BE DbInitializer:
- Rename 18 email in source: AdminEmail const + 17 demo users
(bod/pm/ccm/pro/fin/act/equ/hra/qs/nv) — keep password + role unchanged.
- Them BackfillUserEmailDomainAsync (idempotent): scan user co email
@solutionerp.local, rename sang @solutions.com.vn, update Email +
NormalizedEmail + UserName + NormalizedUserName. Skip neu co conflict
user da ton tai voi email moi. Chay truoc SeedAdmin de tranh tao
duplicate admin.
Admin permission tao user da co san qua /system/users page.
Comment input khi duyet da co san o PeWorkflowPanel (Ghi chu tuy chon
Textarea) + ContractDetailContent (Yeu cau sua / Duyet tiep dialog).
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).
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.