32 KiB
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.tsxChuẩn (golive):fe-admin/src/components/pe/PeDetailTabs.tsxTrầ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
Sectionchỉ 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 ghisub-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 | ✅ có | 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-4 — có 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): labeltext-[11px] font-semibold uppercase tracking-wide **text-brand-600**· valuefont-medium **text-brand-800**· rỗngtext-slate-300+—. - PE tương đương
FormRow(PeDetailTabs.tsx:2166) + label rờitext-[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ùngtext-sm text-muted-foreground(:372,465,547) — token thứ 4, không có ở PE. → chọn 1:text-sm italic text-slate-400cho 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) · KHKKmb-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) · KHKKpx-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 ?? ''}, onChange → assignLine.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ộtCatalogEntryIdthà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ỡ)
peReferenceAmountNHÂ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:442nó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ọiSUM(peReferenceAmount)sai 300%. → Luật: dòng thứ 2+ của cùng NCC ghiPeReferenceAmount = 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ảiDISTINCT theo supplierId. Chưa có SUM nào tồn tại hôm nay (greppeReferenceAmount= 4 hit, toàn hiển thị) ⇒ vá TRƯỚC khi ai đó viết SUM.- Dòng mồi
CatalogEntryId=nullphả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:176chặn trình duyệt VĨNH VIỄN (đúng lớp lỗi K8-blockerfeedback_dryrun_finds_promised_but_unlanded.md). - 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. ContractIdper-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ùngContractId, nếu không dòng sót sẽ trông như "chưa lên HĐ" và:654sẽ 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ạiCreateContractFromSigningPlanFeatures.cskhi land (ngoài hạn-mức file lượt này — khai thẳng, không đoán).- 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). - 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)
- 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=0nếu không phải dòng đầu) · soft-delete dòng thừa (chặnContractId != null) · tái dụng dòngCatalogEntryId=nulllàm dòng đầu · validate từng entryIsActive && ApprovalGroup == plan.ApprovalGroup(bê nguyên:662) · 1 changelogSummary = "Gán N hạng mục cho NCC X: a, b, c". GIỮ NGUYÊNUpdateContractSigningPlanLineCommandcũ (dòng đơn) — không xoá, tránh vỡ K8/route đang chạy. - BE-2 route
PUT /contract-signing-plans/{id}/suppliers/{supplierId}/catalog-entriesbody{ catalogEntryIds: Guid[] }. - 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
PeReferenceAmountkhông nhân bản. - FE-1
types/khkk.tskhông đổi shape (KhkkLineDtogiữ nguyên) — chỉ thêm helpergroupLinesBySupplier(lines). Mirror 2 app. - FE-2 UI §7.6. 6. FE-3
npm run build×2 app + SHA256 pairfe-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ó
approvedAmountriê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ảiapprovedAmount=nullvà 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:
:238foreach (var l in lines) l.ContractId = contract.Id;— hạt là DÒNG, tậpLineIdsdo client gửi lên (:20-22khai rõ "1 call = 1 HĐ cho ĐÚNG tập LineIds").:125-127mọi dòng gửi lên phải cóApprovedAmount != null, không thìConflictException.:128-129dòng đã cóContractId→ConflictException.
Hệ quả bắt buộc, không tranh cãi được:
- ⇒ 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ó
approvedAmountriê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ở. - ⇒ 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Ả
LineIdscủ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 — và:128khiế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. PeReferenceAmount = 0cho 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-217 → FormData{ 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%)
- 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àmdownload:225, không viết hàm mới) +×xoá (dùng lạideleteAttachment:219). Rỗng →<span className="text-slate-300">—</span>. - 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-onlytext), 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ànhuploadTo(dossierItemId: string | null)và cho Section 4 dùng lại vớinull— 1 mutation, 2 chỗ gọi (chống trôi 2 nhánh). - Chống đếm 2 lần: đổi bộ lọc
:256thànhplan.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ố đếmcountcủa S4 sẽ nói dối. - Đính kèm ngay trong Dialog thêm/sửa căn cứ (
:576-625): chỉ bật khiitemForm.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. - Trạng thái:
upload.isPending→ nút hiện "Đang tải…" +disabled(khuôn:540); lỗi đã cógetErrorMessagetoast. Không thêm spinner mới. - Ràng quyền — ĐÃ ĐO, không đoán: S4 dùng
readOnly, S3 dùngisDraft. GrepEnsureDraftEditabletrongContractSigningPlanFeatures.cschỉ 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ứ theoreadOnly(giống S4), KHÔNG theoisDraftlà 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-1360xá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"). - KHÔNG đổi
KhkkAttachmentPurpose. Cám dỗ là thêmShopdrawing/MauVatLieuvà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.ts là cặ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-4 → px-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-foreground → text-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
Fieldtô 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ềnbg-slate-50/70— tương phản header/thân yếu. Hoặc đổi nềnbg-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)
- 🔴 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. - 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ùngFeatures.cs:1310-1360(handler upload) vàCreateContractFromSigningPlanFeatures.cs:100-240(bridge) phải được người land mở đủ. - 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ủ. - Chưa đo
fe-user/src/pages/khkk/KhkkDetailPage.tsx:205,312sâu — chỉ biết qua grep là 2 read-site củatenHangMuc. Khi tách N dòng,:312sẽ in N dòng cho 1 NCC — cần soi lại, có thể phải gộp. - 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)