[CLAUDE] FE-Admin: KHKK an dong goc chua gan hang muc, TONG = tong hang muc that (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m48s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m48s
Anh chot qua 2 cau hoi:
- dong goc: "Van sinh, nhung AN + khong tinh TONG" (khong xoa)
- TONG: "TONG = tong hang muc that"
Va truoc do: "luc moi khoi tao chi can 1 dong nay thoi" (= dong nhap).
DONG GOC la gi (doc BE truoc khi dung):
Features.cs:526 tu sinh 1 dong cho MOI NCC trung thau, mang snapshot PeReferenceAmount
= tong bao gia duoc chon ben phieu PE, va CatalogEntryId = NULL CO CHU DICH (luc tao
chua biet se ky theo hang muc SP-002 nao). Khong phai dong rac — la dong CHO DIEN.
Da lam (FE-only, 0 migration, 0 file BE):
- An dong goc CHUA GAN khoi bang + khoi TONG. Gan hang muc cho no thi no HIEN LAI va
vao TONG nhu dong thuong. STT danh theo danh sach DA LOC (dung index mang goc la
thung so ngay dong dau).
- Dong nhap TU MO khi con NCC chua co dong nao hien => phieu vua tao hien dung 1 dong
nhap nhu anh muon. Gan xong 1 hang muc thi tu dong.
- O chon NCC CHI hien khi that su khong suy ra duoc (>=2 NCC chua voi toi). Phieu 1 NCC
khong thay o nay.
- Nut "Huy" chi hien khi nguoi dung tu bam `+`. Luc dong nhap TU MO thi Huy la nut chet
(bam xong effectiveAddFor tinh lai va mo y nhu cu) — bay nut khong lam duoc gi con te
hon khong bay.
🔴 HE QUA DAY CHUYEN TU BAT DUOC — an thoi la KHONG DU:
ContractSigningPlanWorkflowService.cs:176 chan trinh duyet khi con BAT KY dong nao
CatalogEntryId == null. An dong goc di thi nguoi soan khong con o nao de gan
=> PHIEU KET VINH VIEN, khong trinh duyet duoc. Grep consumer moi loi ra (dong :1066).
Cach ra KHONG can migration, KHONG can noi rao xoa cua BE:
hang muc DAU TIEN cua mot NCC => GAN THANG vao dong goc cua NCC do (PUT), thay vi de
dong moi (POST). Dung vong doi ma Features.cs:530-534 thiet ke ("moi NULL, nguoi soan
gan sau"). Dong goc het NULL => hien ra thanh dong 1 => submit-guard thong.
Cau chan cung da viet lai: cau cu "con N dong chua gan hang muc" tro vao dong KHONG CO
tren man => doi thanh "con N nha cung cap chua co hang muc nao — them hang muc cho ho".
🔴 BAT BIEN TIEN GIU NGUYEN: tong PeReferenceAmount KHONG doi (dong goc van giu so PE cua
no, chi doi ProposedAmount) => BudgetManualAmount cua HD sinh sau
(CreateContractFromSigningPlanFeatures.cs:200) van dung bang so PE. Day chinh la ly do
KHONG chon huong xoa dong goc, va cung la ly do khong phai dung toi rao BE Features.cs:875.
Do duoc:
- tsc -b exit 0 · build 2 app exit 0 · eslint file nay exit 0 · SHA256 2 app IDENTICAL.
- dist 2 app chua MARKER moi 'chua co hang muc nao' = 1/1 (build 700ms la do cache Vite,
da doi chung file dist vua ghi trong 3 phut => output that, khong phai ban cu).
- CONTROL DUONG '— Chon hang muc —' = 2/2 ca 2 dist => phep do dang chay.
Diff CHI 2 file FE, 0 file BE => .NET test khong the bi anh huong (khai ro).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -42,7 +42,7 @@ import {
|
||||
KHKK_APPROVAL_GROUP_LABELS, KHKK_PHASE_BADGE, KHKK_PHASE_LABELS,
|
||||
KhkkAttachmentPurpose, KhkkPhase, KhkkTransitionAction, type KhkkTransitionInput,
|
||||
type DossierItemKindValue, type DossierItemStatusValue, type KhkkAttachmentDto,
|
||||
type KhkkDetailDto, type KhkkDossierItemDto, type KhkkPhaseValue,
|
||||
type KhkkDetailDto, type KhkkDossierItemDto, type KhkkLineDto, type KhkkPhaseValue,
|
||||
type UpsertKhkkDossierItemInput,
|
||||
} from '@/types/khkk'
|
||||
|
||||
@ -196,6 +196,39 @@ export function KhkkDetailContent({
|
||||
onChanged?.()
|
||||
}
|
||||
|
||||
// ─── [S178 — anh: "lúc mới khởi tạo chỉ cần 1 dòng này thôi"] ───────────────────────
|
||||
// DÒNG GỐC CHƯA GÁN = dòng hệ thống tự sinh cho mỗi NCC trúng thầu (`Features.cs:526`),
|
||||
// mang SNAPSHOT `peReferenceAmount` = Σ báo giá được chọn bên phiếu PE, và `catalogEntryId`
|
||||
// để NULL CÓ CHỦ ĐÍCH (lúc tạo chưa biết sẽ ký theo hạng mục SP-002 nào).
|
||||
// 🔴 Anh chốt: **VẪN SINH, chỉ ẨN + không tính TỔNG** — KHÔNG xoá. Vì xoá là mất 3 thứ:
|
||||
// số PE gốc, dấu "NCC này từng trúng thầu", và `BudgetManualAmount` của HĐ sinh ra sau
|
||||
// (`CreateContractFromSigningPlanFeatures.cs:200` = Σ `PeReferenceAmount` các dòng).
|
||||
// BE cũng đang chặn cứng việc xoá nó (`Features.cs:875`) — ẩn ở FE nên KHÔNG phải nới rào.
|
||||
// ⇒ Gán hạng mục cho dòng gốc thì nó HIỆN LẠI và vào TỔNG như dòng thường.
|
||||
const isUnassignedSeed = (l: KhkkLineDto) => l.peReferenceAmount !== 0 && !l.catalogEntryId
|
||||
const visibleLines = plan.lines.filter((l) => !isUnassignedSeed(l))
|
||||
|
||||
// 🔴 HỆ QUẢ DÂY CHUYỀN phải xử, không thì ẩn xong là KẸT: nút `+` bám vào TỪNG DÒNG (nó lấy
|
||||
// NCC từ chính dòng đó — anh chốt vậy ở đợt-3 khi bỏ khối "Thêm hạng mục cho: [NCC]").
|
||||
// Ẩn dòng ⇒ mất luôn nút ⇒ NCC đó KHÔNG CÒN ĐƯỜNG VÀO.
|
||||
// Điều kiện đúng KHÔNG phải "bảng rỗng" mà là "còn NCC nào chưa có dòng nào HIỆN" — phiếu
|
||||
// liên-danh nhiều NCC (PE multi-winner, Mig 58/59) mà chỉ xét bảng-rỗng thì NCC thứ 2 mất
|
||||
// đường vào ngay khi NCC thứ nhất có 1 dòng.
|
||||
const visibleSupplierIds = new Set(visibleLines.map((l) => l.supplierId))
|
||||
const unreachableSuppliers = [
|
||||
...new Map(
|
||||
plan.lines
|
||||
.filter((l) => isUnassignedSeed(l) && !visibleSupplierIds.has(l.supplierId))
|
||||
.map((l) => [l.supplierId, l.supplierName] as const),
|
||||
).entries(),
|
||||
]
|
||||
// Dòng nhập TỰ MỞ khi còn NCC chưa với tới được ⇒ phiếu vừa tạo hiện đúng 1 dòng nhập như
|
||||
// ảnh anh gửi. Gán xong 1 hạng mục thì NCC đó hết "unreachable" ⇒ dòng nhập tự đóng, quay
|
||||
// về nếp cũ (bấm `+` trên dòng).
|
||||
const effectiveAddFor = addFor ?? (isDraft && unreachableSuppliers.length > 0
|
||||
? { supplierId: unreachableSuppliers[0][0] }
|
||||
: null)
|
||||
|
||||
|
||||
// Danh mục hạng mục SP-002 — chỉ tải khi phiếu còn sửa được (tránh 1 request thừa).
|
||||
const catalogOptions = useQuery({
|
||||
@ -238,6 +271,28 @@ export function KhkkDetailContent({
|
||||
onError: (e) => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
// ─── [S178] NHẬN dòng gốc thay vì đẻ dòng mới ───────────────────────────────────────
|
||||
// 🔴 VÌ SAO PHẢI CÓ: ẩn dòng gốc chưa gán (anh chốt) mà không có đường này thì phiếu KẸT
|
||||
// VĨNH VIỄN — submit-guard BE `ContractSigningPlanWorkflowService.cs:176` chặn trình khi
|
||||
// còn BẤT KỲ dòng nào `CatalogEntryId == null`, mà dòng đó nay đã bị ẩn nên người soạn
|
||||
// không còn ô nào để gán. Ẩn ở FE thôi là CHƯA ĐỦ.
|
||||
// ⇒ Cách ra KHÔNG cần migration, cũng KHÔNG cần nới rào xoá của BE: hạng mục ĐẦU TIÊN của
|
||||
// một NCC thì GÁN THẲNG vào chính dòng gốc của NCC đó (đúng vòng đời mà `Features.cs:530-534`
|
||||
// thiết kế: "mồi NULL, người soạn gán sau"). Dòng gốc hết NULL ⇒ hiện ra thành dòng 1 ⇒
|
||||
// submit-guard thông. Các hạng mục SAU mới đẻ dòng mới (`PeReferenceAmount = 0`).
|
||||
// 🔴 BẤT BIẾN TIỀN GIỮ NGUYÊN: Σ `PeReferenceAmount` không đổi (dòng gốc vẫn giữ số PE của
|
||||
// nó) ⇒ `BudgetManualAmount` của HĐ sinh sau (`CreateContractFromSigningPlanFeatures.cs:200`)
|
||||
// vẫn đúng bằng số PE. Đây là lý do KHÔNG chọn hướng xoá dòng gốc.
|
||||
const adoptSeedLine = useMutation({
|
||||
mutationFn: async (p: { lineId: string; catalogEntryId: string; proposedAmount: number }) =>
|
||||
api.put(`/contract-signing-plans/${id}/lines/${p.lineId}`, {
|
||||
catalogEntryId: p.catalogEntryId,
|
||||
proposedAmount: p.proposedAmount,
|
||||
}),
|
||||
onSuccess: () => { toast.success('Đã thêm hạng mục'); setAddFor(null); invalidate() },
|
||||
onError: (e) => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const deleteLine = useMutation({
|
||||
mutationFn: async (lineId: string) => api.delete(`/contract-signing-plans/${id}/lines/${lineId}`),
|
||||
onSuccess: () => { toast.success('Đã xoá hạng mục'); invalidate() },
|
||||
@ -543,7 +598,10 @@ export function KhkkDetailContent({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.lines.map((l, i) => (
|
||||
{/* [S178] `visibleLines`, KHÔNG phải `plan.lines`: dòng gốc chưa gán hạng mục
|
||||
bị ẩn (xem khối tính ở trên). STT vẫn là thứ tự HIỂN THỊ nên đánh theo
|
||||
danh sách ĐÃ LỌC — dùng index của mảng gốc sẽ thủng số ngay dòng đầu. */}
|
||||
{visibleLines.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. */}
|
||||
@ -662,7 +720,7 @@ export function KhkkDetailContent({
|
||||
trùng", đúng thứ chú-thích ở đó cấm.
|
||||
⇒ Dòng này là **dòng nháp phía client**: trông và xếp cột y hệt dòng thật,
|
||||
nhưng chỉ gọi API MỘT LẦN khi đã có hạng mục ⇒ máy giữ nguyên bất biến. */}
|
||||
{isDraft && addFor && (
|
||||
{isDraft && effectiveAddFor && (
|
||||
// [S177 — anh: "bỏ cả khối dòng nhập"] Bỏ VẺ KHỐI RIÊNG (nền brand + viền
|
||||
// brand) — dùng đúng class của dòng dữ liệu thật ở trên, để nó đọc thành
|
||||
// "một dòng nữa của bảng" chứ không phải "một hộp lạ chen vào giữa bảng".
|
||||
@ -680,6 +738,22 @@ export function KhkkDetailContent({
|
||||
nhớ mình vừa bấm `+` ở dòng nào. Anh đã được nêu đánh đổi này và chốt
|
||||
gỡ. Nếu về sau vướng: rẻ nhất là chèn dòng nhập NGAY DƯỚI dòng vừa bấm
|
||||
(vị trí tự nói NCC) thay vì thêm lại chữ. */}
|
||||
{/* [S178] Ô chọn NCC — CHỈ hiện khi thật sự không suy ra được: còn ≥2 NCC
|
||||
chưa có dòng nào hiện. Phiếu 1 NCC (ca thường) KHÔNG thấy ô này, đúng
|
||||
"máy rộng, UI hẹp". Thiếu nó thì phiếu liên-danh nhiều NCC sẽ có NCC
|
||||
thứ 2 không bao giờ với tới được sau khi dòng gốc bị ẩn. */}
|
||||
{unreachableSuppliers.length > 1 && !visibleSupplierIds.has(effectiveAddFor.supplierId) && (
|
||||
<select
|
||||
value={effectiveAddFor.supplierId}
|
||||
onChange={(e) => setAddFor({ supplierId: e.target.value })}
|
||||
aria-label="Nhà cung cấp của dòng mới"
|
||||
className="mb-1 h-7 w-full rounded-md border border-slate-200 bg-white px-2 text-[11px] text-slate-600 focus-visible:border-brand-500 focus-visible:outline-none"
|
||||
>
|
||||
{unreachableSuppliers.map(([sid, sname]) => (
|
||||
<option key={sid} value={sid}>{sname ?? '(NCC không tên)'}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<select
|
||||
value={addCatalogId}
|
||||
onChange={(e) => setAddCatalogId(e.target.value)}
|
||||
@ -710,16 +784,40 @@ export function KhkkDetailContent({
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!addCatalogId || addAmount === '' || Number(addAmount) < 0 || addLine.isPending}
|
||||
onClick={() => addLine.mutate({
|
||||
supplierId: addFor.supplierId,
|
||||
catalogEntryId: addCatalogId,
|
||||
proposedAmount: Number(addAmount),
|
||||
})}
|
||||
disabled={!addCatalogId || addAmount === '' || Number(addAmount) < 0
|
||||
|| addLine.isPending || adoptSeedLine.isPending}
|
||||
// [S178] 2 đường, chọn theo việc NCC này còn dòng gốc chưa gán hay không:
|
||||
// còn → GÁN vào dòng gốc (PUT) ⇒ hết dòng NULL ⇒ submit-guard thông
|
||||
// hết → đẻ dòng mới (POST) như cũ
|
||||
// Tìm lại dòng gốc ngay tại đây (không cache) vì `plan.lines` đã tươi
|
||||
// sau mỗi `invalidate()`; giữ biến ngoài dễ trỏ vào dòng vừa bị gán.
|
||||
onClick={() => {
|
||||
const seed = plan.lines.find(
|
||||
(x) => isUnassignedSeed(x) && x.supplierId === effectiveAddFor.supplierId)
|
||||
if (seed) {
|
||||
adoptSeedLine.mutate({
|
||||
lineId: seed.id,
|
||||
catalogEntryId: addCatalogId,
|
||||
proposedAmount: Number(addAmount),
|
||||
})
|
||||
} else {
|
||||
addLine.mutate({
|
||||
supplierId: effectiveAddFor.supplierId,
|
||||
catalogEntryId: addCatalogId,
|
||||
proposedAmount: Number(addAmount),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{addLine.isPending ? 'Đang thêm…' : 'Thêm'}
|
||||
{addLine.isPending || adoptSeedLine.isPending ? 'Đang thêm…' : 'Thêm'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setAddFor(null)}>Hủy</Button>
|
||||
{/* [S178] "Hủy" CHỈ hiện khi dòng nhập do người dùng MỞ (bấm `+`).
|
||||
Lúc nó TỰ MỞ vì còn NCC chưa với tới được thì Hủy là nút chết —
|
||||
bấm xong `effectiveAddFor` tính lại và mở y như cũ. Bày một nút
|
||||
không làm được gì còn tệ hơn không bày. */}
|
||||
{addFor && (
|
||||
<Button variant="outline" size="sm" onClick={() => setAddFor(null)}>Hủy</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -743,7 +841,11 @@ export function KhkkDetailContent({
|
||||
ô 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))}
|
||||
{/* [S178 — anh chốt "TỔNG = Σ hạng mục thật"] Cộng theo `visibleLines`:
|
||||
dòng gốc chưa gán KHÔNG vào TỔNG, nếu không thì con số trên màn không
|
||||
khớp các dòng nhìn thấy và người dùng sẽ đọc thành lỗi. Người soạn tách
|
||||
dần hạng mục thì TỔNG bò lên dần tới số PE. */}
|
||||
{formatVnd(visibleLines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0))}
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
@ -1004,10 +1106,17 @@ export function KhkkDetailContent({
|
||||
const unassigned = plan.lines.filter(l => !l.catalogEntryId).length
|
||||
const noWorkflow = !plan.approvalWorkflowId
|
||||
const blocked = noWorkflow || unassigned > 0 || plan.lines.length === 0
|
||||
// 🔴 [S178] Câu chặn phải nói THỨ NGƯỜI DÙNG NHÌN THẤY. Dòng gốc chưa gán nay bị
|
||||
// ẩn (anh chốt), nên câu cũ "còn N dòng chưa gán hạng mục" trỏ vào dòng KHÔNG CÓ
|
||||
// trên màn ⇒ người dùng đi tìm mãi không ra. Khi mọi dòng chưa-gán đều là dòng gốc
|
||||
// bị ẩn thì nói theo NCC + đúng thao tác gỡ kẹt (thêm hạng mục cho NCC đó).
|
||||
const unassignedAllHidden = unassigned > 0
|
||||
&& plan.lines.filter(l => !l.catalogEntryId).every(isUnassignedSeed)
|
||||
const why = noWorkflow ? 'Chưa pin quy trình duyệt cho phiếu.'
|
||||
: plan.lines.length === 0 ? 'Phiếu chưa có dòng NCC nào.'
|
||||
: unassigned > 0 ? `Còn ${unassigned} dòng chưa gán hạng mục.`
|
||||
: undefined
|
||||
: unassignedAllHidden ? `Còn ${unassigned} nhà cung cấp chưa có hạng mục nào — thêm hạng mục cho họ trước khi trình.`
|
||||
: unassigned > 0 ? `Còn ${unassigned} dòng chưa gán hạng mục.`
|
||||
: undefined
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user