From b75448e7112843264390e7be7b36760c629e7bbd Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 23 Apr 2026 09:04:46 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User+FE-Admin:=203-panel=20layout?= =?UTF-8?q?=20cho=20danh=20s=C3=A1ch=20H=C4=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign trang Danh sách HĐ (Ct_*_List menu fe-user + /contracts admin) thành 3-panel: List | Detail content | Workflow + lịch sử duyệt. Selected HĐ giữ qua URL ?id= (bookmarkable + back/forward navigation work). ## Components mới (reuse cho cả 3-panel embedded + fullpage detail) ### fe-user/src/components/contracts/ - ContractDetailContent.tsx — Panel 2 body: header sticky (title + phase + actions Yêu cầu sửa/Duyệt) + Info section + Comments thread + form thêm góp ý + Attachments. Transition Dialog inline. Prop optional onBack — render arrow back button (fullpage) hoặc skip (embedded). - WorkflowHistoryPanel.tsx — Panel 3: WorkflowSummaryCard (timeline policy current+next) + Lịch sử duyệt (approvals: phase from→to + actor + timestamp + comment). ### fe-admin/src/components/contracts/ - ContractDetailContent.tsx — variant admin có thêm Phòng ban + Bypass CCM trong Info section. Invalidate ['contracts'] khi transition. - WorkflowHistoryPanel.tsx — identical fe-user. ## Trang refactored ### fe-user - MyContractsPage.tsx — bỏ DataTable, dùng 3-panel grid lg:grid-cols-[320px_1fr_360px] h-[calc(100vh-4rem)]: Panel 1: search box + list compact (mã/tên/NCC/phase/SLA/giá), click update ?id= active highlight ring-brand Panel 2: detail content embedded Panel 3: workflow + history Mobile ( --- .../contracts/ContractDetailContent.tsx | 206 ++++++++++++++ .../contracts/WorkflowHistoryPanel.tsx | 45 +++ .../pages/contracts/ContractDetailPage.tsx | 235 +--------------- .../src/pages/contracts/ContractsListPage.tsx | 265 ++++++++++++++---- .../contracts/ContractDetailContent.tsx | 204 ++++++++++++++ .../contracts/WorkflowHistoryPanel.tsx | 46 +++ .../pages/contracts/ContractDetailPage.tsx | 226 +-------------- .../src/pages/contracts/MyContractsPage.tsx | 222 +++++++++++---- 8 files changed, 899 insertions(+), 550 deletions(-) create mode 100644 fe-admin/src/components/contracts/ContractDetailContent.tsx create mode 100644 fe-admin/src/components/contracts/WorkflowHistoryPanel.tsx create mode 100644 fe-user/src/components/contracts/ContractDetailContent.tsx create mode 100644 fe-user/src/components/contracts/WorkflowHistoryPanel.tsx diff --git a/fe-admin/src/components/contracts/ContractDetailContent.tsx b/fe-admin/src/components/contracts/ContractDetailContent.tsx new file mode 100644 index 0000000..982d64f --- /dev/null +++ b/fe-admin/src/components/contracts/ContractDetailContent.tsx @@ -0,0 +1,206 @@ +// Reusable detail body — used by full-page ContractDetailPage AND embedded +// in ContractsListPage 3-panel layout (Panel 2). Admin variant include thêm +// Phòng ban + Bypass CCM trong Info section. Workflow + history live separately +// trong WorkflowHistoryPanel (Panel 3). +import { useState, type FormEvent } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { ArrowLeft, CheckCircle2, MessageSquare, XCircle } from 'lucide-react' +import { toast } from 'sonner' +import { PhaseBadge } from '@/components/PhaseBadge' +import { SlaTimer } from '@/components/SlaTimer' +import { ContractAttachmentsSection } from '@/components/ContractAttachmentsSection' +import { Button } from '@/components/ui/Button' +import { Select } from '@/components/ui/Select' +import { Textarea } from '@/components/ui/Textarea' +import { Dialog } from '@/components/ui/Dialog' +import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' +import { + ApprovalDecision, + ContractPhase, + ContractPhaseLabel, + type ContractDetail, +} from '@/types/contracts' +import { ContractTypeLabel } from '@/types/forms' + +const fmt = (s: string) => new Date(s).toLocaleString('vi-VN') +const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND' + +export function ContractDetailContent({ + contract: c, + onBack, +}: { + contract: ContractDetail + /** Optional back handler — shown as arrow button next to title. Pass `navigate(-1)` for fullpage; omit for embedded panel. */ + onBack?: () => void +}) { + const qc = useQueryClient() + const [actionOpen, setActionOpen] = useState(false) + const [targetPhase, setTargetPhase] = useState(0) + const [decision, setDecision] = useState(ApprovalDecision.Approve) + const [comment, setComment] = useState('') + const [commentInput, setCommentInput] = useState('') + + const transition = useMutation({ + mutationFn: async () => { + await api.post(`/contracts/${c.id}/transitions`, { targetPhase, decision, comment: comment || null }) + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['contract', c.id] }) + qc.invalidateQueries({ queryKey: ['contracts'] }) + qc.invalidateQueries({ queryKey: ['inbox'] }) + toast.success('Đã chuyển phase') + setActionOpen(false) + setComment('') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + const addComment = useMutation({ + mutationFn: async (content: string) => { + await api.post(`/contracts/${c.id}/comments`, { content }) + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['contract', c.id] }) + setCommentInput('') + toast.success('Đã gửi comment') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + const availableTargets = c.workflow?.nextPhases ?? [] + + function openAction(decisionType: number) { + const targets = c.workflow?.nextPhases ?? [] + const defaultTarget = decisionType === ApprovalDecision.Reject + ? targets.find(t => t === ContractPhase.DangSoanThao) ?? targets[0] + : targets[0] + setTargetPhase(defaultTarget) + setDecision(decisionType) + setActionOpen(true) + } + + return ( +
+ {/* Header — sticky inside scroll container */} +
+
+
+
+ {onBack && ( + + )} +

+ {c.tenHopDong ?? 'HĐ chưa đặt tên'} +

+
+
+ {c.maHopDong ?? '(chưa có mã)'} + +
+
+ {availableTargets.length > 0 && ( +
+ + +
+ )} +
+
+ +
+

Thông tin HĐ

+
+
Loại
{ContractTypeLabel[c.type] ?? '—'}
+
Giá trị
{fmtMoney(c.giaTri)}
+
NCC
{c.supplierName}
+
Dự án
{c.projectName}
+
Phòng ban
{c.departmentName ?? '—'}
+
Người soạn
{c.drafterName ?? '—'}
+
+
SLA
+
+
+
Bypass CCM
{c.bypassProcurementAndCCM ? 'Có (HĐ Chủ đầu tư)' : 'Không'}
+
+ {c.noiDung && ( +
+
Nội dung
+
{c.noiDung}
+
+ )} +
+ +
+

+ + Góp ý ({c.comments.length}) +

+
+ {c.comments.length === 0 &&
Chưa có góp ý.
} + {c.comments.map(cm => ( +
+
+ {cm.userName} + {fmt(cm.createdAt)} · {ContractPhaseLabel[cm.phase]} +
+
{cm.content}
+
+ ))} +
+
{ + e.preventDefault() + if (!commentInput.trim()) return + addComment.mutate(commentInput.trim()) + }} + > +