// Reusable detail body — used by full-page ContractDetailPage AND embedded // in MyContractsPage 3-panel layout (Panel 2). Renders header (title + phase // + actions) + Info + Comments + Attachments + transition Dialog. Workflow + // 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 } 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: ['my-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') }, 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 so actions luôn visible */}
{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}
Người soạn
{c.drafterName ?? '—'}
SLA
{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()) }} >