[CLAUDE] Contract: K2 SP-002 phieu KHKK mang hang-muc + nhom duyet N1-8 (Mig 71) + FE x2 app + 11 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m12s

Mig 71 AddKhkkApprovalGroupCatalogAndFinalizeRuntime: +ApprovalGroup (backfill=1)
+EndedByLevelFinalize (K2 de, K3 tieu - ghost-window khai) + Line +CatalogEntryId/
TenHangMuc + unique (Plan,Supplier,CatalogEntry) filtered IsDeleted=0. Rao
1-PE-1-phieu doi (PeId,ApprovalGroup) 2 site + picker OG-7 ExistingGroups + nut
'Tao them cho nhom khac'. Submit-guard va-5 (CHAN null, thang nen cho-qua-legacy;
ghost-window K2->K4b khai) + dem-so-khop (gate F-1: bit duong hang-muc xoa-mem).
List ?group= end-to-end + Tong-Approved list (va-8). FE: select nhom default 1 +
badge N{g} + cot Hang-muc + cot Tong-duyet, SHA-pair 5/5. Gate K2 PASS-WITH-FLAGS
10f: F-1 guard fix + 2 test bit (xoa-mem chan + reflection ?group=); F-2/F-5/F-7/
F-8/F-9 comment-honest; F-3/F-4 docs-delta. Suite 609/0 (45D+564I). Wave co #53 x5,
artifact + relaunch-cat-got cuu 100% (0 mat cong).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-31 17:05:27 +07:00
parent 89b5bbee8a
commit 0779f2d45a
23 changed files with 8905 additions and 109 deletions

View File

@ -25,6 +25,7 @@ import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
import {
KHKK_APPROVAL_GROUP_LABELS,
KHKK_PHASE_BADGE,
KHKK_PHASE_LABELS,
KhkkPhase,
@ -134,7 +135,7 @@ export function KhkkListPage() {
{/* [S162] Cây toàn trình bám trái + nội dung cũ giữ nguyên bên phải (owner
chốt AskUser 30-07: "áp cả các trang GĐ khác"). `minmax(0,1fr)` để bảng
7 cột không thổi vỡ lưới ở 1366; `<lg` cây xếp trên và tự thu gọn. */}
8 cột không thổi vỡ lưới ở 1366; `<lg` cây xếp trên và tự thu gọn. */}
<div className="grid gap-5 lg:grid-cols-[19rem_minmax(0,1fr)] xl:grid-cols-[21rem_minmax(0,1fr)]">
<PipelineTreePanel currentStage={2} />
@ -172,7 +173,7 @@ export function KhkkListPage() {
</div>
<div className="card-accent overflow-hidden" style={{ ['--accent' as string]: 'var(--color-brand-500)' }}>
{/* [S162] Cột trái ăn ~19rem ⇒ bảng 7 cột cuộn ngang trong thẻ thay vì
{/* [S162] Cột trái ăn ~19rem ⇒ bảng 8 cột cuộn ngang trong thẻ thay vì
tràn ra ngoài lưới trên laptop 1366. */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
@ -183,6 +184,10 @@ export function KhkkListPage() {
<th className="label-eyebrow px-4 py-2.5 text-left">Dự án</th>
<th className="label-eyebrow px-4 py-2.5 text-left">Trạng thái</th>
<th className="label-eyebrow px-4 py-2.5 text-right">Tổng đ xuất</th>
{/* [K2 S164 — vá-8] Σ `ApprovedAmount` (`ListItemDto:64`). Không có cột này
thì màn danh sách nói "Tổng đề xuất" còn màn chi tiết nói "Giá duyệt"
trên CÙNG một phiếu đã duyệt ⇒ người dùng đọc ra hai con số. */}
<th className="label-eyebrow px-4 py-2.5 text-right">Tổng duyệt</th>
<th className="label-eyebrow px-4 py-2.5 text-left">Người soạn</th>
<th className="label-eyebrow px-4 py-2.5 text-left">Ngày tạo</th>
</tr>
@ -190,14 +195,14 @@ export function KhkkListPage() {
<tbody>
{list.isLoading && (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">
<td colSpan={8} className="px-4 py-8 text-center text-slate-500">
Đang tải...
</td>
</tr>
)}
{!list.isLoading && items.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-slate-500">
<td colSpan={8} className="px-4 py-10 text-center text-slate-500">
<span
className="icon-chip mx-auto mb-2 flex"
style={{ ['--chip-bg' as string]: '#f1f5f9', ['--chip-fg' as string]: '#94a3b8' }}
@ -228,6 +233,18 @@ export function KhkkListPage() {
<FileCheck className="h-3.5 w-3.5" />
</span>
<span className="font-mono text-xs text-brand-800">{k.maKeHoach ?? '—'}</span>
{/* [K2 S164] Badge NHÓM DUYỆT — đặt cạnh mã thay vì đẻ cột thứ 9:
bảng này đã cuộn ngang ở 1366 khi có cây bên trái (ghi chú S162).
`!= null` guard cho payload cũ còn trong cache React Query (trước
K2 DTO chưa có field) — thiếu guard thì in ra "Nundefined". */}
{k.approvalGroup != null && (
<span
className="inline-flex items-center rounded border border-violet-300 bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-800"
title={KHKK_APPROVAL_GROUP_LABELS[k.approvalGroup] ?? `Nhóm ${k.approvalGroup}`}
>
N{k.approvalGroup}
</span>
)}
</span>
</td>
<td className="max-w-xs truncate px-4 py-2.5 font-medium text-brand-800">
@ -249,6 +266,11 @@ export function KhkkListPage() {
)}
</td>
<td className="px-4 py-2.5 text-right tabular-nums text-brand-800">{formatVnd(k.totalProposedAmount)}</td>
{/* Phiếu chưa duyệt BE trả 0 (không phải null) ⇒ ép về "—" cho khỏi đọc
nhầm "đã duyệt 0 đ"; số chỉ hiện khi thật sự có tiền đã chốt. */}
<td className="px-4 py-2.5 text-right tabular-nums text-emerald-700">
{k.totalApprovedAmount ? formatVnd(k.totalApprovedAmount) : '—'}
</td>
<td className="px-4 py-2.5 text-xs text-slate-600">{k.drafterFullName ?? '—'}</td>
<td className="px-4 py-2.5 text-xs text-slate-600">{formatDate(k.createdAt)}</td>
</tr>