All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m11s
Anh review truc tiep tren prod, khoanh do tung cho. Land tron cum:
=== 1. GOM NUT THAO TAC (anh: "Bo het di" + "phia duoi them cac nut thao tac"
+ "cho menu dang duyet -> Duyet/Tra lai vay thoi") ===
· THANH NUT DAY moi o `KhkkDetailContent` (sticky bottom-0, khuon PE workspace):
[Xoa phieu] · dong "Cac thay doi da tu dong luu" · [Luu & Gui duyet ->]
· Panel 3 GO "Gui duyet" + "Xoa phieu" => nay CHI con **Duyet / Tra lai**.
· GO nut "+ Lap ke hoach" (header) · nut "Sua" (muc 1) · dai banner "Dang xem ke hoach".
🔴 THU TU CO CHU DICH: KHONG go "Gui duyet" o panel 3 TRUOC khi thanh day co mat — go truoc
la nguoi soan mat duong trinh duyet, phieu ket o Nhap. Vi vay ca cum di CHUNG 1 commit.
🔴 Thanh day chi om nhanh **Submit** — nhanh Approve/Return (o-tich level-finalize + upload
file sau transition) O LAI panel 3. Nhan ban nhanh do sang day la de su-that thu hai cho
duong DUYET.
🔸 KHONG lam nut "Luu" roi: moi o o man nay luu NGAY khi roi o. Them nut khong lam gi la
hua hao — noi thang bang dong chu ben canh thay vi ve nut gia.
🔸 Giu lai dung 1 dong nho "Ve form tao" — bo han thi mo 1 phieu la ket, khong co duong lui.
· `onDeleted` noi vao ca 3 host (List / Create / Detail) de xoa xong khong dung o phieu ma.
=== 2. HANG MUC CUA DU AN (anh: "hang muc cua Du an -. Duyet NCC ay") ===
🔴 Lead bay NHAM o luot truoc: bay `line.tenHangMuc` = danh-muc **SP-002** (86 dong, dung
phan loai HOP DONG). Anh muon **hang muc CONG VIEC cua du an** tu phieu Duyet NCC
(`PE.WorkItemId`). Hai thu trung ten tieng Viet nhung KHAC TRUC — tron la chi sai goi thau.
· BE: `ContractSigningPlanDetailDto` +`PeWorkItemName`; handler join `WorkItems` (loose-Guid
Mig 49, KHONG FK vat ly => join tay null-safe).
· FE: o "Hang muc (du an)" doc `plan.peWorkItemName`; go het khoi liet-ke SP-002 khoi muc 1.
=== 3. MUC 2 RUT VE "HANG MUC + GIA -> TONG" ===
Anh: "Cho nay doi lai la hang muc la dc roi, chon hang muc tien -> thanh tong con lai bo het"
+ "cho chon nhieu dong nhap gia thanh gia tong, ko can so sanh j voi so duyet PRO hay CCM".
· Tieu de: "2. Gia de xuat theo NCC trung thau" -> "2. Hang muc".
· Cot: 6 -> 3 (HANG MUC | GIA | thao-tac). GO **Nha cung cap · Gia tham chieu · Gia duyet**.
· Ghi chu xuong DONG PHU duoi o hang muc (van nhap duoc, khong chiem cot).
· +hang TONG = Sigma gia cac dong.
🔸 Du-lieu 2 cot go di VAN trong DTO; `peReferenceAmount` con dung lam DAU phan biet
dong-goc / dong-them-tay o nut xoa.
=== 4. LOC THEO NHOM (anh: "Loc theo quy trinh luon") ===
Tang duoi man Thao tac hien phieu N1/N7 trong khi dang dung o leaf N2 => doc nham "nhom nay
dang co viec". Nay loc theo `approvalGroup` cua leaf menu. `group == null` => khong loc
(giu duong cu cho host khong truyen).
=== 5. GO DAI ACCENT + 2 O LINK HO SO/GHI CHU ===
· `card-accent` boc `KhkkDetailContent` GO HAN (dai xanh doc man Thao tac). GO CLASS chu
KHONG de `border-0` — class nay ngoai `@layer` nen thang utility (gotcha #66).
· Khoi "Link ho so + Ghi chu" GO TRON ca 2 nhanh ternary. Luot truoc lead moi go o luoi tren
nen 2 o van hien o khoi duoi — anh chup lai dung cho do.
=== CON NO ===
🔴 Cay man Thao tac: anh muon **Nam > Du an > Hang muc > 4 giai doan** (giong cay o man Danh
sach). Hien moi co Du an > Hang muc (gap/mo duoc). Chua land.
🔴 CHUA soi mat prod 375/1366/1440.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
378 lines
18 KiB
TypeScript
378 lines
18 KiB
TypeScript
// [W2 KHKK — S161 · dựng lại S171] Trang chi tiết "Kế hoạch ký kết HĐ" (GĐ2), route
|
|
// `/khkk/:id` GIỮ NGUYÊN (deep-link · bookmark · fallback `<lg` của màn 3-panel).
|
|
// File MIRROR SHA256 identical với fe-admin counterpart.
|
|
//
|
|
// [S171] Ruột trang nay là ĐÚNG 2 component dùng chung với host 3-panel `/khkk/list`:
|
|
// `components/khkk/KhkkDetailContent` (nội dung phiếu, 5 Section đánh số)
|
|
// `components/khkk/KhkkWorkflowPanel` (quy trình duyệt + lịch sử)
|
|
// xếp DỌC — khuôn `PurchaseEvaluationDetailPage` (`:798-805`). Không còn bản sao thứ hai
|
|
// của các Section ở đây: sửa 1 chỗ là cả 2 màn đổi theo.
|
|
import { useState } from 'react'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
|
import { ArrowLeft, FileCheck, FileSignature } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { PageHeader } from '@/components/ui/PageHeader'
|
|
import { Button } from '@/components/ui/Button'
|
|
import { Dialog } from '@/components/ui/Dialog'
|
|
import { Input } from '@/components/ui/Input'
|
|
import { Label } from '@/components/ui/Label'
|
|
import { Select } from '@/components/ui/Select'
|
|
import { KhkkDetailContent } from '@/components/khkk/KhkkDetailContent'
|
|
import { KhkkWorkflowPanel } from '@/components/khkk/KhkkWorkflowPanel'
|
|
import { useAuth } from '@/contexts/AuthContext'
|
|
import { usePermission } from '@/hooks/usePermission'
|
|
import { api } from '@/lib/api'
|
|
import { getErrorMessage } from '@/lib/apiError'
|
|
import { cn } from '@/lib/cn'
|
|
import { ContractTypeLabel } from '@/types/forms'
|
|
import {
|
|
KHKK_APPROVAL_GROUP_LABELS, KHKK_PHASE_BADGE, KHKK_PHASE_LABELS, KhkkPhase,
|
|
type CreateContractFromKhkkInput, type CreateContractFromKhkkResult,
|
|
type KhkkDetailDto, type KhkkLineDto, type KhkkPhaseValue,
|
|
} from '@/types/khkk'
|
|
|
|
function formatVnd(n: number | null): string {
|
|
if (n === null || n === undefined) return '—'
|
|
return n.toLocaleString('vi-VN') + ' đ'
|
|
}
|
|
|
|
export function KhkkDetailPage() {
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const qc = useQueryClient()
|
|
const { user } = useAuth()
|
|
const [searchParams] = useSearchParams()
|
|
const [bridgeOpen, setBridgeOpen] = useState(false)
|
|
|
|
// [M-10] Xóa/quay lại KHÔNG được rụng `?group=` — ném người dùng về "tất cả nhóm" là
|
|
// mất chỗ họ đang đứng. `?group=n` là ĐÚNG URL leaf của nhóm n nên sidebar vẫn sáng đúng.
|
|
const groupParam = searchParams.get('group')
|
|
const group = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
|
|
const backToList = group !== null ? `/khkk/list?group=${group}` : '/khkk/list'
|
|
|
|
// [K7 S167 — vá-1 vế FE] HỘI 2 KHÓA, đúng bằng server, KHÔNG lỏng hơn:
|
|
// · `KeHoachKyKet.Read` — cửa đọc chính phiếu này (`ContractSigningPlansController.cs:36`,
|
|
// `MenuPermissionHandler.cs:40` so khớp CHÍNH XÁC 1 key ⇒ KHÔNG OR `Khkk_*`, gotcha #85);
|
|
// · `Contracts.Create` — cửa của endpoint bắc cầu: tài nguyên SINH RA là Contract nên key
|
|
// phải là của endpoint đích, không phải của màn đang đứng.
|
|
const { can } = usePermission()
|
|
const canReadPlan = can('KeHoachKyKet', 'Read')
|
|
const canCreateContract = can('Contracts', 'Create')
|
|
|
|
const plan = useQuery({
|
|
queryKey: ['khkk-detail', id],
|
|
queryFn: async () => (await api.get<KhkkDetailDto>(`/contract-signing-plans/${id}`)).data,
|
|
enabled: !!id,
|
|
})
|
|
|
|
const invalidate = () => {
|
|
qc.invalidateQueries({ queryKey: ['khkk-detail', id] })
|
|
qc.invalidateQueries({ queryKey: ['khkk-list'] })
|
|
}
|
|
|
|
if (plan.isLoading) {
|
|
return (
|
|
<div className="space-y-5">
|
|
<PageHeader eyebrow="Giai đoạn 2" title="Đang tải..." icon={<FileCheck className="h-5 w-5" />} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (plan.isError || !plan.data) {
|
|
return (
|
|
<div className="space-y-5">
|
|
<PageHeader
|
|
eyebrow="Giai đoạn 2"
|
|
title="Lỗi"
|
|
icon={<FileCheck className="h-5 w-5" />}
|
|
actions={
|
|
<Button variant="outline" onClick={() => navigate(backToList)}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Quay lại
|
|
</Button>
|
|
}
|
|
/>
|
|
<div className="rounded-lg border bg-red-50 p-4 text-sm text-red-800">
|
|
Không tải được dữ liệu kế hoạch ký kết.
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const k = plan.data
|
|
const phase = k.phase as KhkkPhaseValue
|
|
// [P2-7] `readOnly` là quyết-định của HOST (trang này cũng là một host).
|
|
const readOnly =
|
|
!(phase === KhkkPhase.DangSoanThao || phase === KhkkPhase.TraLai)
|
|
|| (!!user?.id && k.drafterUserId !== user.id)
|
|
|
|
// [K7 S167] Dòng bắc cầu được = CHƯA có HĐ ∧ ĐÃ có giá duyệt chốt — đúng 2 guard BE.
|
|
const bridgeableLines = k.lines.filter(l => l.contractId == null && l.approvedAmount != null)
|
|
const showBridge = phase === KhkkPhase.DaDuyet && canReadPlan
|
|
// Thiếu vế nào thì NÓI RA vế đó thay vì ẩn nút (khuôn nút-khoá-kèm-title PE `:547-561`).
|
|
const bridgeBlockReason = !canCreateContract
|
|
? 'Bạn chưa có quyền "Tạo hợp đồng" (Contracts.Create) — nhờ quản trị cấp quyền rồi thử lại.'
|
|
: k.lines.length === 0
|
|
? 'Phiếu chưa có dòng nhà cung cấp nào.'
|
|
: bridgeableLines.length === 0
|
|
? (k.lines.every((l) => l.contractId != null)
|
|
? 'Mọi dòng của phiếu đã được đưa vào hợp đồng.'
|
|
: 'Chưa dòng nào có giá duyệt chốt — phiếu phải qua bước chốt giá trước khi đưa vào HĐ.')
|
|
: null
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<PageHeader
|
|
eyebrow={k.maKeHoach ?? 'Kế hoạch ký kết'}
|
|
title={k.peTenGoiThau ?? 'Kế hoạch ký kết HĐ'}
|
|
icon={<FileCheck className="h-5 w-5" />}
|
|
accent="brand"
|
|
actions={
|
|
<>
|
|
{k.approvalGroup != null && (
|
|
<span
|
|
className="inline-flex items-center rounded-md border border-violet-300 bg-violet-100 px-2.5 py-1 text-xs font-semibold text-violet-800"
|
|
title={KHKK_APPROVAL_GROUP_LABELS[k.approvalGroup] ?? `Nhóm ${k.approvalGroup}`}
|
|
>
|
|
N{k.approvalGroup}
|
|
</span>
|
|
)}
|
|
<span className={cn('inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-medium', KHKK_PHASE_BADGE[phase])}>
|
|
{KHKK_PHASE_LABELS[phase]}
|
|
</span>
|
|
{/* `<span title>` bọc ngoài vì `Button` có `disabled:pointer-events-none`
|
|
(Button.tsx:12) ⇒ title đặt thẳng trên nút đã khoá sẽ không hiện. */}
|
|
{showBridge && (
|
|
<span title={bridgeBlockReason ?? 'Tạo hợp đồng từ một dòng của kế hoạch này'}>
|
|
<Button variant="outline" disabled={bridgeBlockReason != null} onClick={() => setBridgeOpen(true)}>
|
|
<FileSignature className="mr-2 h-4 w-4" />
|
|
Đưa vào HĐ
|
|
</Button>
|
|
</span>
|
|
)}
|
|
<Button variant="outline" onClick={() => navigate(backToList)}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Danh sách
|
|
</Button>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
{/* [S174 owner] Khối "Quy trình duyệt" GỠ khỏi bản DESKTOP của trang này — anh chốt
|
|
"bỏ hết, nó sẽ hiển thị ở Menu duyệt". Nhà của nó ở desktop là Panel-3 màn danh sách
|
|
(`KhkkListPage.tsx:196`), nên gỡ = bỏ BẢN SAO THỨ HAI, không phải bỏ tính năng.
|
|
🔴 [A3] NHƯNG `<lg` KHÔNG có 3 panel: `selectPlan` điều hướng thẳng vào trang này
|
|
(`KhkkListPage.tsx:107`) ⇒ gỡ trắng thì mobile MẤT HẲN đường Gửi duyệt / Xóa phiếu.
|
|
⇒ giữ lại ĐÚNG ở `lg:hidden`: desktop vẫn sạch (panel-3 lo), mobile vẫn thao tác được.
|
|
Đây là nợ lead đã khai với owner ở vòng trước, nay trả. */}
|
|
<div className="lg:hidden">
|
|
<KhkkWorkflowPanel plan={k} onChanged={invalidate} onDeleted={() => navigate(backToList)} />
|
|
</div>
|
|
|
|
<KhkkDetailContent plan={k} readOnly={readOnly} onChanged={invalidate} onDeleted={() => navigate("/khkk/list")} />
|
|
|
|
{bridgeOpen && <CreateContractFromKhkkDialog plan={k} onClose={() => setBridgeOpen(false)} />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ===================== K7 S167 — cầu KHKK → Hợp đồng =================================
|
|
// [vá-16] 0 KHUÔN MỚI — ghép 2 khuôn đang sống:
|
|
// · vỏ dialog + khối "nói thẳng hệ quả" = nút Xóa-phiếu S155 (`PeWorkflowPanel.tsx:879-918`);
|
|
// · ô chọn Quy trình duyệt = picker `ContractCreatePage.tsx:333-343`.
|
|
// [OG-1 "máy rộng, UI hẹp"] Máy BE nhận `lineIds[]` (gộp được N dòng vào 1 HĐ); ô chọn ở
|
|
// đây CỐ Ý 1-1 — mỗi lần bấm đúng 1 dòng. Mở gộp sau = đổi radio thành checkbox, KHÔNG
|
|
// đụng hợp-đồng API.
|
|
type BridgeWorkflowOption = {
|
|
id: string
|
|
code: string
|
|
version: number
|
|
name: string
|
|
isActive: boolean
|
|
isUserSelectable: boolean
|
|
}
|
|
|
|
function CreateContractFromKhkkDialog({ plan, onClose }: { plan: KhkkDetailDto; onClose: () => void }) {
|
|
const navigate = useNavigate()
|
|
const qc = useQueryClient()
|
|
|
|
const bridgeable = plan.lines.filter((l) => l.contractId == null && l.approvedAmount != null)
|
|
const [lineId, setLineId] = useState(() => bridgeable[0]?.id ?? '')
|
|
const [contractType, setContractType] = useState(1)
|
|
const [workflowId, setWorkflowId] = useState('')
|
|
// null = "đang dùng tên mặc định theo dòng đang chọn". Giữ null thay vì đồng bộ bằng
|
|
// useEffect: đổi dòng thì tên gợi ý đổi theo NGAY trong render, không có nhịp lệch.
|
|
const [tenHopDongOverride, setTenHopDongOverride] = useState<string | null>(null)
|
|
|
|
const selected: KhkkLineDto | null = plan.lines.find((l) => l.id === lineId) ?? null
|
|
const defaultName = selected
|
|
? `${plan.peTenGoiThau ?? 'Kế hoạch ký kết'} — ${selected.tenHangMuc ?? selected.supplierName ?? ''}`.trim()
|
|
: ''
|
|
const tenHopDong = tenHopDongOverride ?? defaultName
|
|
|
|
const workflows = useQuery({
|
|
// Query key RIÊNG (không dùng lại `approval-workflows-v2-contract`): cùng URL nhưng LỌC
|
|
// KHÁC ⇒ dùng chung ô cache là để trang này ăn dữ liệu trang kia một cách câm (bài S159).
|
|
queryKey: ['khkk-bridge-contract-workflows'],
|
|
queryFn: async () => {
|
|
const res = await api.get<{ types: { applicableType: number; history: BridgeWorkflowOption[] }[] }>(
|
|
'/approval-workflows-v2',
|
|
{ params: { applicableType: 3 } },
|
|
)
|
|
const bucket = res.data.types.find((t) => t.applicableType === 3)
|
|
// 🔴 LỌC ĐỦ 2 CỜ: `isUserSelectable` (admin ghim cho user chọn) ∧ `isActive` (bản còn
|
|
// hiệu lực). Picker cũ chỉ lọc 1 cờ ⇒ mời chọn bản đã retire rồi ăn 409 từ guard BE.
|
|
return (bucket?.history ?? []).filter((w) => w.isUserSelectable && w.isActive)
|
|
},
|
|
})
|
|
|
|
const create = useMutation({
|
|
mutationFn: async () => {
|
|
const body: CreateContractFromKhkkInput = {
|
|
lineIds: [lineId],
|
|
contractType,
|
|
approvalWorkflowId: workflowId,
|
|
tenHopDong: tenHopDong.trim() || null,
|
|
}
|
|
return (await api.post<CreateContractFromKhkkResult>(
|
|
`/contract-signing-plans/${plan.id}/create-contract`, body,
|
|
)).data
|
|
},
|
|
onSuccess: (data) => {
|
|
toast.success(data.maHopDong ? `Đã tạo hợp đồng ${data.maHopDong}` : 'Đã tạo hợp đồng từ kế hoạch')
|
|
qc.invalidateQueries({ queryKey: ['khkk-detail', plan.id] })
|
|
qc.invalidateQueries({ queryKey: ['khkk-list'] })
|
|
// Cây 4 folder đọc 2 index riêng với `staleTime` 60s — không gọi 2 dòng này thì HĐ vừa
|
|
// tạo cả phút sau mới hiện trên cây.
|
|
qc.invalidateQueries({ queryKey: ['pipeline-khkk-index'] })
|
|
qc.invalidateQueries({ queryKey: ['pipeline-contract-index'] })
|
|
onClose()
|
|
navigate(`/contracts/${data.contractId}`)
|
|
},
|
|
onError: (e) => toast.error(getErrorMessage(e)),
|
|
})
|
|
|
|
const noWorkflow = !workflows.isLoading && (workflows.data?.length ?? 0) === 0
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
size="lg"
|
|
onClose={onClose}
|
|
title="Đưa dòng kế hoạch vào Hợp đồng"
|
|
footer={
|
|
<>
|
|
<Button variant="ghost" onClick={onClose}>Huỷ</Button>
|
|
<Button onClick={() => create.mutate()} disabled={create.isPending || !lineId || !workflowId}>
|
|
{create.isPending ? 'Đang tạo...' : 'Tạo hợp đồng'}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-900">
|
|
<div className="font-semibold">Tạo hợp đồng từ kế hoạch “{plan.maKeHoach ?? 'chưa cấp mã'}”</div>
|
|
<ul className="mt-1.5 list-disc space-y-1 pl-4 text-[11px]">
|
|
<li>Mỗi lần bấm tạo <strong>1 hợp đồng cho 1 dòng</strong> (1 nhà cung cấp). Liên danh nhiều NCC thì làm lần lượt.</li>
|
|
<li>Giá trị HĐ lấy đúng <strong>giá duyệt đã chốt</strong> của dòng — không lấy giá đề xuất.</li>
|
|
<li>HĐ mới ở trạng thái <strong>Nháp</strong> và đi theo quy trình duyệt bạn chọn dưới đây.</li>
|
|
<li>Dòng đã đưa vào HĐ thì <strong>không đưa lần hai</strong> — cần sửa thì xử lý ở chính hợp đồng đó.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label>Chọn dòng đưa vào hợp đồng</Label>
|
|
{plan.lines.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground">Phiếu chưa có dòng nhà cung cấp nào.</div>
|
|
) : (
|
|
<ul className="mt-1 space-y-1.5">
|
|
{plan.lines.map((l) => {
|
|
const done = l.contractId != null
|
|
const noPrice = l.approvedAmount == null
|
|
const disabled = done || noPrice
|
|
return (
|
|
<li key={l.id}>
|
|
<label
|
|
className={cn(
|
|
'flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-sm',
|
|
disabled && 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400',
|
|
!disabled && lineId === l.id && 'border-brand-400 bg-brand-50',
|
|
!disabled && lineId !== l.id && 'border-slate-200 hover:bg-slate-50',
|
|
)}
|
|
title={
|
|
done ? 'Dòng này đã có hợp đồng.'
|
|
: noPrice ? 'Dòng chưa có giá duyệt chốt — phiếu phải qua bước chốt giá.'
|
|
: undefined
|
|
}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="khkk-bridge-line"
|
|
checked={lineId === l.id}
|
|
disabled={disabled}
|
|
onChange={() => { setLineId(l.id); setTenHopDongOverride(null) }}
|
|
/>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="font-medium">{l.tenHangMuc ?? '(chưa gán hạng mục)'}</span>
|
|
<span className="text-slate-500"> · {l.supplierName ?? '—'}</span>
|
|
</span>
|
|
<span className="shrink-0 tabular-nums">{formatVnd(l.approvedAmount)}</span>
|
|
{done && (
|
|
<a href={`/contracts/${l.contractId}`} className="shrink-0 text-[11px] font-medium text-brand-600 hover:underline">
|
|
Đã có HĐ
|
|
</a>
|
|
)}
|
|
{!done && noPrice && <span className="shrink-0 text-[11px]">Chưa có giá chốt</span>}
|
|
</label>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="bridge-type">Loại hợp đồng</Label>
|
|
<Select id="bridge-type" value={contractType} onChange={(e) => setContractType(Number(e.target.value))}>
|
|
{Object.entries(ContractTypeLabel).map(([v, label]) => (
|
|
<option key={v} value={v}>{label}</option>
|
|
))}
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="bridge-workflow">Quy trình duyệt hợp đồng</Label>
|
|
<Select
|
|
id="bridge-workflow"
|
|
value={workflowId}
|
|
onChange={(e) => setWorkflowId(e.target.value)}
|
|
disabled={workflows.isLoading || noWorkflow}
|
|
>
|
|
<option value="">— Chọn quy trình —</option>
|
|
{(workflows.data ?? []).map((w) => (
|
|
<option key={w.id} value={w.id}>{w.code} v{w.version} — {w.name}</option>
|
|
))}
|
|
</Select>
|
|
{noWorkflow && (
|
|
// Rỗng-vì-chưa-cấu-hình phải khác câu với rỗng-vì-đang-tải: người dùng cần biết
|
|
// đây là việc của quản trị, không phải chờ thêm.
|
|
<p className="mt-1 text-[11px] text-red-700">
|
|
Chưa có quy trình duyệt hợp đồng nào được ghim cho người dùng chọn (cần đang hiệu lực
|
|
và được ghim). Nhờ quản trị mở trong màn Quy trình duyệt rồi quay lại.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="bridge-name">Tên hợp đồng</Label>
|
|
<Input id="bridge-name" value={tenHopDong} onChange={(e) => setTenHopDongOverride(e.target.value)} maxLength={500} />
|
|
<p className="mt-1 text-[11px] text-slate-500">
|
|
Bỏ trống thì hệ thống tự đặt theo tên gói thầu và hạng mục của dòng.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
)
|
|
}
|