[CLAUDE] FE-Admin: KHKK gop form them hang muc thanh 1 DONG cua bang (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m49s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m49s
Anh: "Cho 2 cai nay ve lam mot dc ko ?" (mui ten: dong du lieu trong bang <-> panel
"Them hang muc cho:" nam duoi bang). Duoc — panel co dung 3 truong trung khit 3
truong cua mot dong (hang muc / gia / ghi chu), nen no la CHO VE thu hai cho cung
mot thu.
Panel roi -> MOT DONG cua tbody, dung DUNG luoi 4 cot:
cot 1 STT : icon +
cot 2 : dong "Them cho: <NCC>" + select hang muc + o ghi chu DONG PHU
(y het dong du lieu that o tren => hai thu doc thanh CUNG hinh dang)
cot 3 GIA : o so
cot 4 : nut Them / Huy
NCC phai hien vi bang KHONG co cot NCC — khong noi thi khong biet dong moi thuoc ai.
🔴 VI SAO KHONG lam kieu "bam + de ngay dong trong roi nhap tai cho" (dung loi inline
cua cac dong khac, ve mat UI thi con gon hon nua):
AddContractSigningPlanLineCommand:759 khai CatalogEntryId la Guid KHONG nullable +
validator NotEmpty(), boi vi unique index (Plan, Supplier, CatalogEntry) CO Y khong
loc NULL (ContractSigningPlanLineConfiguration.cs:35-38) — trong UNIQUE cua SQL
Server thi NULL == NULL, nen dong trong thu hai cua CUNG mot NCC dung ngay dong goc
cua chinh no. Noi index de lach = "tha troi vo han dong trung", dung thu chu-thich
o do cam.
=> Dong nay la DONG NHAP phia client: trong va xep cot y het dong that, nhung chi goi
API MOT LAN khi da co hang muc ⇒ may giu nguyen bat bien. Gop la gop CHO VE, khong
cat bot truong nao va khong dong vao schema.
Do duoc (2 ve, moi ve co doi chung):
- "Them cho: " = 1 o CA HAI dist => form moi da vao.
- "Them hang muc cho: " (CO dau hai cham, nhan rieng cua panel cu): nguon 3->2
(2 hit con lai la CHU THICH), dist = 0 CA HAI => nhan panel cu da bien mat.
- "Chon hang muc" = 2 trong nguon la DUNG: :553 select cua dong CO SAN (sua tai cho)
⟂ :677 form them. Da truy tung hit thay vi tin con so.
- 🔸 Ky vong dau cua lead SAI 1 lan: doi "Gia de xuat" = 0 sau khi go panel, do ra 6
— vi day la nhan DUNG CHUNG (PeDetailTabs + types/purchaseEvaluation). Da doi sang
discriminator that thay vi ep so cho khop ky vong.
Diff CHI 2 file FE, 0 file BE => .NET test khong the bi anh huong, bo qua vong test do
(khai ro). 2 app build sach · SHA IDENTICAL (verify identical @HEAD TRUOC khi copy de).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -645,6 +645,80 @@ export function KhkkDetailContent({
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* [S177 — anh: "Cho 2 cái này về làm một đc ko?"] Form thêm giờ là MỘT DÒNG
|
||||
của chính bảng, dùng ĐÚNG lưới 4 cột — không còn panel rời bên dưới.
|
||||
🔴 Vì sao KHÔNG làm kiểu "bấm + đẻ ngay dòng trống rồi nhập tại chỗ" (đúng
|
||||
lối inline của các dòng khác): `AddContractSigningPlanLineCommand:759` khai
|
||||
`CatalogEntryId` là `Guid` **không nullable** + validator `NotEmpty()`, vì
|
||||
unique index (Plan, Supplier, CatalogEntry) **CỐ Ý không lọc NULL**
|
||||
(`ContractSigningPlanLineConfiguration.cs:35-38`) — trong UNIQUE của SQL
|
||||
Server thì NULL == NULL, nên dòng trống thứ hai của CÙNG một NCC sẽ đụng
|
||||
ngay dòng gốc của chính nó. Nới index để lách = "thả trôi vô hạn dòng
|
||||
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 && (
|
||||
<tr className="border-b border-brand-200 bg-brand-50/40">
|
||||
<td className="px-3 py-2 text-right align-top text-brand-500">
|
||||
<Plus className="ml-auto h-3.5 w-3.5" />
|
||||
</td>
|
||||
<td className="px-3 py-2 align-top">
|
||||
{/* NCC hiện ở đây vì bảng KHÔNG có cột NCC — không nói thì người dùng
|
||||
không biết dòng mới sẽ thuộc nhà cung cấp nào. */}
|
||||
<div className="mb-1 text-[11px] font-medium text-brand-700">
|
||||
Thêm cho: {addFor.supplierName ?? '(không rõ NCC)'}
|
||||
</div>
|
||||
<select
|
||||
value={addCatalogId}
|
||||
onChange={(e) => setAddCatalogId(e.target.value)}
|
||||
aria-label="Hạng mục của dòng mới"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-xs text-slate-700 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
>
|
||||
<option value="">— Chọn hạng mục —</option>
|
||||
{(catalogOptions.data ?? []).map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.code} — {c.tenVi}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Ghi chú xuống DÒNG PHỤ — y hệt dòng dữ liệu thật ở trên, để hai thứ
|
||||
đọc thành CÙNG một hình dạng chứ không phải hai kiểu form. */}
|
||||
<input
|
||||
type="text" maxLength={1000}
|
||||
value={addNote}
|
||||
onChange={(e) => setAddNote(e.target.value)}
|
||||
placeholder="Ghi chú (tùy chọn)…"
|
||||
aria-label="Ghi chú của dòng mới"
|
||||
className="mt-1 h-7 w-full rounded border border-transparent bg-transparent px-2 text-[11px] text-slate-500 hover:border-slate-200 focus-visible:border-brand-500 focus-visible:bg-white focus-visible:outline-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 align-top">
|
||||
<input
|
||||
type="number" min={0} step={1000}
|
||||
value={addAmount}
|
||||
onChange={(e) => setAddAmount(e.target.value)}
|
||||
placeholder="0"
|
||||
aria-label="Giá của dòng mới"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-right text-xs tabular-nums text-brand-800 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 align-top">
|
||||
<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),
|
||||
note: addNote.trim() || undefined,
|
||||
})}
|
||||
>
|
||||
{addLine.isPending ? 'Đang thêm…' : 'Thêm'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setAddFor(null)}>Hủy</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* 🧊 [S177 — anh: "bỏ đi"] Khối "Thêm hạng mục cho: [NCC…]" ĐÃ GỠ HẲN.
|
||||
Vòng đời đủ 3 nấc trong CÙNG lượt S177, giữ vết để khỏi ai dựng lại:
|
||||
(1) S175 đặt DƯỚI bảng → (2) anh: "+ thêm hạng mục ở đây" ⇒ dời vào
|
||||
@ -678,63 +752,11 @@ export function KhkkDetailContent({
|
||||
mới ở đây là ký với nhà thầu chưa ai chọn (BE cũng chặn, 409).
|
||||
🔴 Giữ 1 dòng vết vì đây là chỗ người sau sẽ tìm khi hỏi "nút thêm đâu rồi". */}
|
||||
|
||||
{addFor && (
|
||||
<div className="mt-2 rounded-md border border-brand-200 bg-brand-50/50 p-3">
|
||||
<div className="mb-2 text-[12px] font-semibold text-brand-800">
|
||||
Thêm hạng mục cho: {addFor.supplierName ?? '(không rõ NCC)'}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="min-w-[14rem] flex-1">
|
||||
<span className="label-eyebrow">Hạng mục</span>
|
||||
<select
|
||||
value={addCatalogId}
|
||||
onChange={(e) => setAddCatalogId(e.target.value)}
|
||||
className="mt-0.5 h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-xs text-slate-700"
|
||||
>
|
||||
<option value="">— Chọn hạng mục —</option>
|
||||
{(catalogOptions.data ?? []).map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.code} — {c.tenVi}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="w-[9rem]">
|
||||
<span className="label-eyebrow">Giá đề xuất</span>
|
||||
<input
|
||||
type="number" min={0} step={1000}
|
||||
value={addAmount}
|
||||
onChange={(e) => setAddAmount(e.target.value)}
|
||||
placeholder="0"
|
||||
className="mt-0.5 h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-right text-xs tabular-nums"
|
||||
/>
|
||||
</label>
|
||||
<label className="min-w-[10rem] flex-1">
|
||||
<span className="label-eyebrow">Ghi chú</span>
|
||||
<input
|
||||
type="text" maxLength={1000}
|
||||
value={addNote}
|
||||
onChange={(e) => setAddNote(e.target.value)}
|
||||
placeholder="(tùy chọn)"
|
||||
className="mt-0.5 h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-xs"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!addCatalogId || addAmount === '' || Number(addAmount) < 0 || addLine.isPending}
|
||||
onClick={() => addLine.mutate({
|
||||
supplierId: addFor.supplierId,
|
||||
catalogEntryId: addCatalogId,
|
||||
proposedAmount: Number(addAmount),
|
||||
note: addNote.trim() || undefined,
|
||||
})}
|
||||
>
|
||||
{addLine.isPending ? 'Đang thêm…' : 'Thêm'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setAddFor(null)}>Hủy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 🧊 [S177 — anh: "Cho 2 cái này về làm một đc ko?"] Panel "Thêm hạng mục" rời
|
||||
ĐÃ GỠ — nội dung của nó nay là 1 DÒNG trong `tbody` (xem trên), dùng chung lưới
|
||||
4 cột với các dòng thật. Giữ vết vì đây là chỗ người sau tìm khi hỏi form đâu.
|
||||
🔴 Đúng 3 trường cũ (hạng mục / giá / ghi chú) đi theo nguyên vẹn — gộp là gộp
|
||||
CHỖ VẼ, không cắt bớt trường nào. */}
|
||||
</>
|
||||
)}
|
||||
{/* 🧊 [S175] 3 dòng chú-thích dưới bảng ĐÃ GỠ — anh: "Bỏ mấy dòng này đi".
|
||||
|
||||
Reference in New Issue
Block a user