[CLAUDE] Contract: KHKK thung rac theo khuon man Duyet NCC — banner + dau xoa (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m41s

Owner gui anh man "Da xoa" cua module Duyet NCC lam MAU ("anh chi la mau —
em soi lai KHKK"). Do KHKK thieu 2 thu so voi mau:

1. Banner do "🗑 Phieu da xoa — chi xem. Khong khoi phuc, khong thao tac."
   PE co o PurchaseEvaluationsListPage:429; KHKK truoc do chi co tooltip hover
   o panel 2, khong co banner nhin thay duoc.
2. Dau "🗑 xoa ~ gio ngay" tren tung the. PE co o :584; KhkkPlanCard = 0 hit.

Banner THAY dropdown trang thai khi xem thung rac (mirror PE): phieu giu
NGUYEN phase luc bi xoa, `IsDeleted` moi la thu phan loai => loc trang thai
o day la nut chet. Chinh `plansForPe` cung co y KHONG loc phase khi xem
thung rac, nen de dropdown lai thi bam ma khong thay gi doi.

🔸 CO Y BO ve "khong con tinh vao luy ke ngan sach" cua ban PE:
`grep 'lũy kế'` trong toan bo components/khkk + pages/khkk = 0 hit => be
nguyen cau do la khai mot dieu chua chung minh cho module nay. Giu dung
phan verify duoc tu chinh ma: khong khoi phuc, khong thao tac.

🔸 Dau xoa dung `updatedAt` chu KHONG phai `deletedAt`: API /deleted tra DTO
DUNG CHUNG voi list song nen khong co `deletedAt`/`deletedBy` (KhkkListItemDto
chi co createdAt + updatedAt). `AuditingInterceptor` ghi updatedAt ngay luot
SaveChanges cua xoa mem => XAP XI. Nhan "~" de khong hua chinh xac; ten nguoi
xoa API khong tra nen co y bo trong thay vi doan. Y het ly do ban PE.

Co truyen o CA HAI cho dung the (than cay + danh sach phang) — thieu 1 cho la
cung mot phieu hien khac nhau o 2 che-do.

mirror sha256 khop; build xanh x2 app; tsc sach x2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-06 09:31:58 +07:00
parent 4f327c36ec
commit 3794d89da5
2 changed files with 90 additions and 20 deletions

View File

@ -105,12 +105,15 @@ export function KhkkPlanCard({
selected, selected,
onSelect, onSelect,
finalize, finalize,
deleted,
}: { }: {
plan: KhkkListItemDto plan: KhkkListItemDto
selected: boolean selected: boolean
onSelect: (id: string) => void onSelect: (id: string) => void
/** Chỉ có khi phiếu ĐANG được chọn (detail đã tải) — list payload không mang cờ này. */ /** Chỉ có khi phiếu ĐANG được chọn (detail đã tải) — list payload không mang cờ này. */
finalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null } finalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null }
/** [S176] Đang xem thùng rác ⇒ hiện dấu thời-điểm xóa. Mirror khuôn màn Duyệt NCC. */
deleted?: boolean
}) { }) {
const frozen = plan.phase === KhkkPhase.DaDuyet const frozen = plan.phase === KhkkPhase.DaDuyet
const amount = frozen ? (plan.totalApprovedAmount ?? plan.totalProposedAmount) : plan.totalProposedAmount const amount = frozen ? (plan.totalApprovedAmount ?? plan.totalProposedAmount) : plan.totalProposedAmount
@ -160,6 +163,22 @@ export function KhkkPlanCard({
)} )}
<span>·</span> <span>·</span>
<span className="truncate">{formatDateTime(plan.createdAt)}</span> <span className="truncate">{formatDateTime(plan.createdAt)}</span>
{/* [S176] "Xóa lúc" — mirror khuôn màn Duyệt NCC (`PurchaseEvaluationsListPage:573`).
🔴 API `/contract-signing-plans/deleted` trả DTO DÙNG CHUNG với list sống ⇒ KHÔNG
có `deletedAt`/`deletedBy`. Mốc dưới là `updatedAt`, mà `AuditingInterceptor` ghi
ngay lượt SaveChanges của thao tác xóa mềm ⇒ XẤP XỈ thời điểm xóa. Nhãn "≈" để
KHÔNG hứa chính xác. Tên người xóa API không trả — cố ý bỏ trống thay vì đoán. */}
{deleted && plan.updatedAt && (
<>
<span>·</span>
<span
className="shrink-0 rounded bg-red-50 px-1 py-0.5 text-[10px] font-medium text-red-700"
title="Thời điểm xóa (xấp xỉ — lấy từ mốc cập nhật cuối, ghi cùng lượt xóa mềm). Xem lịch sử phiếu để biết ai xóa và lý do."
>
🗑 xóa {formatDateTime(plan.updatedAt)}
</span>
</>
)}
</div> </div>
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px] text-slate-500"> <div className="mt-0.5 flex items-center justify-between gap-2 text-[11px] text-slate-500">
<span className="min-w-0 truncate">👤 {plan.drafterFullName ?? '—'}</span> <span className="min-w-0 truncate">👤 {plan.drafterFullName ?? '—'}</span>
@ -293,6 +312,7 @@ export function KhkkListPanel({
selected={selectedId === k.id} selected={selectedId === k.id}
onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect} onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
finalize={selectedId === k.id ? selectedFinalize : undefined} finalize={selectedId === k.id ? selectedFinalize : undefined}
deleted={deletedView}
/> />
</li> </li>
))} ))}
@ -316,16 +336,30 @@ export function KhkkListPanel({
className="pl-8" className="pl-8"
/> />
</div> </div>
<Select {/* [S176] Thùng rác ⇒ THAY dropdown bằng banner, mirror khuôn màn Duyệt NCC
value={phase ?? ''} (`PurchaseEvaluationsListPage:427-431`).
aria-label="Lọc trạng thái" 🔴 Ẩn dropdown là CÓ LÝ DO, không phải cho gọn: phiếu giữ NGUYÊN phase lúc bị xóa,
onChange={e => onPhaseChange(e.target.value === '' ? null : (Number(e.target.value) as KhkkPhaseValue))} `IsDeleted` mới là thứ phân loại ⇒ lọc trạng thái ở đây là nút chết. Chính `plansForPe`
> bên dưới cũng cố ý KHÔNG lọc phase khi xem thùng rác — để dropdown lại thì người dùng
<option value="">Tất cả trạng thái</option> bấm mà không thấy gì đổi.
{(Object.keys(KHKK_PHASE_LABELS) as unknown as number[]).map(p => ( 🔸 CỐ Ý BỎ vế "không còn tính vào lũy kế ngân sách" của bản PE: `grep 'lũy kế'` trong
<option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option> toàn bộ KHKK = 0 hit ⇒ bê nguyên là khai một điều chưa chứng minh cho module này. */}
))} {deletedView ? (
</Select> <div className="rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700">
🗑 <strong>Phiếu đã xóa</strong> chỉ xem. Không khôi phục, không thao tác.
</div>
) : (
<Select
value={phase ?? ''}
aria-label="Lọc trạng thái"
onChange={e => onPhaseChange(e.target.value === '' ? null : (Number(e.target.value) as KhkkPhaseValue))}
>
<option value="">Tất cả trạng thái</option>
{(Object.keys(KHKK_PHASE_LABELS) as unknown as number[]).map(p => (
<option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option>
))}
</Select>
)}
{/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng {/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng
này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200 này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200
phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */} phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */}
@ -412,6 +446,7 @@ export function KhkkListPanel({
selected={selectedId === k.id} selected={selectedId === k.id}
onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect} onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
finalize={selectedId === k.id ? selectedFinalize : undefined} finalize={selectedId === k.id ? selectedFinalize : undefined}
deleted={deletedView}
/> />
</li> </li>
))} ))}

View File

@ -105,12 +105,15 @@ export function KhkkPlanCard({
selected, selected,
onSelect, onSelect,
finalize, finalize,
deleted,
}: { }: {
plan: KhkkListItemDto plan: KhkkListItemDto
selected: boolean selected: boolean
onSelect: (id: string) => void onSelect: (id: string) => void
/** Chỉ có khi phiếu ĐANG được chọn (detail đã tải) — list payload không mang cờ này. */ /** Chỉ có khi phiếu ĐANG được chọn (detail đã tải) — list payload không mang cờ này. */
finalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null } finalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null }
/** [S176] Đang xem thùng rác ⇒ hiện dấu thời-điểm xóa. Mirror khuôn màn Duyệt NCC. */
deleted?: boolean
}) { }) {
const frozen = plan.phase === KhkkPhase.DaDuyet const frozen = plan.phase === KhkkPhase.DaDuyet
const amount = frozen ? (plan.totalApprovedAmount ?? plan.totalProposedAmount) : plan.totalProposedAmount const amount = frozen ? (plan.totalApprovedAmount ?? plan.totalProposedAmount) : plan.totalProposedAmount
@ -160,6 +163,22 @@ export function KhkkPlanCard({
)} )}
<span>·</span> <span>·</span>
<span className="truncate">{formatDateTime(plan.createdAt)}</span> <span className="truncate">{formatDateTime(plan.createdAt)}</span>
{/* [S176] "Xóa lúc" — mirror khuôn màn Duyệt NCC (`PurchaseEvaluationsListPage:573`).
🔴 API `/contract-signing-plans/deleted` trả DTO DÙNG CHUNG với list sống ⇒ KHÔNG
có `deletedAt`/`deletedBy`. Mốc dưới là `updatedAt`, mà `AuditingInterceptor` ghi
ngay lượt SaveChanges của thao tác xóa mềm ⇒ XẤP XỈ thời điểm xóa. Nhãn "≈" để
KHÔNG hứa chính xác. Tên người xóa API không trả — cố ý bỏ trống thay vì đoán. */}
{deleted && plan.updatedAt && (
<>
<span>·</span>
<span
className="shrink-0 rounded bg-red-50 px-1 py-0.5 text-[10px] font-medium text-red-700"
title="Thời điểm xóa (xấp xỉ — lấy từ mốc cập nhật cuối, ghi cùng lượt xóa mềm). Xem lịch sử phiếu để biết ai xóa và lý do."
>
🗑 xóa {formatDateTime(plan.updatedAt)}
</span>
</>
)}
</div> </div>
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px] text-slate-500"> <div className="mt-0.5 flex items-center justify-between gap-2 text-[11px] text-slate-500">
<span className="min-w-0 truncate">👤 {plan.drafterFullName ?? '—'}</span> <span className="min-w-0 truncate">👤 {plan.drafterFullName ?? '—'}</span>
@ -293,6 +312,7 @@ export function KhkkListPanel({
selected={selectedId === k.id} selected={selectedId === k.id}
onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect} onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
finalize={selectedId === k.id ? selectedFinalize : undefined} finalize={selectedId === k.id ? selectedFinalize : undefined}
deleted={deletedView}
/> />
</li> </li>
))} ))}
@ -316,16 +336,30 @@ export function KhkkListPanel({
className="pl-8" className="pl-8"
/> />
</div> </div>
<Select {/* [S176] Thùng rác ⇒ THAY dropdown bằng banner, mirror khuôn màn Duyệt NCC
value={phase ?? ''} (`PurchaseEvaluationsListPage:427-431`).
aria-label="Lọc trạng thái" 🔴 Ẩn dropdown là CÓ LÝ DO, không phải cho gọn: phiếu giữ NGUYÊN phase lúc bị xóa,
onChange={e => onPhaseChange(e.target.value === '' ? null : (Number(e.target.value) as KhkkPhaseValue))} `IsDeleted` mới là thứ phân loại ⇒ lọc trạng thái ở đây là nút chết. Chính `plansForPe`
> bên dưới cũng cố ý KHÔNG lọc phase khi xem thùng rác — để dropdown lại thì người dùng
<option value="">Tất cả trạng thái</option> bấm mà không thấy gì đổi.
{(Object.keys(KHKK_PHASE_LABELS) as unknown as number[]).map(p => ( 🔸 CỐ Ý BỎ vế "không còn tính vào lũy kế ngân sách" của bản PE: `grep 'lũy kế'` trong
<option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option> toàn bộ KHKK = 0 hit ⇒ bê nguyên là khai một điều chưa chứng minh cho module này. */}
))} {deletedView ? (
</Select> <div className="rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700">
🗑 <strong>Phiếu đã xóa</strong> chỉ xem. Không khôi phục, không thao tác.
</div>
) : (
<Select
value={phase ?? ''}
aria-label="Lọc trạng thái"
onChange={e => onPhaseChange(e.target.value === '' ? null : (Number(e.target.value) as KhkkPhaseValue))}
>
<option value="">Tất cả trạng thái</option>
{(Object.keys(KHKK_PHASE_LABELS) as unknown as number[]).map(p => (
<option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option>
))}
</Select>
)}
{/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng {/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng
này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200 này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200
phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */} phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */}
@ -412,6 +446,7 @@ export function KhkkListPanel({
selected={selectedId === k.id} selected={selectedId === k.id}
onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect} onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
finalize={selectedId === k.id ? selectedFinalize : undefined} finalize={selectedId === k.id ? selectedFinalize : undefined}
deleted={deletedView}
/> />
</li> </li>
))} ))}