[CLAUDE] App+Api+FE+Scripts: Edit detail row inline + deps audit helper
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m45s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m45s
## Edit detail row inline (BE)
7 typed UpdateXxxDetailCommand handler trong ContractDetailsFeatures.cs
— pattern lặp giống Add commands, EnsureContractType guard + log
ChangelogAction.Update với summary "Sửa <hạng mục/SP/CV/...>".
7 PUT endpoints trong ContractsController:
- PUT /contracts/{id}/details/{thau-phu|giao-khoan|nha-cung-cap|dich-vu|
mua-ban|nguyen-tac-ncc|nguyen-tac-dv}/{detailId}
## Edit detail row inline (FE)
ContractDetailsTab.tsx refactor:
- DeleteBtn → ActionBtns (Pencil + Trash) với onEdit + onDelete callbacks
- 7 XxxTable signatures + onEdit prop + pass row data via callback
- New EditRowDialog component:
* useEffect populate form từ row data khi target thay đổi
* Reuse FIELDS_BY_TYPE config + buildPayload (compute thanhTien)
* Date field convert ISO → yyyy-MM-dd cho input[type=date]
* PUT /contracts/{id}/details/{slug}/{detailId}
- Parent state editTarget — open dialog, close khi save thành công
Mirror fe-admin (file copy).
## Deps audit helper script
scripts/deps-audit.ps1 — chạy thủ công hoặc CI integration:
- dotnet list package --vulnerable --include-transitive (BE)
- npm audit --audit-level=moderate (fe-admin + fe-user)
- Color-coded output (green/red), summary cuối
- -FailOnHigh switch để CI gate
Skill ref .claude/skills/dependency-audit-erp/SKILL.md (đã có) cho
pin constraints + workflow fix.
## Build
- BE: dotnet build pass (0 error)
- fe-user: tsc + vite pass (11.52s)
- fe-admin: tsc + vite pass (577ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,12 +1,13 @@
|
||||
// Tab "Chi tiết" — hiện table line items theo loại HĐ (7 schema khác nhau,
|
||||
// auto pick render component theo bundle.type). Add row form ở footer (chỉ
|
||||
// khi Phase=DangSoanThao và user là drafter — owner mới được edit details).
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { Pencil, Plus, Trash2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Dialog } from '@/components/ui/Dialog'
|
||||
import { api } from '@/lib/api'
|
||||
import { getErrorMessage } from '@/lib/apiError'
|
||||
import { ContractPhase, type ContractDetail } from '@/types/contracts'
|
||||
@ -21,6 +22,10 @@ import type {
|
||||
NguyenTacDvDetail,
|
||||
} from '@/types/contract-details'
|
||||
|
||||
// Generic shape của 1 row đang edit (parent state). Sub-tables type-narrow
|
||||
// qua bundle.type khi dispatch onEdit.
|
||||
type EditTarget = { row: Record<string, unknown> }
|
||||
|
||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||
|
||||
// Map ContractType → URL slug + render config
|
||||
@ -38,21 +43,23 @@ const TYPE_TO_SLUG: Record<number, TypeKey> = {
|
||||
export function ContractDetailsTab({ contract }: { contract: ContractDetail }) {
|
||||
const qc = useQueryClient()
|
||||
const canEdit = contract.phase === ContractPhase.DangSoanThao
|
||||
const [editTarget, setEditTarget] = useState<EditTarget | null>(null)
|
||||
|
||||
const bundleQuery = useQuery({
|
||||
queryKey: ['contract-details', contract.id],
|
||||
queryFn: async () => (await api.get<ContractDetailsBundle>(`/contracts/${contract.id}/details`)).data,
|
||||
})
|
||||
|
||||
function invalidate() {
|
||||
qc.invalidateQueries({ queryKey: ['contract-details', contract.id] })
|
||||
qc.invalidateQueries({ queryKey: ['contract-changelogs', contract.id] })
|
||||
}
|
||||
|
||||
const deleteRow = useMutation({
|
||||
mutationFn: async (detailId: string) => {
|
||||
await api.delete(`/contracts/${contract.id}/details/${detailId}`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['contract-details', contract.id] })
|
||||
qc.invalidateQueries({ queryKey: ['contract-changelogs', contract.id] })
|
||||
toast.success('Đã xóa')
|
||||
},
|
||||
onSuccess: () => { invalidate(); toast.success('Đã xóa') },
|
||||
onError: err => toast.error(getErrorMessage(err)),
|
||||
})
|
||||
|
||||
@ -61,25 +68,26 @@ export function ContractDetailsTab({ contract }: { contract: ContractDetail }) {
|
||||
|
||||
const bundle = bundleQuery.data
|
||||
|
||||
const onEdit = canEdit
|
||||
? (row: Record<string, unknown>) => setEditTarget({ row })
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{bundle.type === 1 && <ThauPhuTable rows={bundle.thauPhu} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 2 && <GiaoKhoanTable rows={bundle.giaoKhoan} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 3 && <NhaCungCapTable rows={bundle.nhaCungCap} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 4 && <DichVuTable rows={bundle.dichVu} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 5 && <MuaBanTable rows={bundle.muaBan} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 6 && <NguyenTacNccTable rows={bundle.nguyenTacNcc} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 7 && <NguyenTacDvTable rows={bundle.nguyenTacDv} onDelete={deleteRow.mutate} canEdit={canEdit} />}
|
||||
{bundle.type === 1 && <ThauPhuTable rows={bundle.thauPhu} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 2 && <GiaoKhoanTable rows={bundle.giaoKhoan} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 3 && <NhaCungCapTable rows={bundle.nhaCungCap} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 4 && <DichVuTable rows={bundle.dichVu} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 5 && <MuaBanTable rows={bundle.muaBan} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 6 && <NguyenTacNccTable rows={bundle.nguyenTacNcc} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
{bundle.type === 7 && <NguyenTacDvTable rows={bundle.nguyenTacDv} onDelete={deleteRow.mutate} onEdit={onEdit} canEdit={canEdit} />}
|
||||
|
||||
{canEdit && (
|
||||
<AddRowForm
|
||||
contractId={contract.id}
|
||||
contractType={contract.type}
|
||||
existingCount={getRowCount(bundle)}
|
||||
onAdded={() => {
|
||||
qc.invalidateQueries({ queryKey: ['contract-details', contract.id] })
|
||||
qc.invalidateQueries({ queryKey: ['contract-changelogs', contract.id] })
|
||||
}}
|
||||
onAdded={invalidate}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -88,6 +96,14 @@ export function ContractDetailsTab({ contract }: { contract: ContractDetail }) {
|
||||
⚠ Chỉ sửa được chi tiết khi HĐ ở phase "Đang soạn thảo". Hiện tại HĐ đã chuyển sang phase khác.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EditRowDialog
|
||||
target={editTarget}
|
||||
contractId={contract.id}
|
||||
contractType={contract.type}
|
||||
onClose={() => setEditTarget(null)}
|
||||
onSaved={() => { invalidate(); setEditTarget(null) }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -122,15 +138,28 @@ function TableShell({ headers, totalRow, children }: { headers: string[]; totalR
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteBtn({ onDelete }: { onDelete: () => void }) {
|
||||
function ActionBtns({ onEdit, onDelete }: { onEdit?: () => void; onDelete: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => { if (confirm('Xóa dòng này?')) onDelete() }}
|
||||
className="text-slate-400 hover:text-red-600"
|
||||
aria-label="Xóa"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="flex justify-end gap-0.5">
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="rounded p-0.5 text-slate-400 hover:bg-slate-100 hover:text-brand-600"
|
||||
aria-label="Sửa"
|
||||
title="Sửa dòng"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { if (confirm('Xóa dòng này?')) onDelete() }}
|
||||
className="rounded p-0.5 text-slate-400 hover:bg-slate-100 hover:text-red-600"
|
||||
aria-label="Xóa"
|
||||
title="Xóa dòng"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -138,7 +167,7 @@ function totalOf(rows: { thanhTien: number }[]): number {
|
||||
return rows.reduce((s, r) => s + (r.thanhTien ?? 0), 0)
|
||||
}
|
||||
|
||||
function ThauPhuTable({ rows, onDelete, canEdit }: { rows: ThauPhuDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function ThauPhuTable({ rows, onDelete, onEdit, canEdit }: { rows: ThauPhuDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell
|
||||
headers={['Hạng mục', 'ĐVT', 'Khối lượng', 'Đơn giá', 'Thành tiền', 'Hoàn thành', 'Ghi chú']}
|
||||
@ -155,14 +184,14 @@ function ThauPhuTable({ rows, onDelete, canEdit }: { rows: ThauPhuDetail[]; onDe
|
||||
<td className="px-2 py-1.5 text-right font-medium">{fmtMoney(r.thanhTien)}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.thoiGianHoanThanh ? new Date(r.thoiGianHoanThanh).toLocaleDateString('vi-VN') : '—'}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.ghiChu ?? ''}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function GiaoKhoanTable({ rows, onDelete, canEdit }: { rows: GiaoKhoanDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function GiaoKhoanTable({ rows, onDelete, onEdit, canEdit }: { rows: GiaoKhoanDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell
|
||||
headers={['Mã CV', 'Tên công việc', 'ĐVT', 'KL', 'Đơn giá', 'Thành tiền', 'Hoàn thành']}
|
||||
@ -179,14 +208,14 @@ function GiaoKhoanTable({ rows, onDelete, canEdit }: { rows: GiaoKhoanDetail[];
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGia)}</td>
|
||||
<td className="px-2 py-1.5 text-right font-medium">{fmtMoney(r.thanhTien)}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.thoiGianHoanThanh ? new Date(r.thoiGianHoanThanh).toLocaleDateString('vi-VN') : '—'}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function NhaCungCapTable({ rows, onDelete, canEdit }: { rows: NhaCungCapDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function NhaCungCapTable({ rows, onDelete, onEdit, canEdit }: { rows: NhaCungCapDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell
|
||||
headers={['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'Thành tiền', 'Giao hàng']}
|
||||
@ -203,14 +232,14 @@ function NhaCungCapTable({ rows, onDelete, canEdit }: { rows: NhaCungCapDetail[]
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGia)}</td>
|
||||
<td className="px-2 py-1.5 text-right font-medium">{fmtMoney(r.thanhTien)}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.thoiGianGiao ? new Date(r.thoiGianGiao).toLocaleDateString('vi-VN') : '—'}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function DichVuTable({ rows, onDelete, canEdit }: { rows: DichVuDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function DichVuTable({ rows, onDelete, onEdit, canEdit }: { rows: DichVuDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell
|
||||
headers={['Mã DV', 'Tên DV', 'ĐVT', 'Thời gian', 'Đơn giá', 'Thành tiền']}
|
||||
@ -226,14 +255,14 @@ function DichVuTable({ rows, onDelete, canEdit }: { rows: DichVuDetail[]; onDele
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.thoiGian)}</td>
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGia)}</td>
|
||||
<td className="px-2 py-1.5 text-right font-medium">{fmtMoney(r.thanhTien)}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function MuaBanTable({ rows, onDelete, canEdit }: { rows: MuaBanDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function MuaBanTable({ rows, onDelete, onEdit, canEdit }: { rows: MuaBanDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell
|
||||
headers={['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'VAT (%)', 'Thành tiền']}
|
||||
@ -250,14 +279,14 @@ function MuaBanTable({ rows, onDelete, canEdit }: { rows: MuaBanDetail[]; onDele
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGia)}</td>
|
||||
<td className="px-2 py-1.5 text-right text-xs">{r.thueVAT}%</td>
|
||||
<td className="px-2 py-1.5 text-right font-medium">{fmtMoney(r.thanhTien)}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function NguyenTacNccTable({ rows, onDelete, canEdit }: { rows: NguyenTacNccDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function NguyenTacNccTable({ rows, onDelete, onEdit, canEdit }: { rows: NguyenTacNccDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell headers={['Nhóm SP', 'Tên SP', 'ĐVT', 'Giá min', 'Giá max', 'Điều kiện thanh toán']}>
|
||||
{rows.length === 0 && <tr><td colSpan={8} className="px-3 py-6 text-center text-xs text-slate-400">Chưa có SP.</td></tr>}
|
||||
@ -270,14 +299,14 @@ function NguyenTacNccTable({ rows, onDelete, canEdit }: { rows: NguyenTacNccDeta
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGiaToiThieu)}</td>
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGiaToiDa)}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.dieuKienThanhToan ?? '—'}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
)
|
||||
}
|
||||
|
||||
function NguyenTacDvTable({ rows, onDelete, canEdit }: { rows: NguyenTacDvDetail[]; onDelete: (id: string) => void; canEdit: boolean }) {
|
||||
function NguyenTacDvTable({ rows, onDelete, onEdit, canEdit }: { rows: NguyenTacDvDetail[]; onDelete: (id: string) => void; onEdit?: (row: Record<string, unknown>) => void; canEdit: boolean }) {
|
||||
return (
|
||||
<TableShell headers={['Loại DV', 'Tên DV', 'ĐVT', 'Giá min', 'Giá max', 'SLA']}>
|
||||
{rows.length === 0 && <tr><td colSpan={8} className="px-3 py-6 text-center text-xs text-slate-400">Chưa có DV.</td></tr>}
|
||||
@ -290,7 +319,7 @@ function NguyenTacDvTable({ rows, onDelete, canEdit }: { rows: NguyenTacDvDetail
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGiaToiThieu)}</td>
|
||||
<td className="px-2 py-1.5 text-right">{fmtMoney(r.donGiaToiDa)}</td>
|
||||
<td className="px-2 py-1.5 text-xs text-slate-500">{r.sla ?? '—'}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <DeleteBtn onDelete={() => onDelete(r.id)} />}</td>
|
||||
<td className="px-2 py-1.5">{canEdit && <ActionBtns onEdit={onEdit ? () => onEdit(r as unknown as Record<string, unknown>) : undefined} onDelete={() => onDelete(r.id)} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
@ -569,3 +598,86 @@ function buildPayload(contractType: number, order: number, form: Record<string,
|
||||
}
|
||||
return common
|
||||
}
|
||||
|
||||
// ===== Edit dialog — populated từ row data, PUT thay POST =====
|
||||
|
||||
function EditRowDialog({
|
||||
target, contractId, contractType, onClose, onSaved,
|
||||
}: {
|
||||
target: { row: Record<string, unknown> } | null
|
||||
contractId: string
|
||||
contractType: number
|
||||
onClose: () => void
|
||||
onSaved: () => void
|
||||
}) {
|
||||
const [form, setForm] = useState<Record<string, string>>({})
|
||||
|
||||
// Populate khi target thay đổi (open dialog với row data)
|
||||
useEffect(() => {
|
||||
if (!target) return
|
||||
const init: Record<string, string> = {}
|
||||
for (const f of FIELDS_BY_TYPE[contractType] ?? []) {
|
||||
const v = target.row[f.name]
|
||||
if (v == null) init[f.name] = ''
|
||||
else if (f.type === 'date') {
|
||||
// ISO string → yyyy-MM-dd cho input[type=date]
|
||||
const d = new Date(String(v))
|
||||
init[f.name] = isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10)
|
||||
} else {
|
||||
init[f.name] = String(v)
|
||||
}
|
||||
}
|
||||
setForm(init)
|
||||
}, [target, contractType])
|
||||
|
||||
const submit = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!target) return
|
||||
const slug = TYPE_TO_SLUG[contractType]
|
||||
const detailId = target.row.id as string
|
||||
const order = Number(target.row.order ?? 1)
|
||||
const payload = buildPayload(contractType, order, form)
|
||||
// Patch id field cho BE PUT (URL có detailId nhưng body cũng cần)
|
||||
payload.id = detailId
|
||||
await api.put(`/contracts/${contractId}/details/${slug}/${detailId}`, payload)
|
||||
},
|
||||
onSuccess: () => { toast.success('Đã lưu'); onSaved() },
|
||||
onError: err => toast.error(getErrorMessage(err)),
|
||||
})
|
||||
|
||||
if (!target) return null
|
||||
const fields = FIELDS_BY_TYPE[contractType] ?? []
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!target}
|
||||
onClose={onClose}
|
||||
title="Sửa chi tiết"
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose}>Hủy</Button>
|
||||
<Button onClick={() => submit.mutate()} disabled={submit.isPending}>
|
||||
{submit.isPending ? 'Đang lưu…' : 'Lưu'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{fields.map(f => (
|
||||
<div key={f.name} className="space-y-1">
|
||||
<label className="text-[11px] font-medium text-slate-600">{f.label}</label>
|
||||
<Input
|
||||
type={f.type === 'number' ? 'number' : f.type === 'date' ? 'date' : 'text'}
|
||||
value={form[f.name] ?? ''}
|
||||
onChange={e => setForm(s => ({ ...s, [f.name]: e.target.value }))}
|
||||
placeholder={f.placeholder}
|
||||
step={f.type === 'number' ? 'any' : undefined}
|
||||
required={f.required}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user