Compare commits
3 Commits
bf177408b0
...
7f38c02ab3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f38c02ab3 | |||
| d5c6f12fc6 | |||
| 19712d89fc |
@ -1,6 +1,19 @@
|
||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||
|
||||
**Last updated:** 2026-05-07 (Session 11 chốt — **Migration 17 manual budget fields PE + HĐ + toggle "Nhập tay". 5 commit per-chunk. 83 test pass.**)
|
||||
**Last updated:** 2026-05-07 (Session 11+ chốt — **PE BudgetFieldRow inline editor + 3 commit FE-only.**)
|
||||
|
||||
## TL;DR Session 11+ housekeeping (07/05 — inline budget editor)
|
||||
|
||||
User feedback after Session 11: muốn toggle + 2 fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" của PeDetailTabs, không chỉ ở "Sửa header" page. Empty values cứ empty.
|
||||
|
||||
- ✅ **BudgetFieldRow component** (~125 LOC) thay FormRow tĩnh cũ ở `b. Ngân sách`. canEdit=`!readOnly && isDraft`. Save dùng existing PUT endpoint (KHÔNG cần BE thay đổi).
|
||||
- ✅ **fe-admin** Chunk 1 (commit `19712d8`) · **fe-user mirror** Chunk 2 (commit `d5c6f12`).
|
||||
- ✅ **Verify**: 2 build pass + 0 TS error.
|
||||
|
||||
UX:
|
||||
- **Workspace + Danh sách + isDraft** → editable inline: toggle + Select OR 2 input + nút Lưu khi dirty
|
||||
- **Duyệt + !isDraft** → display only (link card / manual values / "—" empty)
|
||||
- **Empty state**: hiển thị "—" thay vì "(chưa link)" verbose (per user)
|
||||
|
||||
## TL;DR Session 11 (07/05 — Migration 17 manual budget fields)
|
||||
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
|
||||
| Ngày | Ai | Task | Commit |
|
||||
|---|---|---|---|
|
||||
| 2026-05-07 | Claude | **PE BudgetFieldRow inline editor — toggle + 2 fields trong Section 2 (cả 3 view)** — User feedback after Session 11: muốn toggle "Nhập tay" + 2 input fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" của PeDetailTabs — KHÔNG chỉ ở "Sửa header" page, mà cả 3 view (Workspace / Danh sách / Duyệt). Empty values cứ hiển thị empty (không text "(chưa link)" verbose). 2 chunk mirror per-commit (build pass mỗi chunk): C1 fe-admin BudgetFieldRow component (~125 LOC) thay FormRow tĩnh ở `b. Ngân sách`. canEdit=`!readOnly && isDraft`: render toggle + Select Budget OR 2 input grid 2-col + nút "Lưu ngân sách" (chỉ hiện khi dirty) + "Hủy thay đổi". Save: full PUT `/pe/:id` với current entity values + new budget payload. Read-only mode (Duyệt + !isDraft): chỉ display, KHÔNG toggle/inputs, empty hiển thị "—". C2 fe-user mirror y hệt 1 file (rule §3.9). Mỗi chunk: build pass + 0 TS error. KHÔNG đụng BE — re-use existing PUT endpoint. | `19712d8` (C1) · `d5c6f12` (C2) · (current C3) |
|
||||
| 2026-05-07 | Claude | **Migration 17 — manual budget fields fallback cho PE + HĐ (toggle "Nhập tay")** — User feedback: khi project chưa có Budget approved (Phase=DaDuyet eligible), user phải break flow đi tạo Budget + duyệt + quay lại link. UX kém. Solution: thêm fallback "Nhập tay" — checkbox toggle cạnh Label Ngân sách, khi ON → hide Select Budget, show 2 input field grid 2-col (Tên tham chiếu text + Số tiền number formatted VND). Lưu trên entity row, KHÔNG cần Budget entity. Q1-3 chốt: 1 = stick-toggle reveal 2 input fields; 2 = cả BudgetId + manual fields cùng null OK (KHÔNG XOR validate); 3 = mirror logic sang HĐ luôn (cả 7 ContractType qua ContractCreatePage). 5 chunk per-commit (build + 83 test pass mỗi chunk): C1 Migration 17 `AddManualBudgetFieldsToPeAndContract` 4 ALTER (PE + HĐ × Name nvarchar(200) + Amount decimal(18,2)) + Domain 2 entity + 2 EF config (HasMaxLength + HasPrecision) — applied LocalDB OK, 3-file rule. C2 App CQRS — CreatePurchaseEvaluationCommand + Update + DTO + Validator (>=0 when has value), CreateContractCommand + Update + DTO + diff log audit, CreateContractFromEvaluation carry forward pe.BudgetManualName/Amount → contract khi gen HĐ từ phiếu. C3 FE-Admin — types +2 field, PeHeaderForm toggle + 2 input + payload conditional (manual mode clear budgetId, link mode clear manual), PeDetailTabs Section "b. Ngân sách" fallback display "Tên · Số tiền + badge nhập tay" khi !budget + có manual data, refactor PurchaseEvaluationCreatePage wrap PeHeaderForm DRY (222→30 LOC), ContractCreatePage NewContractForm + EditContractForm cùng pattern + read-only display branch khi !isDraft. C4 fe-user mirror y hệt 6 file. C5 docs (this row + HANDOFF + session log). KHÔNG đụng Budget entity / Phase=DaDuyet validation (giữ invariant). | `ecd5f7e` (C1) · `0f7901c` (C2) · `bab5031` (C3) · `14f8d9d` (C4) · (current C5) |
|
||||
| 2026-05-07 | Claude | **PE "Thao tác" 2-panel workspace + Panel 1 read-only picker + Section 5 disabled** — User chỉ thị restructure leaf "Thao tác" (Pe_DuyetNcc_Create + Pe_DuyetNccPhuongAn_Create) từ page tạo header riêng (`/purchase-evaluations/new?type=N` — chỉ form Tên/Project/Địa điểm/Payment/Budget) sang workspace 2-panel mirror pattern HĐ Thầu phụ ContractCreatePage. 5 câu chốt spec trước code: Q1 Panel 2 KHÔNG render Workflow Panel + Approvals + History (chỉ data entry); Panel 1 = pure picker, KHÔNG inline edit/delete; Q2 mirror HĐ Thầu phụ pattern (sticky "+ Thêm mới" + Panel 2 transition new→edit form); Q3 leaf "Danh sách" + "Duyệt" giữ 3-panel hiện tại; Q4 route mới `/purchase-evaluations/workspace?type={1\|2}`; Q5 Section 5 Ý kiến 4PB disable trong workspace (vì người ta nhập khi duyệt, không phải lúc nhập liệu). 2 chunk per-commit (build + 83 test pass mỗi chunk): C1 fe-admin (3 file mới `PeListPanel.tsx` ~180 LOC pure picker reuse + `PeHeaderForm.tsx` ~210 LOC extract + `PurchaseEvaluationWorkspacePage.tsx` ~120 LOC, 3 file sửa `PeDetailTabs.tsx` thêm prop `mode?: 'detail' \| 'workspace'` + Section 5 hint banner amber + Layout.tsx resolver `Pe_*_Create`→`/workspace?type=N` + App.tsx route mới). C2 fe-user mirror y hệt 6 file (rule §3.9). KHÔNG đụng BE / migration / schema / endpoint. Route `/new` cũ giữ tồn tại cho deep-link "Sửa header" button. **Total +1142 LOC FE / 0 BE / 32 FE pages.** | `ee0d360` (C1) · `ecf3c59` (C2) · (current C3) |
|
||||
| 2026-05-04 | Claude | **User Manual 7 file rewrite compact cho end-user** — User feedback "ko cần quá đầy đủ chi tiết, cho end-user họ làm". Setup `package.json` + `npm install docx@9.5.0` + script `npm run gen:all`. Rewrite 7 generator scripts theo style end-user friendly: BỎ field validation table 5 cột (Tên field/Kiểu/Bắt buộc/Validation/Ví dụ), BỎ error troubleshoot table 3 cột (Lỗi/Nguyên nhân/Cách xử lý), BỎ FAQ chi tiết 8 câu (giữ 1 chương "Khi gặp lỗi" 4-5 bullet), BỎ phím tắt table. GIỮ: tổng quan ngắn 1-2 câu mỗi chức năng, numbered steps đơn giản 3-7 bước, note/warn/tip chỉ critical, bullet liệt kê. 7 file: 01-Bat-dau (12.1KB cũ 21.7KB ↓44%) / 02-Hop-dong / 03-Duyet-Workflow (mention 2-stage NV/TPB Mig 16) / 04-PE-Phieu-Duyet-NCC (A/B + 4PB + tạo HĐ) / 05-Budget / 06-7-Loai-HD-Cheatsheet / admin-02-Quan-ly-Users-Roles (mention bypass review S9). Refactor user-01 dùng `_helpers.js` shared (trước có helpers inline 793 dòng, giờ ~110 dòng). Tổng output ~86KB / 7 file (cũ ~123KB ↓30%). | `16c2c9c` |
|
||||
|
||||
@ -32,7 +32,8 @@ import {
|
||||
type PeQuote,
|
||||
type PeSupplier,
|
||||
} from '@/types/purchaseEvaluation'
|
||||
import type { Supplier } from '@/types/master'
|
||||
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
||||
import type { Paged, Supplier } from '@/types/master'
|
||||
|
||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||
|
||||
@ -306,6 +307,158 @@ function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
||||
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
||||
// Duyệt). Edit chỉ enable khi !readOnly + isDraft (Drafter sửa). Read-only
|
||||
// khi pendingMe=1 hoặc phase đã chuyển khỏi DangSoanThao. Empty values hiển
|
||||
// thị empty (per user 2026-05-07).
|
||||
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||
const canEdit = !readOnly && isDraft
|
||||
const qc = useQueryClient()
|
||||
|
||||
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
||||
const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId
|
||||
const [manualMode, setManualMode] = useState(initialManual)
|
||||
const [budgetId, setBudgetId] = useState(ev.budgetId ?? '')
|
||||
const [manualName, setManualName] = useState(ev.budgetManualName ?? '')
|
||||
const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0)
|
||||
|
||||
// Eligible budgets — chỉ fetch khi user có khả năng edit
|
||||
const eligibleBudgets = useQuery({
|
||||
queryKey: ['eligible-budgets', ev.projectId],
|
||||
queryFn: async () => (await api.get<Paged<BudgetListItem>>('/budgets', {
|
||||
params: { pageSize: 100, projectId: ev.projectId, phase: BudgetPhase.DaDuyet },
|
||||
})).data.items,
|
||||
enabled: canEdit,
|
||||
})
|
||||
|
||||
// Dirty detect — compare current state vs ev original
|
||||
const dirty = manualMode !== initialManual
|
||||
|| (manualMode && (manualName !== (ev.budgetManualName ?? '') || manualAmount !== (ev.budgetManualAmount ?? 0)))
|
||||
|| (!manualMode && budgetId !== (ev.budgetId ?? ''))
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload = manualMode
|
||||
? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null }
|
||||
: { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null }
|
||||
await api.put(`/purchase-evaluations/${ev.id}`, {
|
||||
id: ev.id,
|
||||
tenGoiThau: ev.tenGoiThau,
|
||||
diaDiem: ev.diaDiem,
|
||||
moTa: ev.moTa,
|
||||
paymentTerms: ev.paymentTerms,
|
||||
...payload,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Đã cập nhật ngân sách')
|
||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||
qc.invalidateQueries({ queryKey: ['pe-list'] })
|
||||
},
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
// Read-only mode: chỉ display (không toggle, không edit)
|
||||
if (!canEdit) {
|
||||
return (
|
||||
<FormRow
|
||||
label="b. Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}{ev.budget.tenNganSach}
|
||||
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : ev.budgetManualAmount != null || ev.budgetManualName ? (
|
||||
<span className="text-slate-700">
|
||||
{ev.budgetManualName && <span>{ev.budgetManualName}</span>}
|
||||
{ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
|
||||
{ev.budgetManualAmount != null && (
|
||||
<span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
|
||||
)}
|
||||
<span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
|
||||
</span>
|
||||
) : <span className="text-slate-400">—</span>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Editable mode (canEdit=true)
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={manualMode}
|
||||
onChange={e => setManualMode(e.target.checked)}
|
||||
className="h-3.5 w-3.5 rounded border-slate-300"
|
||||
/>
|
||||
Nhập tay (không link)
|
||||
</label>
|
||||
{!manualMode ? (
|
||||
<Select
|
||||
value={budgetId}
|
||||
onChange={e => setBudgetId(e.target.value)}
|
||||
className="text-sm"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{eligibleBudgets.data?.map(b => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<Input
|
||||
value={manualName}
|
||||
onChange={e => setManualName(e.target.value)}
|
||||
placeholder="Tên ngân sách (vd Tạm tính T11/2025)"
|
||||
maxLength={200}
|
||||
className="text-sm"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={manualAmount || ''}
|
||||
onChange={e => setManualAmount(Number(e.target.value))}
|
||||
placeholder="Số tiền (đ)"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{dirty && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => save.mutate()}
|
||||
disabled={save.isPending}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
{save.isPending ? 'Đang lưu…' : 'Lưu ngân sách'}
|
||||
</Button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setManualMode(initialManual)
|
||||
setBudgetId(ev.budgetId ?? '')
|
||||
setManualName(ev.budgetManualName ?? '')
|
||||
setManualAmount(ev.budgetManualAmount ?? 0)
|
||||
}}
|
||||
className="text-[11px] text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
Hủy thay đổi
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) =====
|
||||
function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
|
||||
const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||
@ -335,27 +488,7 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
|
||||
<span className="font-medium text-emerald-700">✓ {ev.selectedSupplierName}</span>
|
||||
) : <span className="text-slate-400">— (chưa chọn)</span>}
|
||||
/>
|
||||
<FormRow
|
||||
label="b. Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}{ev.budget.tenNganSach}
|
||||
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : ev.budgetManualAmount != null || ev.budgetManualName ? (
|
||||
// Mig 17 — manual budget fallback: hiển thị tên + số tiền nhập tay,
|
||||
// không phải link vào /budgets/{id} (không có Budget entity).
|
||||
<span className="text-slate-700">
|
||||
{ev.budgetManualName && <span>{ev.budgetManualName}</span>}
|
||||
{ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
|
||||
{ev.budgetManualAmount != null && (
|
||||
<span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
|
||||
)}
|
||||
<span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
|
||||
</span>
|
||||
) : <span className="text-slate-400">— (chưa link)</span>}
|
||||
/>
|
||||
<BudgetFieldRow ev={ev} readOnly={readOnly} />
|
||||
<FormRow
|
||||
label="c. Giá chào thầu"
|
||||
value={giaChaoThau != null ? (
|
||||
|
||||
@ -32,7 +32,8 @@ import {
|
||||
type PeQuote,
|
||||
type PeSupplier,
|
||||
} from '@/types/purchaseEvaluation'
|
||||
import type { Supplier } from '@/types/master'
|
||||
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
||||
import type { Paged, Supplier } from '@/types/master'
|
||||
|
||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||
|
||||
@ -306,6 +307,158 @@ function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
||||
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
||||
// Duyệt). Edit chỉ enable khi !readOnly + isDraft (Drafter sửa). Read-only
|
||||
// khi pendingMe=1 hoặc phase đã chuyển khỏi DangSoanThao. Empty values hiển
|
||||
// thị empty (per user 2026-05-07).
|
||||
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||
const canEdit = !readOnly && isDraft
|
||||
const qc = useQueryClient()
|
||||
|
||||
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
||||
const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId
|
||||
const [manualMode, setManualMode] = useState(initialManual)
|
||||
const [budgetId, setBudgetId] = useState(ev.budgetId ?? '')
|
||||
const [manualName, setManualName] = useState(ev.budgetManualName ?? '')
|
||||
const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0)
|
||||
|
||||
// Eligible budgets — chỉ fetch khi user có khả năng edit
|
||||
const eligibleBudgets = useQuery({
|
||||
queryKey: ['eligible-budgets', ev.projectId],
|
||||
queryFn: async () => (await api.get<Paged<BudgetListItem>>('/budgets', {
|
||||
params: { pageSize: 100, projectId: ev.projectId, phase: BudgetPhase.DaDuyet },
|
||||
})).data.items,
|
||||
enabled: canEdit,
|
||||
})
|
||||
|
||||
// Dirty detect — compare current state vs ev original
|
||||
const dirty = manualMode !== initialManual
|
||||
|| (manualMode && (manualName !== (ev.budgetManualName ?? '') || manualAmount !== (ev.budgetManualAmount ?? 0)))
|
||||
|| (!manualMode && budgetId !== (ev.budgetId ?? ''))
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload = manualMode
|
||||
? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null }
|
||||
: { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null }
|
||||
await api.put(`/purchase-evaluations/${ev.id}`, {
|
||||
id: ev.id,
|
||||
tenGoiThau: ev.tenGoiThau,
|
||||
diaDiem: ev.diaDiem,
|
||||
moTa: ev.moTa,
|
||||
paymentTerms: ev.paymentTerms,
|
||||
...payload,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Đã cập nhật ngân sách')
|
||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||
qc.invalidateQueries({ queryKey: ['pe-list'] })
|
||||
},
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
// Read-only mode: chỉ display (không toggle, không edit)
|
||||
if (!canEdit) {
|
||||
return (
|
||||
<FormRow
|
||||
label="b. Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}{ev.budget.tenNganSach}
|
||||
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : ev.budgetManualAmount != null || ev.budgetManualName ? (
|
||||
<span className="text-slate-700">
|
||||
{ev.budgetManualName && <span>{ev.budgetManualName}</span>}
|
||||
{ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
|
||||
{ev.budgetManualAmount != null && (
|
||||
<span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
|
||||
)}
|
||||
<span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
|
||||
</span>
|
||||
) : <span className="text-slate-400">—</span>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Editable mode (canEdit=true)
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={manualMode}
|
||||
onChange={e => setManualMode(e.target.checked)}
|
||||
className="h-3.5 w-3.5 rounded border-slate-300"
|
||||
/>
|
||||
Nhập tay (không link)
|
||||
</label>
|
||||
{!manualMode ? (
|
||||
<Select
|
||||
value={budgetId}
|
||||
onChange={e => setBudgetId(e.target.value)}
|
||||
className="text-sm"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{eligibleBudgets.data?.map(b => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<Input
|
||||
value={manualName}
|
||||
onChange={e => setManualName(e.target.value)}
|
||||
placeholder="Tên ngân sách (vd Tạm tính T11/2025)"
|
||||
maxLength={200}
|
||||
className="text-sm"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={manualAmount || ''}
|
||||
onChange={e => setManualAmount(Number(e.target.value))}
|
||||
placeholder="Số tiền (đ)"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{dirty && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => save.mutate()}
|
||||
disabled={save.isPending}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
{save.isPending ? 'Đang lưu…' : 'Lưu ngân sách'}
|
||||
</Button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setManualMode(initialManual)
|
||||
setBudgetId(ev.budgetId ?? '')
|
||||
setManualName(ev.budgetManualName ?? '')
|
||||
setManualAmount(ev.budgetManualAmount ?? 0)
|
||||
}}
|
||||
className="text-[11px] text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
Hủy thay đổi
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) =====
|
||||
function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
|
||||
const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||
@ -335,27 +488,7 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
|
||||
<span className="font-medium text-emerald-700">✓ {ev.selectedSupplierName}</span>
|
||||
) : <span className="text-slate-400">— (chưa chọn)</span>}
|
||||
/>
|
||||
<FormRow
|
||||
label="b. Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}{ev.budget.tenNganSach}
|
||||
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : ev.budgetManualAmount != null || ev.budgetManualName ? (
|
||||
// Mig 17 — manual budget fallback: hiển thị tên + số tiền nhập tay,
|
||||
// không phải link vào /budgets/{id} (không có Budget entity).
|
||||
<span className="text-slate-700">
|
||||
{ev.budgetManualName && <span>{ev.budgetManualName}</span>}
|
||||
{ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
|
||||
{ev.budgetManualAmount != null && (
|
||||
<span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
|
||||
)}
|
||||
<span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
|
||||
</span>
|
||||
) : <span className="text-slate-400">— (chưa link)</span>}
|
||||
/>
|
||||
<BudgetFieldRow ev={ev} readOnly={readOnly} />
|
||||
<FormRow
|
||||
label="c. Giá chào thầu"
|
||||
value={giaChaoThau != null ? (
|
||||
|
||||
Reference in New Issue
Block a user