Files
solution-erp/.claude/workflows/runs/2026-08-05-S174-khkk-style-audit/sub-frontend-designer-L5.md
2026-08-05 16:27:07 +07:00

32 KiB
Raw Blame History

sub-frontend-designer-L5 — LĂNG-KÍNH 5: NỘI DUNG PHIẾU

Run: 2026-08-05-S174-khkk-style-audit · vai: frontend-designer (ensemble /fable-clone lane L5) Đối tượng: fe-admin/src/components/khkk/KhkkDetailContent.tsx Chuẩn (golive): fe-admin/src/components/pe/PeDetailTabs.tsx Trần lượt: 25 (≈4 khung / ≈14 đọc / ≈7 chốt)

0. KHUNG RỖNG (ghi @lượt-1, TRƯỚC khi đọc file) — append-as-I-go → TICK @lượt-19

  • §1+§2+§3. Bản đồ + diff khuôn Section — PE Section chỉ 2 prop, KHKK 8 prop (KHKK GIÀU HƠN chuẩn)
  • §4. Diff bảng — 7 trục, 5 lệch
  • §5. Diff Field / empty-state — 4 token rỗng khác nhau giữa 2 màn
  • §6. Diff khoảng cách — KHKK mất bậc responsive
  • §7. O3 — KHÔNG CẦN MIGRATION (UNIQUE 3 cột Mig 71 đã cho N dòng/NCC)
    • 7.1 FE · [x] 7.2 BE 13 tầng · [x] 7.3 grep consumer đủ · [x] 7.4 6 rủi ro
    • 7.5 thứ tự land · [x] 7.6 spec UI · [x] 7.7 bridge K7 khoá lại chỗ mơ hồ
  • §8. O4 — FE-ONLY, 0 BE, 0 migration (dossierItemId đã land đủ 6 tầng)
  • §9. Backlog 6 P0 + 8 P1 + 3 P2
  • §10. Giới hạn — KHÔNG có screenshot (khai thẳng)

📌 Lệch tên file trong đề bài: khối TASK (🔴 lượt 1-2) chỉ định sub-frontend-designer-**L5**.md; khối RUN-TRACE ở cuối lại ghi sub-frontend-designer-**4**.md. Tao theo lệnh 🔴 (cụ thể hơn + là lệnh chặn lượt 1) và ghi đúng 1 file …-L5.md — không tạo file thứ 2 (isolation B6). Em main muốn tên kia thì đổi tên, đừng để 2 bản.


Nhật ký đọc (append tới đâu ghi tới đó)

§1+§2+§3 — Diff KHUNG Section (PE chuẩn ⟂ KHKK) [đọc @lượt 2-5]

PE Section (fe-admin/src/components/pe/PeDetailTabs.tsx:514-523) — TRẦN, 2 prop:

function Section({ title, children }: { title: string; children: React.ReactNode })
  <section className="px-3 py-3 sm:px-5 sm:py-4">
    <h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>

KHKK Section (fe-admin/src/components/khkk/KhkkDetailContent.tsx:60-96) — GIÀU, 8 prop: title · icon · accent · head · chipBg · chipFg · count? · actions?<section className="px-5 py-4"> + icon-chip 1.75rem + h3 text-sm font-semibold tracking-tight {head} + count-pill + actions ml-auto.

Trục PE (chuẩn golive) KHKK Verdict
Đánh số trong title "1./2./3./5." nhúng chuỗi (PeDetailTabs.tsx:402,405,418,426) y hệt (:287,357,449,529) KHỚP
Count trong title nhúng CHUỖI (${details.length} hạng mục · ${suppliers.length} NCC) (:405) ⚠️ HAI KIỂU LẪN: S2 nhúng chuỗi (${plan.lines.length} dòng) (:357) · S3/S4 dùng count pill (:455,535) LỆCH NỘI-BỘ
icon-chip PE không có icon ở section-head KHKK GIÀU HƠN chuẩn
accent/head color đồng loạt text-slate-500 brand/teal/violet per-section KHKK GIÀU HƠN chuẩn
Kiểu chữ head text-xs uppercase tracking-wide (eyebrow) text-sm tracking-tight (title thường) LỆCH
actions slot không (PE nhét nút vào thân, vd ItemsTab) ml-auto KHKK GIÀU HƠN
Padding px-3 py-3 sm:px-5 sm:py-4có bậc responsive, comment :515-516 ghi rõ "laptop màn nhỏ tiết kiệm ~16px" px-5 py-4 CỨNG 🔴 KHKK REGRESS — mất bài học laptop 1366/mobile 375
divide-y giữa section divide-y divide-slate-200 (:397) y hệt (:284) KHỚP
Vỏ ngoài KHÔNG card-accent (bọc bởi host) card-accent + --accent: brand-500 (:264) KHKK GIÀU HƠN
Khối "nổi bật" PE có: section 4 KHÔNG bọc Section mà là <section> trần chứa khối viền + thanh header bg-[#1F7DC1] text-white uppercase (:423-425 + :1995-1998) KHKK không có khối nổi bật nào 🔴 THIẾU — đây chính là khuôn cho O3 (xem §7.6)

⚖️ Kết luận khung (quan trọng, không đảo chiều mù quáng): KHKK không phải "xấu hơn" — nó giàu hơn PE ở 4 trục (icon/accent/actions/card). Việc cần làm KHÔNG phải hạ KHKK xuống mức PE, mà là (a) vá đúng 2 chỗ KHKK regress thật (padding responsive + count 2-kiểu), (b) port ngược khuôn "khối nổi bật header xanh" của PE sang KHKK cho O3. Nếu muốn đồng bộ 1 chiều thì hướng đúng là nâng PE lên khuôn Section giàu của KHKK (việc lớn, ngoài lượt này) — ghi vào backlog §9-P2.

§4 — Diff BẢNG (table)

Trục PE KHKK Verdict
Layout table-fixed + width từng cột (w-[24%] w-[9%]…, :2719-2728) + min-w-[860px] w-full text-sm không table-fixed, không set width cột (:375,468) 🔴 LỆCH — cột KHKK co giãn theo nội dung ⇒ nhảy layout khi tên NCC dài
Viền lưới ĐẦY ĐỦ kiểu Excel: border border-slate-300 mọi ô (:1402-1407) hoặc border border-slate-200 + border-r (:2719-2727) chỉ border-b hàng (:388,481) LỆCH (chấp nhận được — nhưng phải chọn 1)
thead bg-slate-50 text-slate-600 (:2720) hoặc bg-slate-100 text-[11px] font-semibold uppercase tracking-wide text-slate-600 (:1404,1509) — PE không dùng .label-eyebrow lần nào trong file 3613 dòng bg-slate-50/70 + th class="label-eyebrow" (:376-383,469-476) LỆCH: .label-eyebrow = 11px/700/ls .07em/**brand-600** (fe-admin/src/index.css:89-95) ⇒ th KHKK màu brand, th PE slate. Head-xanh trên nền bg-slate-50/70 = nhạt, kém tương phản header/thân
Body font-size text-xs / text-[13px] text-sm bảng + text-xs từng ô lẫn lộn (:397,431-434) LỆCH nội-bộ: cột NCC text-sm, cột Hạng mục/Ghi chú text-xs cùng hàng ⇒ baseline gãy
Số text-right + tabular-nums y hệt (:431-433) KHỚP
overflow overflow-x-auto + min-w overflow-x-auto không min-w (:374,467) LỆCH nhẹ
Hàng rỗng <td …text-slate-300>—</td> dùng '—' chuỗi trần trong text-slate-600 (:434,493) LỆCH — em-dash KHKK cùng màu chữ thật ⇒ đọc nhầm là dữ liệu

§5 — Diff FIELD / label / value / rỗng

  • KHKK Field (KhkkDetailContent.tsx:98-116): label text-[11px] font-semibold uppercase tracking-wide **text-brand-600** · value font-medium **text-brand-800** · rỗng text-slate-300 + .
  • PE tương đương FormRow (PeDetailTabs.tsx:2166) + label rời text-[11px] font-semibold uppercase tracking-wide **text-slate-500** (:1774), value mặc định slate.
  • 🔴 Lệch nặng nhất về màu: KHKK tô brand-600/brand-800 cho MỌI value (:110-111) ⇒ toàn phiếu xanh, mất hierarchy (FD4 "1 focal point"). PE giữ value slate, chỉ tô màu chỗ CÓ NGHĨA (winner emerald :2008, cảnh báo amber :1336).
  • Rỗng: PE có 3 biến thể — text-sm text-slate-500 (:2572,3194,3285), italic text-slate-400 (:603,736,3380,3533), text-slate-300 — (in-cell). KHKK dùng text-sm text-muted-foreground (:372,465,547) — token thứ 4, không có ở PE. → chọn 1: text-sm italic text-slate-400 cho empty-block, text-slate-300 — cho in-cell.

§6 — Diff KHOẢNG CÁCH

  • Section padding: xem bảng §3 (KHKK mất bậc px-3 py-3 sm:).
  • Header→body: PE mb-3 (:519) · KHKK mb-3 (:75) → KHỚP .
  • Grid field: KHKK gap-x-6 gap-y-4 md:grid-cols-2 (:301,344) — trên scale .
  • Ô bảng: PE px-2 py-1.5 (dày đặc, ERP) · KHKK px-3 py-2 (thoáng hơn) → LỆCH, nhưng KHKK dễ đọc hơn; giữ KHKK, ghi rõ là chủ đích.
  • 🔴 Bảng KHKK không có hover:bg-* hàng (PE cũng không ở table, nhưng có ở nút) — thiếu affordance khi bảng có hành động inline (S2 select, S3 sửa/xóa). FD4 "states".

§7 — O3: Section 2 hạng mục 1→N + SUM + TỔNG GIÁ DUYỆT phía trên

7.1 Hiện trạng FE

KhkkDetailContent.tsx:397-430 — 1 <select> SINGLE trên mỗi dòng, value={l.catalogEntryId ?? ''}, onChangeassignLine.mutate({lineId, catalogEntryId}) (:175-180) → PUT /contract-signing-plans/{id}/lines/{lineId} body {catalogEntryId}. KHÔNG có tổng ở đâu cả — bảng :375-438 không <tfoot>, section-head :357 chỉ đếm dòng. Đọc-site khác: KhkkDetailPage.tsx:205 (đặt tên HĐ) + :312 (list dòng ở panel khác).

7.2 Hiện trạng BE (grep toàn repo, không mở thêm file)

Tầng file:line shape
Entity ContractSigningPlanLine.cs:23,27 Guid? CatalogEntryId + string? TenHangMuc (denorm 500)
EF cfg ContractSigningPlanLineConfiguration.cs:37 UNIQUE 3 cột (PlanId, SupplierId, CatalogEntryId) filtered WHERE IsDeleted=0; :35 CỐ Ý không loại NULL ⇒ 2 dòng cùng (Plan,NCC) đều null = VI PHẠM
EF cfg …Configuration.cs:41 HasIndex(CatalogEntryId)không FK vật lý
Auto-gen ContractSigningPlanFeatures.cs:523-524 sinh dòng mồi CatalogEntryId=null, TenHangMuc=null (1 dòng / NCC trúng thầu của PE)
Command Features.cs:620-623 UpdateContractSigningPlanLineCommand(PlanId, LineId, **Guid CatalogEntryId**) — SINGLE, NotEmpty() :632
Handler Features.cs:649-669 :654 chặn khi line.ContractId != null · :662 chặn entry !IsActive ‖ ApprovalGroup ≠ plan.ApprovalGroup · :666-669 ghi CatalogEntryId + denorm TenHangMuc · :678-687 changelog EntityId = line.Id
DTO Features.cs:87-90 (…LineDto) + :840 map Guid? CatalogEntryId, string? TenHangMuc
Controller ContractSigningPlansController.cs:126 + :313 UpdateSigningPlanLineBody(Guid CatalogEntryId)
Submit-guard ContractSigningPlanWorkflowService.cs:176-187 mọi dòng phải có hạng mục · :182-185 đếm-so-khớp okCount != catalogIds.Count (chống hạng mục xoá-mềm/sai nhóm)
Bridge K7 CreateContractFromSigningPlanFeatures.cs:121,175-181 l.TenHangMuc ?? "(chưa gán hạng mục)" · join tên hạng mục → đặt TÊN HĐ
Test ContractSigningPlanSchemaTests.cs:196-297 3 vế: filtered-unique · chặn 2 dòng sống trùng 3 cột · :291-297 CHO PHÉP cùng (Plan,NCC) KHÁC CatalogEntryId — "mục đích của Mig 71"
Test ContractSigningPlanGroupAndCatalogTests.cs:937-966,1020-1021 Lines.Single() — assert gán + snapshot đổi theo lần gán CUỐI
Test ContractSigningPlanApprovalTests.cs:240-241,274 seed line có hạng mục để qua guard
FE type fe-admin/src/types/khkk.ts:210-225 (mirror fe-user) peReferenceAmount: number · proposedAmount: number · approvedAmount: number | null · contractId: string | null

7.3 🟢 KẾT LUẬN CARDINALITY — MÁY ĐÃ RỘNG SẴN, KHÔNG CẦN MIGRATION

UNIQUE của Mig 71 là 3 cột (PlanId, SupplierId, CatalogEntryId) và test SchemaTests.cs:291-297 chứng minh bằng phép đo rằng "cùng (Plan, NCC) khác CatalogEntryId" được phép. ⇒ N hạng mục cho 1 NCC = N DÒNG, DB chịu ngay hôm nay. Khớp đúng nguyên tắc owner feedback_may_rong_ui_hep.md (máy rộng — UI hẹp).

Phương án chốt = A (N-ROW), KHÔNG bảng nối, KHÔNG cột mới, KHÔNG migration.

  • Bác B (bảng nối …LineCatalogEntries): giết ý nghĩa UNIQUE Mig 71 (cột CatalogEntryId thành cột chết), phải viết lại 3 test schema, đổi submit-guard + DTO + bridge K7 — đắt gấp bội mà không mua thêm khả năng nào, vì giá tiền vốn đã nằm ở DÒNG.
  • Bác C (cột JSON/CSV nhiều id): mất index, mất đếm-so-khớp :182-185, mất changelog per-hạng-mục.

7.4 🔴 6 rủi ro PHẢI xử khi tách 1 dòng → N dòng (đây mới là phần dễ vỡ)

  1. peReferenceAmount NHÂN BẢN ⇒ tổng tham chiếu phồng ×N. Nó là snapshot tổng báo giá của NCC trên phiếu PE (KhkkDetailContent.tsx:442 nói rõ) — thuộc về NCC, không thuộc hạng mục. Tách 3 dòng mà chép cùng số ⇒ mọi SUM(peReferenceAmount) sai 300%. → Luật: dòng thứ 2+ của cùng NCC ghi PeReferenceAmount = 0; FE hiển thị merge (chỉ dòng đầu của nhóm NCC in số, dòng sau in ); mọi phép tổng tham chiếu phải DISTINCT theo supplierId. Chưa có SUM nào tồn tại hôm nay (grep peReferenceAmount = 4 hit, toàn hiển thị) ⇒ vá TRƯỚC khi ai đó viết SUM.
  2. Dòng mồi CatalogEntryId=null phải bị TIÊU THỤ, không bỏ lại. Bulk-assign phải dùng lại dòng null làm dòng đầu; để lại ⇒ WorkflowService.cs:176 chặn trình duyệt VĨNH VIỄN (đúng lớp lỗi K8-blocker feedback_dryrun_finds_promised_but_unlanded.md).
  3. UNIQUE cấm 2 dòng null cùng NCC (Configuration.cs:29-35) ⇒ không được tạo trước N dòng rỗng rồi gán sau. Thứ tự bắt buộc: insert kèm CatalogEntryId ngay.
  4. ContractId per-dòng (bridge K7). Khi 1 NCC có 3 dòng vào cùng 1 HĐ, cả 3 phải được đóng dấu cùng ContractId, nếu không dòng sót sẽ trông như "chưa lên HĐ" và :654 sẽ cho sửa hạng mục của HĐ đã ký. → hàm bridge phải stamp theo nhóm NCC, không theo dòng lẻ. Phải đọc lại CreateContractFromSigningPlanFeatures.cs khi land (ngoài hạn-mức file lượt này — khai thẳng, không đoán).
  5. Xoá hạng mục khỏi multi-select = soft-delete dòng. Chặn nếu ContractId != null (đối xứng :654). Soft-delete + filtered-unique cho phép chọn lại đúng hạng mục đó sau (SchemaTests.cs:196-233 đã chứng).
  6. Test .Single() (GroupAndCatalogTests.cs:937,966,1020) vẫn xanh (ca 1-dòng), nhưng phải thêm ca N-dòng — nếu không, cardinality mới không có chứng nhân nào.

7.5 Thứ tự LAND (BE trước, FE sau — không đảo)

  1. BE-1 SetContractSigningPlanSupplierCatalogsCommand(PlanId, SupplierId, IReadOnlyList<Guid> CatalogEntryIds)diff-sync 1 phát, nguyên tử: giữ dòng trùng · insert dòng thiếu (PeReferenceAmount=0 nếu không phải dòng đầu) · soft-delete dòng thừa (chặn ContractId != null) · tái dụng dòng CatalogEntryId=null làm dòng đầu · validate từng entry IsActive && ApprovalGroup == plan.ApprovalGroup (bê nguyên :662) · 1 changelog Summary = "Gán N hạng mục cho NCC X: a, b, c". GIỮ NGUYÊN UpdateContractSigningPlanLineCommand cũ (dòng đơn) — không xoá, tránh vỡ K8/route đang chạy.
  2. BE-2 route PUT /contract-signing-plans/{id}/suppliers/{supplierId}/catalog-entries body { catalogEntryIds: Guid[] }.
  3. BE-3 test-before cho ca N (repro: 1 NCC 3 hạng mục → 3 dòng · bỏ 1 → 2 dòng sống · dòng có ContractId → 409) + ca PeReferenceAmount không nhân bản.
  4. FE-1 types/khkk.ts không đổi shape (KhkkLineDto giữ nguyên) — chỉ thêm helper groupLinesBySupplier(lines). Mirror 2 app.
  5. FE-2 UI §7.6. 6. FE-3 npm run build ×2 app + SHA256 pair fe-admin/fe-user.

⚠️ Ambiguity phải hỏi owner TRƯỚC BE-1, không đoán: "giá duyệt là tổng" = (i) mỗi hạng mục có approvedAmount riêng, tổng = SUM các dòng hay (ii) 1 giá cho cả NCC, hạng mục chỉ là nhãn? Model A phục vụ (i) tự nhiên; nếu owner ý (ii) thì dòng 2+ phải approvedAmount=null và tổng lấy theo NCC. Câu chữ owner nghiêng về (i) ("sum tổng") nhưng đây là tiền — không suy diễn.

7.6 Spec UI (đúng khuôn PE golive)

(a) TỔNG GIÁ DUYỆT — khối nổi bật ĐẶT TRÊN bảng, bê thẳng khuôn PE PeDetailTabs.tsx:1995-2019:

<div className="overflow-hidden rounded-lg border border-slate-300">
  <div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">
    Tổng giá duyệt
  </div>
  <div className="p-3">
    <div className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2.5">
      → số lớn: text-[18px] font-semibold text-emerald-900 tabular-nums   (SUM approvedAmount ≠ null)
      → dòng phụ text-[12px] text-slate-500: "N/M dòng đã có giá duyệt · Đề xuất: <SUM proposedAmount> · Tham chiếu: <SUM DISTINCT theo NCC>"
      → khi tất cả null:  <span className="text-slate-400">— (chưa có giá duyệt)</span>   ← khuôn PE :2015
      → khi CHỈ MỘT PHẦN có giá: thêm  <div className="text-[11px] italic text-amber-700">…</div>  ← khuôn PE :1336
    </div>
  </div>
</div>

Đặt ngay dưới <Section title="2. …"> header, trên overflow-x-auto (KhkkDetailContent.tsx:371). Badge budgetFrozen (:364-368) chuyển vào khối này thay vì actions🔒 thuộc về CON SỐ, không thuộc về tiêu đề. Bỏ emoji 🔒<Lock className="h-3 w-3"/> lucide (FD3 cấm emoji thay icon; PE cũng đang phạm ở :388,412 nhưng đó là nợ PE, không nhân bản sang KHKK). (b) <tfoot> echo trong bảng: <tr className="border-t-2 border-slate-300 bg-slate-50 font-semibold"> — nhãn "TỔNG" colSpan tới cột Giá đề xuất, rồi 2 ô tabular-nums. Số phía trên = focal point, tfoot = xác nhận tại chỗ (không mâu thuẫn FD4 vì khác cấp nhấn). (c) Gộp dòng theo NCC: hàng đầu của mỗi NCC in tên + rowSpan cột NCC/Giá tham chiếu; hàng sau để trống → mắt thấy ngay "1 NCC nhiều hạng mục". Kèm 1 hàng nhóm-tổng nhỏ bg-slate-50/60 text-[11px] "Cộng NCC X: …" khi nhóm ≥2 dòng. (d) Multi-select hạng mục: KHÔNG dùng <select multiple> (xấu, khó dùng trên cảm ứng, không hiện nhãn dài). Dùng popover checkbox-list + chip: nút <Button variant="outline" size="sm"> in "N hạng mục" → popover danh sách checkbox catalogOptions.data (search khi >12 mục) → đóng popover mới mutate 1 lần (nguyên tử, khớp BE-1). Chip hiển thị dạng <span className="inline-flex items-center rounded-md border border-teal-300 bg-teal-100 px-2 py-0.5 text-[11px] font-medium text-teal-800"> + × khi sửa được. Chip của dòng đã lên HĐ → không có ×, title="Dòng đã vào hợp đồng — hạng mục bất biến" (khớp rào BE :654). (e) Giữ nguyên 2 nhánh lỗi/rỗng đang tốt (:417-427): catalogOptions.isError → thử lại; nhóm N chưa có hạng mục → cảnh báo amber. Cả hai phải được port sang popover, đừng rơi mất khi đổi control. (f) a11y: popover = role="dialog" + aria-label="Chọn hạng mục cho NCC {tên}", đóng bằng Esc, focus-trap, nút mở aria-expanded; checkbox có <label> thật (FD5). Tap target ≥44px cho hàng checkbox.

7.7 🔴 ĐO THÊM — bridge K7 GIẢI QUYẾT LUÔN chỗ mơ hồ ở 7.5 (đo bằng grep-có-ngữ-cảnh, không mở file)

CreateContractFromSigningPlanFeatures.cs:

  • :238 foreach (var l in lines) l.ContractId = contract.Id; — hạt là DÒNG, tập LineIds do client gửi lên (:20-22 khai rõ "1 call = 1 HĐ cho ĐÚNG tập LineIds").
  • :125-127 mọi dòng gửi lên phải có ApprovedAmount != null, không thì ConflictException.
  • :128-129 dòng đã có ContractIdConflictException.

Hệ quả bắt buộc, không tranh cãi được:

  1. Cách đọc (ii) ("1 giá cho cả NCC, dòng 2+ để null") LÀM VỠ BRIDGE K7 NGAY — dòng null giá không bao giờ vào HĐ được. ⇒ Model A phải đi với cách đọc (i): MỖI hạng mục có approvedAmount riêng, tổng = SUM. Đây khớp luôn câu owner "sum tổng". → Câu hỏi ở 7.5 hạ từ "chặn" xuống "xác nhận 1 dòng với owner", không còn là rủi ro mở.
  2. ⇒ Rủi ro #4 (§7.4) cụ thể hoá: dialog tạo HĐ phải chọn theo NHÓM NCC (tick 1 NCC = gửi TẤT CẢ LineIds của NCC đó). Nếu để user tick từng dòng, họ sẽ tạo HĐ cho 1 trong 3 hạng mục rồi 2 hạng mục kia mồ côi — :128 khiến không thể gộp lại sau. Đây là bẫy dữ-liệu-không-hoàn-tác ⇒ P0 phải làm CÙNG lượt tách N-dòng, không để wave sau.
  3. PeReferenceAmount = 0 cho dòng 2+ (§7.4 #1) an toàn với bridge — bridge không đọc field này (không xuất hiện trong vùng grep :120-240).

§8 — O4: Section 3 "Căn cứ hồ sơ" cho ĐÍNH KÈM FILE

8.1 Section 4 đang upload thế nào

KhkkDetailContent.tsx:206-217FormData{ file, purpose }POST /contract-signing-plans/{id}/attachments, header multipart/form-data. purpose cứng KhkkAttachmentPurpose.DossierScan = 1 (:210 + types/khkk.ts:103-110). Tải xuống :225-239 (/attachments/{attId}/download, blob + createObjectURL). Xoá :219-223. Lọc hiển thị :256 — loại ApprovalAttachment(3) vì file đó thuộc panel 3.

8.2 🟢 ĐƯỜNG ĐÃ CÓ SẴN — O4 = FE-ONLY, 0 migration, 0 dòng BE

Chuỗi dossierItemId land đủ 6 tầng (grep toàn repo, không phải comment suông — đã tự kiểm theo bài học K8):

Tầng file:line bằng chứng
Entity ContractSigningPlanAttachment.cs:28 public Guid? ContractSigningPlanDossierItemId
Mig 69 (đã chạy) 20260729122015_AddContractSigningPlans.cs:85 + :252-254 cột + index IX_…_ContractSigningPlanDossierItemId
EF cfg ContractSigningPlanAttachmentConfiguration.cs:11,31 index, cố ý không FK (né multiple-cascade)
Command Features.cs:1279 Guid? DossierItemId · :1334-1337 tự kiểm căn cứ thuộc ĐÚNG phiếu (vì không có FK) · :1354 gán
Controller ContractSigningPlansController.cs:181,191 [FromForm] Guid? dossierItemId = null truyền thẳng
DTO ra Features.cs:106, 854, 1379 → FE types/khkk.ts:241 dossierItemId: string | null
FE chỉ đơn giản là chưa bao giờ GỬI và chưa bao giờ ĐỌC field này. Không dựng đường mới.

8.3 Spec UI (tái dụng 100%)

  1. Thêm cột "File" vào bảng S3 (:469-477), đặt trước cột Thao tác. Nội dung/ô = danh sách file của căn cứ đó: plan.attachments.filter(a => a.dossierItemId === d.id) → mỗi file 1 chip <button> mở tải xuống (dùng lại hàm download :225, không viết hàm mới) + × xoá (dùng lại deleteAttachment :219). Rỗng → <span className="text-slate-300">—</span>.
  2. Nút đính kèm per-dòng: <label> ẩn <input type="file" multiple> giống :538-542 (thu nhỏ về icon <Paperclip className="h-3.5 w-3.5"/> + sr-only text), gọi mutation mới nhưng cùng endpoint: fd.append('file', f); fd.append('purpose', String(KhkkAttachmentPurpose.DossierScan)); fd.append('dossierItemId', d.id). → gộp thành uploadTo(dossierItemId: string | null)cho Section 4 dùng lại với null — 1 mutation, 2 chỗ gọi (chống trôi 2 nhánh).
  3. Chống đếm 2 lần: đổi bộ lọc :256 thành plan.attachments.filter(a => a.purpose !== ApprovalAttachment && a.dossierItemId == null) ⇒ file của căn cứ chỉ hiện ở mục 3. Thêm chú thích dưới S4: "N file scan căn cứ đang hiển thị ở mục 3." khi có. Nếu bỏ qua bước này, số đếm count của S4 sẽ nói dối.
  4. Đính kèm ngay trong Dialog thêm/sửa căn cứ (:576-625): chỉ bật khi itemForm.id đã tồn tại (upload cần id) — với căn cứ MỚI hiện dòng chữ text-[11px] text-slate-500 "Lưu căn cứ trước rồi đính kèm file." Không tự chế cơ chế upload-trước-lưu.
  5. Trạng thái: upload.isPending → nút hiện "Đang tải…" + disabled (khuôn :540); lỗi đã có getErrorMessage toast. Không thêm spinner mới.
  6. Ràng quyền — ĐÃ ĐO, không đoán: S4 dùng readOnly, S3 dùng isDraft. Grep EnsureDraftEditable trong ContractSigningPlanFeatures.cs chỉ ra 3 hit: :283 (định nghĩa) · :587 (sửa nháp) · :647 (gán hạng mục)KHÔNG có ở handler upload (:1310-1360). ⇒ BE cho phép đính kèm sau khi phiếu đã trình. Vậy đính kèm căn cứ theo readOnly (giống S4), KHÔNG theo isDraft là hợp lệ và đúng nghiệp vụ (TVGS trả kết quả muộn). Caveat: đây là suy từ vắng-mặt hit — người land phải mở đúng vùng :1310-1360 xác nhận 1 lần trước khi mở nút (bài học "vắng-mặt trông giống ổn").
  7. KHÔNG đổi KhkkAttachmentPurpose. Cám dỗ là thêm Shopdrawing/MauVatLieu vào enum — sai: loại đã nằm ở DossierItemKind (types/khkk.ts:67-78) của chính căn cứ mà file treo vào. Thêm enum = 2 nguồn sự thật cho cùng 1 sự việc.

§9 — Backlog thi-hành-được (P0 → P2)

Điều kiện chung mọi item FE: file KhkkDetailContent.tsx + types/khkk.tscặp MIRROR SHA256 — đo lúc audit: fa3035496cda0662… (2 app khớp) · aa4103de6362e67e… (2 app khớp). ⇒ mọi sửa phải land cả 2 app rồi sha256sum lại thành cặp; build npm run build ×2.

P0 — owner-item, đụng dữ-liệu (làm cùng lượt, không tách)

# Việc Chạm Đo xong =
P0-1 BE SetContractSigningPlanSupplierCatalogsCommand diff-sync N hạng mục/NCC (§7.5-1) + route (§7.5-2) ContractSigningPlanFeatures.cs, ContractSigningPlansController.cs 3 test-before ca N xanh (§7.5-3)
P0-2 PeReferenceAmount = 0 cho dòng 2+ cùng NCC (§7.4 #1) cùng handler test: 1 NCC 3 hạng mục ⇒ SUM(PeReferenceAmount) = số PE gốc, không ×3
P0-3 Dialog tạo HĐ chọn theo nhóm NCC (§7.7 #2) — chống hạng mục mồ côi không hoàn tác được FE dialog K7 tick 1 NCC ⇒ payload chứa ĐỦ LineIds của NCC đó
P0-4 FE multi-select popover + chip (§7.6-d,e,f) KhkkDetailContent.tsx:397-430 ×2 app 2 nhánh lỗi/rỗng :417-427 còn nguyên trong popover
P0-5 Khối "TỔNG GIÁ DUYỆT" trên bảng (§7.6-a) + <tfoot> (§7.6-b) + gộp dòng theo NCC (§7.6-c) :355-445 ×2 app có ca all-null + ca một-phần-null hiển thị đúng
P0-6 O4: đính kèm file per-căn-cứ (§8.3-1,2,4,5,6) + sửa bộ lọc :256 thêm && a.dossierItemId == null (§8.3-3) :206-217, :256, :464-525, :576-625 ×2 app count S4 không đếm 2 lần

P1 — style lệch có hại (rẻ, làm ngay sau P0)

# Việc file:line Vì sao không phải "cho đẹp"
P1-1 Section padding px-5 py-4px-3 py-3 sm:px-5 sm:py-4 KhkkDetailContent.tsx:74 Mất bậc responsive PE cố ý thêm cho laptop nhỏ (PeDetailTabs.tsx:515-516). Panel giữa của layout 3-panel còn hẹp hơn PE ⇒ hại gấp đôi
P1-2 Thống nhất 1 kiểu count: bỏ (${plan.lines.length} dòng) khỏi title :357, dùng prop count như S3/S4 :357 Cùng 1 màn 2 quy ước đếm = người đọc phải học 2 lần
P1-3 Bảng: thêm table-fixed + width từng cột + min-w-[880px] trong overflow-x-auto :374-384, :467-477 Không có width ⇒ tên NCC dài đẩy cột tiền co lại; số tiền nhảy cột giữa 2 phiếu = mất niềm tin vào con số
P1-4 Ô rỗng trong bảng: '—' trần → <span className="text-slate-300">—</span> :434, :490, :493 Em-dash đang cùng màu chữ thật ⇒ đọc nhầm là dữ liệu (PE làm đúng ở :1415,2761,2769)
P1-5 Empty-block: text-muted-foregroundtext-sm italic text-slate-400 (khớp PE :3380,3533) :372, :465, :547 token thứ 4 không tồn tại trong PE ⇒ 2 màn cạnh nhau khác sắc độ
P1-6 Bỏ emoji 🔒<Lock className="h-3 w-3"/> lucide :366 FD3 cấm emoji thay icon; nền amber + emoji = chỗ duy nhất trên phiếu dùng emoji
P1-7 Font-size trong 1 hàng bảng: chọn text-xs cho cả hàng (ERP dense, khớp PE :2719) thay vì trộn text-sm/text-xs :389-434, :482-493 baseline gãy giữa các ô cùng hàng
P1-8 Thêm hover:bg-slate-50/60 cho <tr> ở 2 bảng có thao tác inline :388, :481 FD4 "states" — bảng bấm được mà không phản hồi hover

P2 — nợ khung, cần quyết định (KHÔNG tự làm)

  • P2-1 Field tô brand-600/brand-800 cho MỌI value (:110-111) → PE giữ slate, chỉ tô chỗ có nghĩa. Sửa = đổi diện mạo cả phiếu ⇒ cần owner nhìn 1 lần. Đề xuất: label giữ text-brand-600, value → text-slate-900, chừa màu cho winner/cảnh báo.
  • P2-2 label-eyebrow (brand-600) trong <th> trên nền bg-slate-50/70 — tương phản header/thân yếu. Hoặc đổi nền bg-slate-100 (khuôn PE :1404), hoặc đổi th về slate-600.
  • P2-3 Hướng đồng bộ khung: KHKK giàu hơn PE ở icon-chip/accent/actions/card-accent. Đừng hạ KHKK; nếu muốn 1 khuôn thì nâng PE lên — việc lớn (PE 3613 dòng, 5 section, đang golive) ⇒ phải là run riêng có screenshot đối chứng.

§10 — Giới hạn của lượt này (khai thẳng, không giấu)

  1. 🔴 KHÔNG có screenshot / FD2 visual-loop. Đây là lane AUDIT (isolation: chỉ được ghi 1 file sub-MD, không giao code-file) và môi trường spawn không có dev-server + API chạy; trang KHKK lại nằm sau ProtectedRoute (gotcha authed-rig #3 trong MEMORY). ⇒ Mọi kết luận ở đây là đọc mã + đo grep, KHÔNG phải "đã nhìn". Người land P0/P1 phải chạy vòng soi thật (mobile 375 + laptop 1366 + desktop 1440) trước khi tuyên PASS.
  2. Hạn mức 1 file BE đã dùng cho ContractSigningPlanFeatures.cs (đọc :608-687). ContractSigningPlanWorkflowService.cs, CreateContractFromSigningPlanFeatures.cs, ContractSigningPlanLineConfiguration.cs, entity, controller, test — chỉ đọc qua grep-có-ngữ-cảnh, không mở toàn file. Số dòng trích là thật; ngữ cảnh quanh nó thì chưa soi hết — đặc biệt vùng Features.cs:1310-1360 (handler upload) và CreateContractFromSigningPlanFeatures.cs:100-240 (bridge) phải được người land mở đủ.
  3. Chưa đo KhkkListPanel/KhkkWorkListPanel/KhkkWorkflowPanel (3 file, 1406 dòng) — lăng-kính này chốt phạm vi ở NỘI DUNG PHIẾU. Lệch style ở panel 1/3 do lane khác phủ.
  4. Chưa đo fe-user/src/pages/khkk/KhkkDetailPage.tsx:205,312 sâu — chỉ biết qua grep là 2 read-site của tenHangMuc. Khi tách N dòng, :312 sẽ in N dòng cho 1 NCC — cần soi lại, có thể phải gộp.
  5. Cách đọc "sum tổng" đã được §7.7 khoá lại bằng ràng buộc bridge, nhưng vẫn nên xác nhận 1 dòng với owner trước khi viết BE — vì đây là tiền.

END sub-frontend-designer-L5 — VERDICT=AUDIT-DONE · O3=Model-A-N-ROW-0-migration (máy đã rộng sẵn từ Mig 71) · O4=FE-ONLY-0-BE (rail dossierItemId đã land đủ 6 tầng từ Mig 69) · style: 6 P0 + 8 P1 + 3 P2 · KHÔNG có screenshot (lane audit, khai thẳng)