[CLAUDE] Contract: W5+W6FE card Nguồn-gốc KHKK + panel duyệt 2-nút mirror KhkkWorkflowPanel (dual-render V1) + Designer type-3 family-slice + ruột pre-fill đo-khai
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 8m27s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 8m27s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -4,12 +4,16 @@
|
||||
// approval history live separately in WorkflowHistoryPanel (Panel 3).
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, ListChecks } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, ListChecks, GitBranch } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
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'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
@ -23,6 +27,10 @@ import {
|
||||
type ContractDetail,
|
||||
} from '@/types/contracts'
|
||||
import { ContractTypeLabel } from '@/types/forms'
|
||||
// [W5 S187] Nhãn 8 nhóm duyệt KHKK — IMPORT, KHÔNG chép chuỗi. Map này là single-source
|
||||
// (`types/khkk.ts:140-149`, đã đối chiếu từng ký tự với nhãn BE seed @K4a); đẻ map thứ 2 ở
|
||||
// đây là phá đúng cái acceptance nó phục vụ.
|
||||
import { KHKK_APPROVAL_GROUP_LABELS } from '@/types/khkk'
|
||||
|
||||
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
|
||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
|
||||
@ -71,6 +79,34 @@ export function ContractDetailContent({
|
||||
|
||||
const availableTargets = c.workflow?.nextPhases ?? []
|
||||
|
||||
// ===== [W6 S187 · YC-023 GĐ3] DUAL-RENDER V2-trạm ⟂ V1-legacy ==========================
|
||||
// 🔴 `approvalWorkflowId != null` là ĐÚNG cờ phân nhánh — cùng cờ mà BE dùng để chọn đường
|
||||
// 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ờ).
|
||||
// • 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.
|
||||
const isV2 = c.approvalWorkflowId != null
|
||||
|
||||
// ===== [W5 S187 · YC-023 GĐ3] NGUỒN GỐC KHKK (GĐ2) =====================================
|
||||
// 🔴 Điều kiện render là `!= null` (LOOSE — bắt cả `undefined`) CÓ CHỦ ĐÍCH: `types/contracts.ts:174-182`
|
||||
// khai 2 nghĩa KHÁC NHAU — `undefined` = payload/cache TanStack CHƯA có W2; `null` = BE nói
|
||||
// "HĐ này không có nguồn KHKK". Cả 2 đều KHÔNG có gì để vẽ, nhưng KHÔNG được suy `undefined`
|
||||
// thành "HĐ tạo tay" ở bất kỳ chỗ nào khác.
|
||||
// Nguồn dữ liệu: BE reverse-join `ContractSigningPlanLines WHERE ContractId = id`
|
||||
// (`ContractFeatures.cs:720-748`) — `Contract` KHÔNG có con-trỏ ngược sang kế hoạch.
|
||||
const src = c.source
|
||||
const srcGroup = src?.approvalGroup ?? null
|
||||
// Nhãn nhóm: tra map single-source. Truthy-check theo đúng khuôn `khkkGroupMenuLabel`
|
||||
// (`types/khkk.ts:159-162`) vì `Record<number,string>` trả `string` cho MỌI số — nhóm lạ
|
||||
// (ngoài 1..8, vd dữ liệu SQL thô) phải hiện "chưa có trong danh mục" thay vì ô trống CÂM.
|
||||
const srcGroupLabel = srcGroup != null ? KHKK_APPROVAL_GROUP_LABELS[srcGroup] : undefined
|
||||
|
||||
function openAction(decisionType: number) {
|
||||
const targets = c.workflow?.nextPhases ?? []
|
||||
const defaultTarget = decisionType === ApprovalDecision.Reject
|
||||
@ -102,7 +138,9 @@ export function ContractDetailContent({
|
||||
<PhaseBadge phase={c.phase} />
|
||||
</div>
|
||||
</div>
|
||||
{availableTargets.length > 0 && (
|
||||
{/* [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. */}
|
||||
{!isV2 && availableTargets.length > 0 && (
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button variant="outline" onClick={() => openAction(ApprovalDecision.Reject)}>
|
||||
<XCircle className="h-4 w-4" />
|
||||
@ -139,6 +177,114 @@ export function ContractDetailContent({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* [W5 S187 · YC-023 GĐ3] Card "Nguồn gốc Kế hoạch ký kết" — đứng NGAY SAU khối thông
|
||||
tin chính: người mở HĐ cần biết nó đẻ ra từ đâu trước khi đọc góp ý / chi tiết.
|
||||
Tông VIOLET = GĐ2 (KHKK) theo bảng màu 4 giai đoạn `PipelineStageFolders.tsx:41-46`;
|
||||
link sang Phiếu Duyệt NCC dùng tông BRAND = GĐ1 (PE).
|
||||
⚠️ Chỉ dùng stop violet CÓ THẬT trong `index.css` `@theme` (50/100/500/600/700) —
|
||||
violet-300/800 rơi về bảng Tailwind mặc định, lệch hệ màu (đã thấy ở
|
||||
`KhkkDetailContent.tsx:477`, pre-existing, không sửa ở wave này).
|
||||
Vỏ `<section className="rounded-lg border … p-5">` + `<h2 className="mb-3 …">` bám
|
||||
ĐÚNG khuôn các khối cùng trang — 2 app đều KHÔNG có shadcn `Card`. */}
|
||||
{src != null && (
|
||||
<section className="rounded-lg border border-violet-100 bg-violet-50/60 p-5">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Nguồn gốc Kế hoạch ký kết
|
||||
</h2>
|
||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="col-span-2">
|
||||
<dt className="text-slate-500">Mã kế hoạch</dt>
|
||||
<dd className="mt-1 flex flex-wrap items-center gap-2">
|
||||
{/* 🔴 CHỈ link khi phiếu cha còn đọc được. `maKeHoach === null` ⇔ phiếu đã xoá
|
||||
mềm (BE `ContractFeatures.cs:738-740` lấy mã qua query CÓ global filter), mà
|
||||
màn `/khkk/:id` đọc qua `GetContractSigningPlanQuery`
|
||||
(`ContractSigningPlanFeatures.cs:957-962`) cũng KHÔNG `IgnoreQueryFilters`
|
||||
⇒ 404. Link tới đó = ngõ cụt. Trạng thái này nói bằng CHỮ NHÌN THẤY ĐƯỢC,
|
||||
không giấu sau tooltip. */}
|
||||
{src.maKeHoach != null ? (
|
||||
<Link
|
||||
// `?group=` KHÔNG phải param chết: `KhkkDetailPage.tsx:50-52` (cả 2 app) đọc
|
||||
// và validate bằng REGEX CHUỖI `/^[1-8]$/` rồi dựng đường "quay lại danh sách"
|
||||
// `/khkk/list?group=n` ⇒ giữ đúng chỗ đứng + sidebar sáng đúng leaf nhóm.
|
||||
to={`/khkk/${src.planId}${srcGroup != null ? `?group=${srcGroup}` : ''}`}
|
||||
className="font-mono font-medium text-violet-600 underline underline-offset-2 hover:text-violet-700"
|
||||
>
|
||||
{src.maKeHoach}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-mono text-slate-500">(phiếu đã xoá)</span>
|
||||
<span className="text-[11px] text-slate-500">— không mở được phiếu gốc</span>
|
||||
</>
|
||||
)}
|
||||
{srcGroup != null && (
|
||||
<span className="rounded bg-violet-100 px-2 py-0.5 text-[11px] font-medium text-violet-700">
|
||||
Nhóm N{srcGroup} · {srcGroupLabel ? srcGroupLabel : 'chưa có trong danh mục'}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<dt className="text-slate-500">Hạng mục đưa vào HĐ ({src.tenHangMucs.length})</dt>
|
||||
<dd className="mt-1">
|
||||
{src.tenHangMucs.length === 0 ? (
|
||||
// BE dựng `Source` chỉ khi có ≥1 dòng nối ⇒ nhánh này gần như bất khả; giữ chữ
|
||||
// trung tính để rỗng-vì-dữ-liệu không trông giống lỗi tải.
|
||||
<span className="text-slate-400">(không có hạng mục nào)</span>
|
||||
) : (
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{src.tenHangMucs.map((ten, i) => (
|
||||
<li
|
||||
key={`${i}-${ten}`}
|
||||
className="rounded border border-violet-100 bg-white px-2 py-0.5 text-[12px] text-slate-700"
|
||||
>
|
||||
{ten}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-slate-500">Σ giá duyệt chốt</dt>
|
||||
{/* Dùng ĐÚNG formatter của file (`fmtMoney`) — không đẻ khuôn tiền thứ 2. */}
|
||||
<dd className="mt-0.5 font-medium text-slate-900">
|
||||
{src.approvedAmountTotal != null ? fmtMoney(src.approvedAmountTotal) : '—'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-slate-500">Phiếu Duyệt NCC gốc</dt>
|
||||
<dd className="mt-0.5">
|
||||
{src.purchaseEvaluationId ? (
|
||||
<Link
|
||||
to={`/purchase-evaluations/${src.purchaseEvaluationId}`}
|
||||
className="font-medium text-brand-600 underline underline-offset-2 hover:text-brand-700"
|
||||
>
|
||||
Mở phiếu Duyệt NCC →
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user