[CLAUDE] App+Api+FE: Kế thừa HĐ từ phiếu Duyệt NCC (Phase 4)
BE:
- CreateContractFromEvaluationCommand: guard DaDuyet + SelectedSupplier
+ ContractId=null → tạo Contract draft mới với SupplierId/ProjectId/
DepartmentId kế thừa từ PE. GiaTri = sum(details.thanhTienNganSach).
DraftData = PE.PaymentTerms. Gen MaHopDong ngay + pin WorkflowDefinitionId
theo ContractType user chọn. Log Changelog cả 2 bảng (Contract +
PurchaseEvaluation), link 2 chiều PE.ContractId = contract.Id.
- ListApprovedPurchaseEvaluationsQuery: DaDuyet + ContractId=null cho
FE picker.
- 2 endpoint mới:
GET /api/purchase-evaluations/approved-pending-contract
POST /api/purchase-evaluations/{id}/create-contract
FE:
- PeDetailTabs InfoTab: nếu Phase=DaDuyet && !ContractId && SelectedSupplierId
→ banner emerald + button "Tạo HĐ từ phiếu" → CreateContractDialog
(pick ContractType dropdown 7 loại + TenHopDong + bypass CCM flag)
- Sau khi tạo → navigate /contracts/{newId}
- Mirror fe-user.
KHÔNG auto-map PE Details → Contract Details per-type (PE schema ≠ 7
ContractType details schemas — user điền lại sau). PE → Contract link
qua FK ContractId cho navigation + history.
This commit is contained in:
@ -121,18 +121,97 @@ export function PeDetailTabs({
|
||||
|
||||
// ===== Tab: Thông tin =====
|
||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
return (
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
||||
<Field label="Dự án" value={ev.projectName} />
|
||||
<Field label="Địa điểm" value={ev.diaDiem ?? '—'} />
|
||||
<Field label="Mô tả" value={ev.moTa ?? '—'} />
|
||||
<Field label="NCC được chọn" value={ev.selectedSupplierName ?? '—'} />
|
||||
<Field label="Điều khoản thanh toán" value={ev.paymentTerms ?? '—'} />
|
||||
{ev.contractId && (
|
||||
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
||||
<div className="space-y-4">
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
||||
<Field label="Dự án" value={ev.projectName} />
|
||||
<Field label="Địa điểm" value={ev.diaDiem ?? '—'} />
|
||||
<Field label="Mô tả" value={ev.moTa ?? '—'} />
|
||||
<Field label="NCC được chọn" value={ev.selectedSupplierName ?? '—'} />
|
||||
<Field label="Điều khoản thanh toán" value={ev.paymentTerms ?? '—'} />
|
||||
{ev.contractId && (
|
||||
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
||||
)}
|
||||
</dl>
|
||||
{canCreateContract && (
|
||||
<div className="rounded border border-emerald-200 bg-emerald-50 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm text-emerald-800">
|
||||
✓ Phiếu đã duyệt. Bấm để tạo HĐ mới kế thừa NCC + hạng mục.
|
||||
</div>
|
||||
<Button onClick={() => setCreateOpen(true)} className="gap-1.5 text-xs">
|
||||
<Plus className="h-3.5 w-3.5" /> Tạo HĐ từ phiếu
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
{createOpen && <CreateContractDialog evaluation={ev} onClose={() => setCreateOpen(false)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBundle; onClose: () => void }) {
|
||||
const navigate = useNavigate()
|
||||
const [form, setForm] = useState({
|
||||
contractType: 1,
|
||||
tenHopDong: evaluation.tenGoiThau,
|
||||
bypassProcurementAndCCM: false,
|
||||
})
|
||||
const mut = useMutation({
|
||||
mutationFn: async () =>
|
||||
api.post<{ contractId: string }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form),
|
||||
onSuccess: res => {
|
||||
toast.success('Đã tạo HĐ từ phiếu.')
|
||||
navigate(`/contracts/${res.data.contractId}`)
|
||||
},
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
const typeOptions = [
|
||||
[1, 'HĐ Thầu phụ'],
|
||||
[2, 'HĐ Giao khoán'],
|
||||
[3, 'HĐ Nhà cung cấp'],
|
||||
[4, 'HĐ Dịch vụ'],
|
||||
[5, 'HĐ Mua bán'],
|
||||
[6, 'HĐ Nguyên tắc NCC'],
|
||||
[7, 'HĐ Nguyên tắc DV'],
|
||||
] as const
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onClose={onClose}
|
||||
title="Tạo HĐ từ phiếu Duyệt NCC"
|
||||
footer={<>
|
||||
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
||||
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Tạo</Button>
|
||||
</>}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-500">
|
||||
NCC: <strong>{evaluation.selectedSupplierName}</strong> · Dự án: {evaluation.projectName}
|
||||
</p>
|
||||
<div>
|
||||
<Label>Loại HĐ</Label>
|
||||
<Select value={form.contractType} onChange={e => setForm({ ...form, contractType: Number(e.target.value) })}>
|
||||
{typeOptions.map(([v, lbl]) => <option key={v} value={v}>{lbl}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tên HĐ</Label>
|
||||
<Input value={form.tenHopDong} onChange={e => setForm({ ...form, tenHopDong: e.target.value })} />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.bypassProcurementAndCCM}
|
||||
onChange={e => setForm({ ...form, bypassProcurementAndCCM: e.target.checked })}
|
||||
/>
|
||||
Bypass CCM (áp dụng HĐ với Chủ đầu tư)
|
||||
</label>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user