[CLAUDE] FE-User: YC-030 Q1/Q2/Q3 — my-contracts layout KHKK-thuần (ContractListPanel) + panel duyệt 3 món + máy duyệt về panel-3 (5 host ×2 app)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -12,9 +12,8 @@ import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
import { SlaTimer } from '@/components/SlaTimer'
|
||||
import { ContractAttachmentsSection } from '@/components/ContractAttachmentsSection'
|
||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||
// [W6 S187] Màn duyệt V2-trạm — CHỈ dùng cho HĐ đã pin `approvalWorkflowId` (xem khối
|
||||
// dual-render ở dưới). HĐ V1 legacy không import nhánh này lúc chạy.
|
||||
import { ContractWorkflowPanel } from '@/components/contracts/ContractWorkflowPanel'
|
||||
// [WQ3b S190 · YC-030 Q5] 🧊 Import `ContractWorkflowPanel` (W6/S187) ĐÃ GỠ khỏi file này —
|
||||
// máy duyệt dời sang PANEL-3 của từng host, xem khối lineage ở `:isV2`.
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
@ -133,13 +132,23 @@ export function ContractDetailContent({
|
||||
// duyệt (`ContractWorkflowService.cs:111` `if (contract.ApprovalWorkflowId is Guid awId)`)
|
||||
// VÀ cùng cờ mục "Ý kiến cấp duyệt (Quy trình V2)" bên dưới đã dùng từ S29. Một cờ, ba nơi
|
||||
// đọc, không đẻ khái niệm "HĐ mới" thứ hai.
|
||||
// • V2 ⇒ `ContractWorkflowPanel` (sơ đồ Bước→Cấp + banner lượt-ai + 2 nút Duyệt/Trả lại,
|
||||
// có gating theo Cấp đang chờ).
|
||||
// • V2 ⇒ máy duyệt V2-trạm (sơ đồ Bước→Cấp + banner lượt-ai + 2 nút Duyệt/Trả lại, có
|
||||
// gating theo Cấp đang chờ) — 🔴 [WQ3b S190 · YC-030 Q5] KHÔNG CÒN render trong file này.
|
||||
// • V1 legacy (không pin — 7 HĐ prod chạy nhánh này, `ContractWorkflowService.cs:110`)
|
||||
// ⇒ GIỮ NGUYÊN 2 nút "Yêu cầu sửa"/"Duyệt → tiếp" ở header + Dialog chọn phase.
|
||||
// 2 khối là THAY THẾ nhau, không chồng: để cả hai thì cùng một HĐ V2 có 2 đường bấm duyệt
|
||||
// (đường header không nói được đang ở Bước/Cấp nào, cũng không gating theo người) — mâu
|
||||
// thuẫn hiển thị chứ không phải tiện thêm.
|
||||
// thuẫn hiển thị chứ không phải tiện thêm. `isV2` Ở LẠI vì nửa V1 (`!isV2` ở header) vẫn đọc.
|
||||
//
|
||||
// ===== [WQ3b S190 · YC-030 Q5] MÁY DUYỆT DỜI VỀ PANEL-3 (khuôn KHKK) ====================
|
||||
// 🧊 W6/S187 đặt `ContractWorkflowPanel` ngay TRONG file này (nhánh `isV2`, chỗ khối JSX đã
|
||||
// gỡ bên dưới) = panel 2 của các host 3-panel. FLAG WQ1 (`MyContractsPage.tsx:18-23`) chỉ ra
|
||||
// hệ quả: panel 3 muốn mang máy duyệt như `KhkkListPage` thì MỘT HĐ có HAI bộ nút Duyệt/Trả
|
||||
// lại — đúng thứ chính khối trên cấm. Owner chốt YC-030 Q5 "cấu trúc duyệt như KHKK" ⇒ máy
|
||||
// duyệt ra khỏi thân chi tiết, mỗi HOST tự render ở panel cạnh (`MyContractsPage` ·
|
||||
// `ContractDetailPage` ×2 · `ContractsListPage` fe-admin · `InboxPage`), CÙNG điều kiện
|
||||
// `approvalWorkflowId != null` đang tính ở dưới. Host nào quên render thì HĐ V2 ở host đó
|
||||
// KHÔNG có đường duyệt — đây là chỗ dễ hụt nhất của lượt dời này, đã liệt đủ 5 host.
|
||||
const isV2 = c.approvalWorkflowId != null
|
||||
|
||||
// ===== [W5 S187 · YC-023 GĐ3] NGUỒN GỐC KHKK (GĐ2) =====================================
|
||||
@ -187,8 +196,9 @@ export function ContractDetailContent({
|
||||
<PhaseBadge phase={c.phase} />
|
||||
</div>
|
||||
</div>
|
||||
{/* [W6 S187] `!isV2` = nửa V1 của dual-render. HĐ pin quy trình V2 thao tác duyệt ở
|
||||
`ContractWorkflowPanel` bên dưới (đúng Bước/Cấp + đúng người), không ở đây. */}
|
||||
{/* [W6 S187 · sửa WQ3b S190] `!isV2` = nửa V1 của dual-render. HĐ pin quy trình V2
|
||||
thao tác duyệt ở máy duyệt V2-trạm tại PANEL-3 của host (đúng Bước/Cấp + đúng
|
||||
người), không ở header này. */}
|
||||
{!isV2 && availableTargets.length > 0 && (
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button variant="outline" onClick={() => openAction(ApprovalDecision.Reject)}>
|
||||
@ -321,18 +331,10 @@ export function ContractDetailContent({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* [W6 S187 · YC-023 GĐ3] Màn duyệt V2-trạm — nửa V2 của dual-render.
|
||||
🔴 ĐỨNG CAO, ngay dưới khối nhận-dạng HĐ: 2 nút duyệt của nhánh V1 vốn nằm ở HEADER
|
||||
dính (`sticky top-0`), nên nếu nhánh V2 đẩy thao tác duyệt xuống tận cuối trang thì
|
||||
đúng người-đang-chờ-duyệt lại phải cuộn đi tìm — mất chỗ đứng cũ chứ không phải sắp
|
||||
xếp lại. Cùng lý do khuôn nguồn KHKK để panel ở cột-3 luôn nhìn thấy.
|
||||
Vỏ `<section className="rounded-lg border … p-5">` bám đúng khuôn các khối cùng trang
|
||||
(2 app đều KHÔNG có shadcn `Card`). */}
|
||||
{isV2 && (
|
||||
<section className="rounded-lg border border-brand-200 bg-white p-5">
|
||||
<ContractWorkflowPanel contract={c} />
|
||||
</section>
|
||||
)}
|
||||
{/* [WQ3b S190 · YC-030 Q5] Ở ĐÂY TRƯỚC LÀ máy duyệt V2-trạm (W6/S187) — đã dời sang
|
||||
panel-3 của từng host theo khuôn KHKK; lý do + danh sách host xem khối `isV2` ở trên.
|
||||
Chỗ trống này CỐ Ý không lấp bằng khối khác: thứ tự đọc còn lại (thông tin → nguồn
|
||||
KHKK → góp ý → file → chi tiết) là thứ tự cũ, không xáo thêm trong cùng một lượt. */}
|
||||
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
|
||||
@ -23,20 +23,26 @@
|
||||
// phân 2 ngả bằng ĐÚNG `targetPhase`: `== TuChoi` ⇒ TuChoi, else ⇒ TraLai. Gửi 98 tường
|
||||
// minh thay vì "số nào cũng được miễn khác 99".
|
||||
//
|
||||
// ══ 3 MẢNH CỦA KHUÔN KHKK CỐ Ý KHÔNG PORT (đo ra, không phải cắt cho gọn) ═════════════
|
||||
// 1. ❌ Ô-tích "Cấp này KẾT THÚC quy trình": `TransitionContractBody` có ĐÚNG 3 field;
|
||||
// `skipToFinal` là tham số C# default (`TransitionAsync(..., bool skipToFinal=false)`)
|
||||
// KHÔNG nằm trên API surface ⇒ vẽ ô-tích = control chết, tích xong không đổi gì.
|
||||
// 2. ❌ Badge "⚑ Duyệt thay CEO" cho Cấp có `allowApproverFinalize`: `ApproveV2Async` của HĐ
|
||||
// KHÔNG ĐỌC cờ đó lần nào (`ContractWorkflowService.cs:230-407` — cờ duy nhất nó đọc là
|
||||
// `AllowApproverSkipToFinal` `:335`, cho đường skip mà FE không gọi được). DTO vẫn trả
|
||||
// field (`ContractDtos.cs:138`) và chú thích W2 ghi rõ "dùng hay không là quyết của W6".
|
||||
// Vẽ badge nói "duyệt ở đây là KẾT THÚC" = NÓI DỐI: duyệt xong vẫn chạy tiếp Cấp sau.
|
||||
// 3. ❌ Banner kết-thúc-sớm + meta `{code} v{NN} · {name}`: `ContractDetail` 0 field
|
||||
// `endedByLevelFinalize`/`workflowCode`/`workflowVersion`/`workflowName`. Mượn
|
||||
// `c.workflow.policyName` là ghép nhãn của workflow-policy V1 vào quy trình V2 — hai
|
||||
// vật khác nhau. Cây thô đã INLINE trong detail ⇒ cũng KHÔNG gọi thêm
|
||||
// `/approval-workflows-v2/{id}` ("KHÔNG đẻ đường thứ 3" — `ContractDtos.cs:86-88`).
|
||||
// ══ [WQ3b S190 · YC-030 Q3] 3 MẢNH KHUÔN KHKK — TỪ CHỐI @S187, PORT ĐỦ ĐỢT NÀY ════════
|
||||
// 🧊 Bản S187 liệt 3 mảnh này là "CỐ Ý KHÔNG PORT" và lý do khi đó ĐÚNG: cả ba đều thiếu
|
||||
// máy ở BE. WQ3-BE (cùng phiên S190) dựng xong máy ⇒ 3 lý do đó HẾT HIỆU LỰC, không phải
|
||||
// bị bỏ qua. Ghi lại lineage để người sau đọc panel này không tưởng ai đó lờ mất chú thích:
|
||||
// 1. ✅ Ô-tích "Cấp này KẾT THÚC quy trình" — `TransitionContractBody` nay có
|
||||
// `ApplyLevelFinalize`/`SkipToFinal` (`ContractsController.cs:269-274`), `?? true` ở
|
||||
// `:71`. Trước đây vẽ ô-tích là control CHẾT; nay tích/bỏ-tích đổi thật đường đi.
|
||||
// 🔴 Ô-tích là OPT-OUT: BE cần CẢ HAI vế `AllowApproverFinalize ∧ applyLevelFinalize`
|
||||
// (`ContractWorkflowService.cs:527`), mà bỏ trống ⇒ `true`. Vì vậy panel gửi giá trị
|
||||
// TƯỜNG MINH mỗi lần duyệt, không dựa vào mặc định của server.
|
||||
// 2. ✅ Badge cho Cấp có `allowApproverFinalize` — `ApproveV2Async` NAY ĐỌC cờ đó (`:527`)
|
||||
// ⇒ câu "duyệt ở đây là KẾT THÚC" không còn nói dối. CHỮ trên badge KHÔNG chép nguyên
|
||||
// KHKK ("⚑ Duyệt thay CEO"): quy trình HĐ không có trạm CEO nào để "thay" — terminal
|
||||
// của HĐ là `DaPhatHanh` + gen mã RG-001 (`:392`). Dùng chữ đúng vật: "⚑ Cấp kết thúc".
|
||||
// 3. ✅ Banner kết-thúc-sớm + meta `{code} v{NN}` — `ContractDetail` nay có
|
||||
// `workflowCode`/`workflowVersion`/`endedByLevelFinalize` (`ContractDtos.cs:103-105`),
|
||||
// không phải mượn `c.workflow.policyName` của V1 nữa. ⚠️ VẪN THIẾU so với KHKK:
|
||||
// `workflowName` + `finalizeStepName`/`finalizeLevelName` — BE HĐ không trả 3 field đó
|
||||
// ⇒ meta chỉ `{code} v{NN}`, còn tên Bước/Cấp kết-thúc panel TỰ SUY từ ý-kiến ký gần
|
||||
// nhất (xem chỗ dựng `finalizeMark`), KHÔNG bịa field và KHÔNG gọi thêm endpoint thứ 3.
|
||||
//
|
||||
// ══ 2 KHỐI BỎ VÌ TRÙNG CHỖ ĐÃ CÓ TRÊN CÙNG TRANG ════════════════════════════════════
|
||||
// Cả 4 host (`ContractDetailPage` ×2 app · `MyContractsPage` · `ContractsListPage` ·
|
||||
@ -62,6 +68,7 @@ import {
|
||||
type ContractDetail,
|
||||
type ContractLevelOpinion,
|
||||
type ContractWorkflowLevel,
|
||||
type TransitionContractBody,
|
||||
} from '@/types/contracts'
|
||||
|
||||
type NodeStatus = 'Done' | 'Current' | 'Pending'
|
||||
@ -102,6 +109,10 @@ export function ContractWorkflowPanel({
|
||||
const { user } = useAuth()
|
||||
const [action, setAction] = useState<PanelAction | null>(null)
|
||||
const [comment, setComment] = useState('')
|
||||
// [WQ3b S190] Mặc định KHÔNG tick — khuôn KHKK (`KhkkWorkflowPanel.tsx:103`, vốn theo
|
||||
// `PeWorkflowPanel.tsx:61`). Người duyệt phải chủ động chọn "dừng ở đây"; mặc-định-tick là
|
||||
// đổi hành vi im lặng của mọi Cấp được uỷ quyền.
|
||||
const [applyLevelFinalize, setApplyLevelFinalize] = useState(false)
|
||||
|
||||
const steps = c.workflowSteps ?? []
|
||||
const curStepIdx = c.currentWorkflowStepIndex
|
||||
@ -154,12 +165,43 @@ export function ContractWorkflowPanel({
|
||||
const actorInLevel = isAdmin || actorIsCurrentApprover
|
||||
const blockedByLevel = isWaiting && !actorInLevel
|
||||
|
||||
// [WQ3b S190] Ô-tích phải soi cờ của ĐÚNG Cấp mà BE sẽ chấm, không phải "có Cấp nào trong
|
||||
// nhóm bật cờ". Mirror `matchingLevel` (`ContractWorkflowService.cs:442-443`): own-level
|
||||
// TRƯỚC, Admin không có own ⇒ phần tử ĐẦU nhóm cùng-order. Soi cờ của Cấp khác là hiện một
|
||||
// ô-tích nói dối — tích xong BE vẫn trình tiếp vì cờ ở Cấp bị chấm là `false`.
|
||||
const actingLevel = currentLevels.find(l => l.approverUserId === user?.id)
|
||||
?? (isAdmin ? currentLevels[0] : undefined)
|
||||
const approverFinalizeEligible = actingLevel?.allowApproverFinalize === true
|
||||
|
||||
// [WQ3b S190] Tên Bước/Cấp đã KẾT THÚC sớm — DTO HĐ không có `finalizeStepName`/
|
||||
// `finalizeLevelName` như KHKK (`ContractDtos.cs:103-105` chỉ 3 field). Suy từ ý-kiến ký
|
||||
// GẦN NHẤT: `ApproveV2Async` UPSERT opinion NGAY TRƯỚC nhánh finalize trong cùng giao dịch
|
||||
// (`:435-529`) ⇒ chữ ký cuối cùng chính là Cấp đã bấm kết thúc. Không suy được (0 ý kiến,
|
||||
// phiếu cũ) ⇒ để `null` và banner nói câu chung, KHÔNG bịa tên.
|
||||
const finalizeMark = useMemo(() => {
|
||||
const ops = c.levelOpinions ?? []
|
||||
if (ops.length === 0) return null
|
||||
const last = [...ops].sort((a, b) => Date.parse(b.signedAt) - Date.parse(a.signedAt))[0]
|
||||
return { stepOrder: last.stepOrder, stepName: last.stepName, levelOrder: last.levelOrder, levelName: last.levelName }
|
||||
}, [c.levelOpinions])
|
||||
|
||||
const transition = useMutation({
|
||||
mutationFn: async (a: PanelAction) => {
|
||||
// Xem khối WIRE ở đầu file: cặp (targetPhase, decision) LÀ thứ chọn nhánh trong
|
||||
// `TransitionAsync`, không phải một tên endpoint riêng.
|
||||
const body = a === 'approve'
|
||||
? { targetPhase: ContractPhase.ChoDuyet, decision: ApprovalDecision.Approve, comment: comment.trim() || null }
|
||||
// [WQ3b S190 · YC-030 Q3] `applyLevelFinalize` CHỈ gắn ở nhánh duyệt, và GẮN TƯỜNG MINH
|
||||
// kể cả khi Cấp không được uỷ quyền: bỏ trống ⇒ controller `?? true`
|
||||
// (`ContractsController.cs:71`), tức im lặng = ĐỒNG Ý kết thúc sớm. Gửi
|
||||
// `approverFinalizeEligible ? applyLevelFinalize : true` = đúng khuôn KHKK
|
||||
// (`KhkkWorkflowPanel.tsx:199`): Cấp không bật cờ thì vế BE `AllowApproverFinalize`
|
||||
// đã `false` nên giá trị này vô hại; Cấp có bật thì đi theo đúng ô-tích người dùng chọn.
|
||||
const body: TransitionContractBody = a === 'approve'
|
||||
? {
|
||||
targetPhase: ContractPhase.ChoDuyet,
|
||||
decision: ApprovalDecision.Approve,
|
||||
comment: comment.trim() || null,
|
||||
applyLevelFinalize: approverFinalizeEligible ? applyLevelFinalize : true,
|
||||
}
|
||||
: { targetPhase: ContractPhase.TraLai, decision: ApprovalDecision.Reject, comment: comment.trim() || null }
|
||||
await api.post(`/contracts/${c.id}/transitions`, body)
|
||||
},
|
||||
@ -167,6 +209,10 @@ export function ContractWorkflowPanel({
|
||||
toast.success(a === 'approve' ? 'Đã duyệt cấp hiện tại.' : 'Đã trả lại người soạn sửa.')
|
||||
setAction(null)
|
||||
setComment('')
|
||||
// Trả ô-tích về mặc định. (Khuôn KHKK `:215-220` KHÔNG reset cờ này — với HĐ thì tick còn
|
||||
// sót là nguy: duyệt Cấp 1 có tick nhưng cấp đó không được uỷ quyền ⇒ BE bỏ qua, sang Cấp 2
|
||||
// ĐƯỢC uỷ quyền thì hộp thoại mở ra đã sẵn tick, kết thúc sớm mà người bấm không chủ ý.)
|
||||
setApplyLevelFinalize(false)
|
||||
// `['contract', id]` = queryKey detail của CẢ 4 host (`ContractDetailPage` ×2 app,
|
||||
// `MyContractsPage`, `ContractsListPage`, `InboxPage`). 3 key còn lại là prefix của
|
||||
// các danh sách — TanStack khớp theo tiền tố nên không cần liệt đủ tham số lọc.
|
||||
@ -192,15 +238,42 @@ export function ContractWorkflowPanel({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ── 1. Header ───────────────────────────────────────────────────────── */}
|
||||
{/* ── 1. Header + meta {code} v{NN} ───────────────────────────────────── */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
|
||||
{/* [WQ3b S190] Mã + phiên bản quy trình ĐANG PIN — khuôn `KhkkWorkflowPanel.tsx:286-289`.
|
||||
🔴 `font-mono` vì đây là MÃ (`HD-TP`…), và `padStart(2,'0')` cho `v01` ≠ `v1` để
|
||||
người đọc so nhanh với bản Designer. KHÔNG có `workflowName` như KHKK — BE HĐ không
|
||||
trả field đó (xem khối lineage đầu file), nên dòng này dừng ở mã + phiên bản. */}
|
||||
{c.workflowCode && (
|
||||
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
|
||||
{c.workflowCode}
|
||||
{c.workflowVersion != null && ` v${String(c.workflowVersion).padStart(2, '0')}`}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-[11px] text-slate-500">
|
||||
Bước = Phòng · Cấp = nhân viên duyệt. Nhiều nhân viên cùng một Cấp thì chỉ cần MỘT
|
||||
người duyệt là sang Cấp kế.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── 1-bis. Banner kết-thúc-sớm ──────────────────────────────────────── */}
|
||||
{/* [WQ3b S190 · YC-030 Q3] Cờ RUNTIME `endedByLevelFinalize` (BE `:529`) — chỉ bật khi
|
||||
việc ĐÃ xảy ra, nên câu ở thì QUÁ KHỨ. Nếu không có banner này, HĐ kết thúc sớm trông
|
||||
y hệt HĐ… bị bỏ dở: cây còn 2-3 Bước ○ chưa ai ký mà phiếu lại `Đã phát hành`.
|
||||
Tông EMERALD khuôn KHKK (`:299`) = "kết thúc hợp lệ", không dùng violet/amber. */}
|
||||
{c.endedByLevelFinalize && (
|
||||
<div className="rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
|
||||
✅ Quy trình đã KẾT THÚC sớm tại cấp được uỷ quyền — không trình tiếp
|
||||
{finalizeMark && (
|
||||
<> (Bước {finalizeMark.stepOrder}
|
||||
{finalizeMark.stepName ? ` — ${finalizeMark.stepName}` : ''} · Cấp {finalizeMark.levelOrder}
|
||||
{finalizeMark.levelName ? ` — ${finalizeMark.levelName}` : ''})</>
|
||||
)}
|
||||
. Các Bước/Cấp phía sau vẽ mờ vì không ai phải duyệt.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 2. Sơ đồ Bước → Cấp → NV ────────────────────────────────────────── */}
|
||||
{steps.length > 0 ? (
|
||||
<ol className="space-y-2">
|
||||
@ -225,9 +298,17 @@ export function ContractWorkflowPanel({
|
||||
<ul className="ml-7 mt-1.5 space-y-1 border-l-2 border-violet-200 pl-2">
|
||||
{orders.map(order => {
|
||||
const group = step.levels.filter(l => l.order === order)
|
||||
const ls: NodeStatus = levelStatus(idx, order)
|
||||
// [WQ3b S190] Cấp được uỷ quyền kết thúc — cờ CẤU HÌNH (khác cờ runtime
|
||||
// `endedByLevelFinalize` ở banner trên).
|
||||
const finalizeHere = group.some(l => l.allowApproverFinalize)
|
||||
const hasSigned = group.some(l => signedByLevelId.has(l.id))
|
||||
// Nhánh KHÔNG CHẠY: HĐ đã phát hành bằng đường kết-thúc-sớm ⇒ các Cấp sau
|
||||
// đó không ai ký. Vẽ MỜ + giữ trạng thái ○ thay vì bịa ✓ cho cả cây
|
||||
// (`stepStatus` trả 'Done' cho MỌI Bước khi `isIssued`).
|
||||
const skipped = isIssued && c.endedByLevelFinalize === true && !hasSigned
|
||||
const ls: NodeStatus = skipped ? 'Pending' : levelStatus(idx, order)
|
||||
return (
|
||||
<li key={`${step.id}-${order}`} className="text-[11px]">
|
||||
<li key={`${step.id}-${order}`} className={cn('text-[11px]', skipped && 'opacity-50')}>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<span className={cn('mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[9px] font-bold', STATUS_DOT[ls])}>
|
||||
{STATUS_ICON[ls]}
|
||||
@ -237,6 +318,21 @@ export function ContractWorkflowPanel({
|
||||
{group[0]?.name || `Cấp ${order}`}
|
||||
{ls === 'Current' && <span className="ml-1.5 text-[10px] font-normal text-brand-700">đang chờ</span>}
|
||||
{ls === 'Done' && <span className="ml-1.5 text-[10px] font-normal text-emerald-600">đã duyệt</span>}
|
||||
{skipped && <span className="ml-1.5 text-[10px] font-normal text-slate-500">không phải qua</span>}
|
||||
{/* Badge Cấp-kết-thúc. CHỮ khác KHKK ("⚑ Duyệt thay CEO") có chủ
|
||||
đích: HĐ không có trạm CEO — kết thúc ở đây nghĩa là HĐ PHÁT
|
||||
HÀNH luôn + sinh mã (`ContractWorkflowService.cs:527-534`).
|
||||
`title` cắm trên `<span>` KHÔNG disabled ⇒ tooltip bật thật
|
||||
(bài S182), và chữ "⚑ Cấp kết thúc" đã nhìn-thấy-được nên
|
||||
thông tin phân biệt không nằm sau hover. */}
|
||||
{finalizeHere && (
|
||||
<span
|
||||
className="ml-1.5 inline-flex items-center rounded bg-violet-100 px-1.5 py-0.5 text-[9px] font-semibold text-violet-800"
|
||||
title="Cấp này được uỷ quyền KẾT THÚC quy trình: duyệt ở đây là HĐ phát hành luôn, không trình tiếp Bước/Cấp sau. Người duyệt vẫn bỏ tích được để trình tiếp như thường."
|
||||
>
|
||||
⚑ Cấp kết thúc
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Liệt-kê NV của cấp: người ĐÃ KÝ in đậm, NV cùng cấp còn lại mờ. */}
|
||||
<div className="flex flex-wrap items-center gap-x-1 gap-y-0.5 text-slate-500">
|
||||
@ -369,6 +465,30 @@ export function ContractWorkflowPanel({
|
||||
từ Bước 1 · Cấp 1.
|
||||
</div>
|
||||
)}
|
||||
{/* [WQ3b S190 · YC-030 Q3] Ô-tích "Cấp này KẾT THÚC quy trình" — khuôn
|
||||
`KhkkWorkflowPanel.tsx:665-679`. CHỈ hiện khi Cấp BE-sẽ-chấm bật cờ
|
||||
(`approverFinalizeEligible`), không hiện theo "nhóm có ai đó bật". Hậu quả HĐ nặng
|
||||
hơn KHKK nên nói thẳng: kết thúc ở đây là PHÁT HÀNH + sinh mã HĐ, không lùi được. */}
|
||||
{action === 'approve' && approverFinalizeEligible && (
|
||||
<div className="mb-3 rounded border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
|
||||
<label className="flex cursor-pointer items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5"
|
||||
checked={applyLevelFinalize}
|
||||
onChange={e => setApplyLevelFinalize(e.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
<span className="font-medium">Cấp này KẾT THÚC quy trình</span>
|
||||
<span className="mt-0.5 block text-[11px] text-emerald-700/80">
|
||||
{applyLevelFinalize
|
||||
? 'Đã tích — duyệt xong HĐ chuyển thẳng "Đã phát hành" (sinh mã HĐ), KẾT THÚC tại cấp này, KHÔNG trình tiếp cấp sau.'
|
||||
: 'Mặc định: duyệt xong HĐ TRÌNH TIẾP lên cấp sau của quy trình. TÍCH vào nếu muốn kết thúc ngay tại cấp này.'}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<Label>
|
||||
Ghi chú{commentRequired ? <span className="text-red-500"> *</span> : ' (tùy chọn)'}
|
||||
</Label>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { ContractDetailContent } from '@/components/contracts/ContractDetailContent'
|
||||
import { ContractWorkflowPanel } from '@/components/contracts/ContractWorkflowPanel'
|
||||
import { WorkflowHistoryPanel } from '@/components/contracts/WorkflowHistoryPanel'
|
||||
import { api } from '@/lib/api'
|
||||
import type { ContractDetail } from '@/types/contracts'
|
||||
@ -27,7 +28,14 @@ export function ContractDetailPage() {
|
||||
<div className="lg:col-span-2">
|
||||
<ContractDetailContent contract={detail.data} onBack={() => navigate(-1)} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="space-y-4">
|
||||
{/* [WQ3b S190 · YC-030 Q5] Máy duyệt V2 dời từ ContractDetailContent ra host —
|
||||
vị trí cột-cạnh khuôn KHKK, không nhân đôi bộ nút. */}
|
||||
{detail.data.approvalWorkflowId != null && (
|
||||
<section className="rounded-lg border border-brand-200 bg-white p-5">
|
||||
<ContractWorkflowPanel contract={detail.data} />
|
||||
</section>
|
||||
)}
|
||||
<WorkflowHistoryPanel contract={detail.data} />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@ -16,6 +16,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { FileText, Search } from 'lucide-react'
|
||||
import { ContractDetailContent } from '@/components/contracts/ContractDetailContent'
|
||||
import { ContractWorkflowPanel } from '@/components/contracts/ContractWorkflowPanel'
|
||||
import { WorkflowHistoryPanel } from '@/components/contracts/WorkflowHistoryPanel'
|
||||
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
|
||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
@ -263,7 +264,17 @@ export function ContractsListPage() {
|
||||
{/* [gate F-1/F-2] Panel-3 TỰ CUỘN ⇒ thân cây `lg:max-h-none` giữ đúng 1 thanh cuộn;
|
||||
sticky/self-start của khuôn vô hiệu ở host display:block này — by-design. */}
|
||||
{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}
|
||||
{selectedId && detail.data && <WorkflowHistoryPanel contract={detail.data} />}
|
||||
{selectedId && detail.data && (
|
||||
<div className="space-y-4">
|
||||
{/* [WQ3b S190 · YC-030 Q5] Máy duyệt V2 dời từ ContractDetailContent ra host. */}
|
||||
{detail.data.approvalWorkflowId != null && (
|
||||
<section className="rounded-lg border border-brand-200 bg-white p-5">
|
||||
<ContractWorkflowPanel contract={detail.data} />
|
||||
</section>
|
||||
)}
|
||||
<WorkflowHistoryPanel contract={detail.data} />
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -184,6 +184,41 @@ export type ContractDetail = {
|
||||
currentWorkflowStepIndex?: number | null
|
||||
/** Cây quy trình THÔ (xem `ContractWorkflowStep`). */
|
||||
workflowSteps?: ContractWorkflowStep[] | null
|
||||
|
||||
// ===== [WQ3b S190 · YC-030 Q3] 3 field meta/kết-thúc-sớm (BE `ContractDtos.cs:103-105`) =====
|
||||
// Khai `?` theo ĐÚNG quy ước 2-nghĩa của khối trên: `undefined` = bundle/cache CHƯA có WQ3-BE.
|
||||
/**
|
||||
* Mã QUY TRÌNH V2 đã pin (`HD-TP`, `HD-NCC`…) — 🔴 KHÔNG phải `maHopDong`, hai thứ cùng
|
||||
* gọi là "mã". `null` khi HĐ chưa pin V2 (V1 legacy) ⇒ panel không vẽ dòng meta.
|
||||
*/
|
||||
workflowCode?: string | null
|
||||
/** Số phiên bản quy trình đã pin — hiển thị `v{NN}` (pad 2). */
|
||||
workflowVersion?: number | null
|
||||
/**
|
||||
* CỜ RUNTIME (BE Mig 73, write-site duy nhất `ContractWorkflowService.cs:529`): HĐ này ĐÃ
|
||||
* kết thúc sớm tại một Cấp được uỷ quyền. 🔴 KHÔNG suy từ `allowApproverFinalize` của cây —
|
||||
* cờ cây là CẤU HÌNH (được phép), cờ này là VIỆC ĐÃ XẢY RA (bài #78 / PE Mig 60).
|
||||
* BE non-nullable (V1 legacy = `false`); `?` ở đây chỉ để chịu payload cũ.
|
||||
*/
|
||||
endedByLevelFinalize?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* [WQ3b S190 · YC-030 Q3] Body `POST /contracts/{id}/transitions` — mirror BE
|
||||
* `TransitionContractBody` (`ContractsController.cs:269-274`).
|
||||
* 🔴 2 field cuối OMIT-ĐƯỢC và omit KHÔNG đồng nghĩa `false`:
|
||||
* • `applyLevelFinalize` bỏ trống ⇒ controller `?? true` (`:71`) ⇒ Cấp có
|
||||
* `allowApproverFinalize` sẽ KẾT THÚC quy trình. Muốn opt-out phải gửi `false` TƯỜNG MINH.
|
||||
* • `skipToFinal` bỏ trống ⇒ `false` (mặc định record BE). FE hiện KHÔNG có đường bấm cho cờ
|
||||
* này (nó là F2/Mig 31, cần `allowApproverSkipToFinal` mà DTO cây chưa trả) — khai ở đây để
|
||||
* nơi gọi thấy đủ hình dạng API, KHÔNG phải để dùng ngay.
|
||||
*/
|
||||
export type TransitionContractBody = {
|
||||
targetPhase: number
|
||||
decision: number
|
||||
comment: string | null
|
||||
applyLevelFinalize?: boolean
|
||||
skipToFinal?: boolean
|
||||
}
|
||||
|
||||
// [W2 S187 · YC-023] Khối "Nguồn gốc KHKK" — BE reverse-join `ContractSigningPlanLines
|
||||
|
||||
Reference in New Issue
Block a user