[CLAUDE] Contract: KHKK man duyet — bo "(N dong)" + cot STT + ma hang muc (Mig 72)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 7m2s

Anh giao qua anh prod man duyet KHKK, 2 diem khoanh do:
 (1) "Bo 1 dong di nhe" -> go hau to "(N dong)" khoi title Section 2.
 (2) "Hien thi stt va ma hang muc (Du an) - ma hang muc chi tiet"
     -> bang 3->4 cot (+STT cot rieng, anh chot), cot HANG MUC them dong
        ma "<ma du an> - <ma chi tiet>" nam TREN ten.

Mig 72 AddMaHangMucToContractSigningPlanLines (anh chot DENORM co migration):
 - +cot MaHangMuc nvarchar(20) = BANG nguon ContractCatalogEntry.Code.
 - backfill Sql() idempotent (WHERE MaHangMuc IS NULL), COY KHONG loc IsDeleted:
   hang muc xoa mem SAU ngay lap phieu thi dong phieu van phai giu duoc ma —
   do chinh la ly do cot nay la denorm. Loc di la bo trong dung dong can cuu nhat.
 - ten bang doi chieu voi ToTable() cua CA HAI configuration truoc khi viet
   (Mig 60 tung backfill soi NHAM BANG, chay sach, phai de Mig 61 va — gotcha #78/#79).

PeWorkItemCode KHONG denorm — noi join WorkItems san co o Features.cs:929, 0 cot moi.
Chup lai vao KHKK la chup mot thu von da la con tro loose-Guid qua PE (Mig 49).

3 write-site denorm sua DU: :527 moi null · :695 gan lai · :805 tao moi.
Sot 1 cho = de dong co ten ma khong co ma, chi lo ra o luong it ai bam.

FE 2 app SHA IDENTICAL (verify identical @HEAD TRUOC khi copy de).
Build BE 0 warning 0 error · 2 app build sach · 644/644 test pass.
Lint 42 van de = NO CO SAN, 0 cai o 2 file luot nay dung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-06 14:47:08 +07:00
parent d081681e04
commit 530af22910
10 changed files with 7245 additions and 14 deletions

View File

@ -66,6 +66,18 @@ function moneyOrDash(n: number | null | undefined): ReactNode {
return n == null ? DASH : formatVnd(n)
}
/**
* [S177 — anh: "hiển thị stt và mã hạng mục (Dự án) - mã hạng mục chi tiết"]
* Ghép `<mã hạng mục dự án> - <mã hạng mục chi tiết>` cho dòng bảng hạng mục.
* 🔴 LỌC rồi mới nối, KHÔNG nối rồi mới cắt: thiếu một vế mà cứ chèn " - " thì ra
* "DAO_MONG - " hoặc " - A1-01" — dấu nối mồ côi trông như dữ liệu hỏng chứ không như
* thiếu dữ liệu. Cả hai vế rỗng ⇒ trả null để chỗ gọi BỎ HẲN dòng mã, không vẽ ô trống.
*/
function joinMaHangMuc(maDuAn: string | null, maChiTiet: string | null): string | null {
const parts = [maDuAn, maChiTiet].filter((s): s is string => s != null && s !== '')
return parts.length === 0 ? null : parts.join(' - ')
}
function formatDateTime(iso: string): string {
return new Date(iso).toLocaleString('vi-VN')
}
@ -456,8 +468,11 @@ export function KhkkDetailContent({
</Section>
{/* ─────────────── 2 ─────────────── */}
{/* [S177 — anh: "Bỏ 1 dòng đi nhé"] Bỏ hậu tố "(N dòng)" khỏi `title`: bảng ngay dưới
đã có cột STT nên số dòng tự đọc được ở dòng cuối, đếm thêm trên tiêu đề là nói
hai lần. (Chú thích phải nằm NGOÀI thẻ — JSX không nhận comment giữa attribute.) */}
<Section
title={`2. Hạng mục (${plan.lines.length} dòng)`}
title="2. Hạng mục"
icon={<Users className="h-4 w-4" />}
accent="var(--color-teal-500)"
head="text-teal-700"
@ -487,21 +502,29 @@ export function KhkkDetailContent({
(đúng ý đồ của `overflow-x-auto` bọc ngoài) thay vì bóp chữ. */}
<div className="overflow-x-auto">
<table className="w-full min-w-[880px] table-fixed text-xs">
{/* [S177] 3 → 4 cột (+STT). Bề rộng lấy 6% từ cột hạng mục, KHÔNG đụng 2 cột
bên phải: cột Giá phải giữ nguyên bề rộng thì số mới còn thẳng hàng giữa
các phiếu — đó chính là lý do `table-fixed` được chốt ở S175. */}
<colgroup>
<col className="w-[52%]" />
<col className="w-[6%]" />
<col className="w-[46%]" />
<col className="w-[32%]" />
<col className="w-[16%]" />
</colgroup>
<thead className="border-b border-slate-200 bg-slate-50/70">
<tr>
<th className="label-eyebrow px-3 py-2 text-right">STT</th>
<th className="label-eyebrow px-3 py-2 text-left">Hạng mục</th>
<th className="label-eyebrow px-3 py-2 text-right">Giá</th>
<th className="label-eyebrow px-3 py-2 text-right"></th>
</tr>
</thead>
<tbody>
{plan.lines.map((l) => (
{plan.lines.map((l, i) => (
<tr key={l.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
{/* [S177] STT = thứ tự HIỂN THỊ, không phải id nghiệp vụ. BE đã sắp
`OrderBy(CreatedAt).ThenBy(Id)` nên thứ tự này ổn định giữa 2 lần tải. */}
<td className="px-3 py-2 text-right align-top tabular-nums text-slate-400">{i + 1}</td>
<td className="px-3 py-2 text-slate-700">
{isDraft && !l.contractId ? (
<>
@ -538,6 +561,15 @@ export function KhkkDetailContent({
</>
) : (
<>
{/* [S177 — anh: "mã hạng mục (Dự án) - mã hạng mục chi tiết"]
Mã đứng TRÊN tên: mã ngắn + đều nên mắt quét dọc được cả cột,
còn tên thì dài ngắn khác nhau. `font-mono` để các mã thẳng cột.
Vắng cả hai mã ⇒ `joinMaHangMuc` trả null ⇒ không vẽ dòng này. */}
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc) && (
<div className="font-mono text-[11px] font-medium text-slate-500">
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc)}
</div>
)}
<div>{orDash(l.tenHangMuc)}</div>
{l.note && <div className="mt-0.5 text-[11px] text-slate-500">{l.note}</div>}
</>
@ -584,7 +616,9 @@ export function KhkkDetailContent({
còn được dùng làm DẤU phân biệt dòng-gốc ⟂ dòng-thêm-tay ở nút xoá trên. */}
<tfoot>
<tr className="border-t-2 border-slate-300 bg-slate-50">
<td className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
{/* [S177] `colSpan={2}` nuốt cột STT mới — thiếu nó thì chữ "Tổng" tụt sang
ô hạng mục và con số Tổng rơi lệch một cột so với cột Giá bên trên. */}
<td colSpan={2} className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
<td className="px-3 py-2 text-right text-sm font-bold tabular-nums text-brand-800">
{formatVnd(plan.lines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0))}
</td>

View File

@ -225,6 +225,9 @@ export interface KhkkLineDto {
// tiết hiện "—"; ĐƯỜNG GÁN (Lines-editor) thuộc **K4b**, K2 chỉ hiển thị.
catalogEntryId: string | null
tenHangMuc: string | null
// [Mig 72 S177] `ContractSigningPlanLineDto` append-CUỐI — MÃ hạng mục ("A1-01"), DENORM
// cùng lúc với `tenHangMuc`. null = dòng chưa gán hạng mục ⇒ hiển thị lùi về tên.
maHangMuc: string | null
}
export interface KhkkDossierItemDto {
@ -396,6 +399,13 @@ export interface KhkkDetailDto extends KhkkListItemDto {
finalizeLevelName: string | null
/** Lịch sử duyệt (BE đã lọc `decision = Pending`). */
approvals: KhkkApprovalDto[]
/**
* [S177] MÃ của hạng mục CÔNG VIỆC DỰ ÁN ("DAO_MONG") — cặp mã của `peWorkItemName`.
* 🔴 Khai Ở ĐÂY chứ KHÔNG ở `KhkkListItemDto`, dù `peWorkItemName` nằm trên đó: BE chỉ
* trả `PeWorkItemCode` trên `ContractSigningPlanDetailDto` (append-CUỐI), list KHÔNG có.
* Khai nhầm lên list là hứa một field mà payload danh sách không bao giờ mang.
*/
peWorkItemCode: string | null
}
// [K7 S167 — vá-4b] Tập id HỢP ĐỒNG mà một phiếu KHKK đã đẻ ra. Gom từ MỌI nguồn CÓ THẬT