[CLAUDE] FE-User+FE-Admin: bỏ tabs, Chi tiết + Lịch sử 7/3 grid dưới Overview
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m44s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m44s
User feedback: thay vì click tab để switch, hiển thị Chi tiết + Lịch sử LUÔN ngay dưới Tổng quan content. Tỷ lệ cột 7 (Chi tiết) - 3 (Lịch sử điều chỉnh). ## Thay đổi (apply 2 app) ContractDetailContent.tsx: - Bỏ TabsNav + tab state + TabButton helper - Bỏ conditional render theo tab - Tổng quan content (Info / Comments / Attachments) render flat đầu tiên - Thêm grid lg:grid-cols-10 dưới cùng: - lg:col-span-7 → ContractDetailsTab (line items) - lg:col-span-3 → ContractChangelogsTab (timeline) - Mobile (<lg): stack vertical 1 cột, Chi tiết trên, Lịch sử dưới ## Build verify - fe-user: tsc + vite pass (17.23s) - fe-admin: tsc + vite pass (8.12s) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -4,7 +4,7 @@
|
|||||||
// trong WorkflowHistoryPanel (Panel 3).
|
// trong WorkflowHistoryPanel (Panel 3).
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, Info, ListChecks, History } from 'lucide-react'
|
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, ListChecks, History } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
import { SlaTimer } from '@/components/SlaTimer'
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
@ -17,7 +17,6 @@ import { Textarea } from '@/components/ui/Textarea'
|
|||||||
import { Dialog } from '@/components/ui/Dialog'
|
import { Dialog } from '@/components/ui/Dialog'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import { cn } from '@/lib/cn'
|
|
||||||
import {
|
import {
|
||||||
ApprovalDecision,
|
ApprovalDecision,
|
||||||
ContractPhase,
|
ContractPhase,
|
||||||
@ -29,8 +28,6 @@ import { ContractTypeLabel } from '@/types/forms'
|
|||||||
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
|
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
|
||||||
|
|
||||||
type Tab = 'overview' | 'details' | 'history'
|
|
||||||
|
|
||||||
export function ContractDetailContent({
|
export function ContractDetailContent({
|
||||||
contract: c,
|
contract: c,
|
||||||
onBack,
|
onBack,
|
||||||
@ -45,7 +42,6 @@ export function ContractDetailContent({
|
|||||||
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
|
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const [commentInput, setCommentInput] = useState('')
|
const [commentInput, setCommentInput] = useState('')
|
||||||
const [tab, setTab] = useState<Tab>('overview')
|
|
||||||
|
|
||||||
const transition = useMutation({
|
const transition = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@ -122,76 +118,81 @@ export function ContractDetailContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs nav — Tổng quan / Chi tiết / Lịch sử */}
|
{/* Tổng quan content — luôn hiển thị, không tabs */}
|
||||||
<nav className="flex gap-1 border-b border-slate-200">
|
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||||
<TabButton active={tab === 'overview'} onClick={() => setTab('overview')} icon={Info} label="Tổng quan" />
|
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin HĐ</h2>
|
||||||
<TabButton active={tab === 'details'} onClick={() => setTab('details')} icon={ListChecks} label={`Chi tiết (${ContractTypeLabel[c.type] ?? ''})`} />
|
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<TabButton active={tab === 'history'} onClick={() => setTab('history')} icon={History} label="Lịch sử" />
|
<div><dt className="text-slate-500">Loại</dt><dd>{ContractTypeLabel[c.type] ?? '—'}</dd></div>
|
||||||
</nav>
|
<div><dt className="text-slate-500">Giá trị</dt><dd>{fmtMoney(c.giaTri)}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">Phòng ban</dt><dd>{c.departmentName ?? '—'}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<dt className="text-slate-500 mb-1">SLA</dt>
|
||||||
|
<dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd>
|
||||||
|
</div>
|
||||||
|
<div><dt className="text-slate-500">Bypass CCM</dt><dd>{c.bypassProcurementAndCCM ? 'Có (HĐ Chủ đầu tư)' : 'Không'}</dd></div>
|
||||||
|
</dl>
|
||||||
|
{c.noiDung && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<dt className="text-sm text-slate-500">Nội dung</dt>
|
||||||
|
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{c.noiDung}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
{tab === 'overview' && (
|
<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">
|
||||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
<MessageSquare className="h-4 w-4" />
|
||||||
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin HĐ</h2>
|
Góp ý ({c.comments.length})
|
||||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
</h2>
|
||||||
<div><dt className="text-slate-500">Loại</dt><dd>{ContractTypeLabel[c.type] ?? '—'}</dd></div>
|
<div className="space-y-3">
|
||||||
<div><dt className="text-slate-500">Giá trị</dt><dd>{fmtMoney(c.giaTri)}</dd></div>
|
{c.comments.length === 0 && <div className="text-sm text-slate-400">Chưa có góp ý.</div>}
|
||||||
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
{c.comments.map(cm => (
|
||||||
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
<div key={cm.id} className="rounded-md border border-slate-100 p-3">
|
||||||
<div><dt className="text-slate-500">Phòng ban</dt><dd>{c.departmentName ?? '—'}</dd></div>
|
<div className="flex items-center justify-between text-xs text-slate-500">
|
||||||
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
<span className="font-medium text-slate-700">{cm.userName}</span>
|
||||||
<div className="col-span-2">
|
<span>{fmt(cm.createdAt)} · {ContractPhaseLabel[cm.phase]}</span>
|
||||||
<dt className="text-slate-500 mb-1">SLA</dt>
|
|
||||||
<dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div><dt className="text-slate-500">Bypass CCM</dt><dd>{c.bypassProcurementAndCCM ? 'Có (HĐ Chủ đầu tư)' : 'Không'}</dd></div>
|
<div className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{cm.content}</div>
|
||||||
</dl>
|
|
||||||
{c.noiDung && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<dt className="text-sm text-slate-500">Nội dung</dt>
|
|
||||||
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{c.noiDung}</dd>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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" />
|
|
||||||
Góp ý ({c.comments.length})
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{c.comments.length === 0 && <div className="text-sm text-slate-400">Chưa có góp ý.</div>}
|
|
||||||
{c.comments.map(cm => (
|
|
||||||
<div key={cm.id} className="rounded-md border border-slate-100 p-3">
|
|
||||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
|
||||||
<span className="font-medium text-slate-700">{cm.userName}</span>
|
|
||||||
<span>{fmt(cm.createdAt)} · {ContractPhaseLabel[cm.phase]}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{cm.content}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<form
|
))}
|
||||||
className="mt-4 flex gap-2"
|
</div>
|
||||||
onSubmit={(e: FormEvent) => {
|
<form
|
||||||
e.preventDefault()
|
className="mt-4 flex gap-2"
|
||||||
if (!commentInput.trim()) return
|
onSubmit={(e: FormEvent) => {
|
||||||
addComment.mutate(commentInput.trim())
|
e.preventDefault()
|
||||||
}}
|
if (!commentInput.trim()) return
|
||||||
>
|
addComment.mutate(commentInput.trim())
|
||||||
<Textarea rows={2} placeholder="Thêm góp ý…" value={commentInput} onChange={e => setCommentInput(e.target.value)} />
|
}}
|
||||||
<Button type="submit" disabled={addComment.isPending || !commentInput.trim()}>
|
>
|
||||||
Gửi
|
<Textarea rows={2} placeholder="Thêm góp ý…" value={commentInput} onChange={e => setCommentInput(e.target.value)} />
|
||||||
</Button>
|
<Button type="submit" disabled={addComment.isPending || !commentInput.trim()}>
|
||||||
</form>
|
Gửi
|
||||||
</section>
|
</Button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
|
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'details' && <ContractDetailsTab contract={c} />}
|
{/* Chi tiết + Lịch sử điều chỉnh — 7/3 grid bên dưới */}
|
||||||
{tab === 'history' && <ContractChangelogsTab contractId={c.id} />}
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-10">
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 lg:col-span-7">
|
||||||
|
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<ListChecks className="h-4 w-4" />
|
||||||
|
Chi tiết ({ContractTypeLabel[c.type] ?? '—'})
|
||||||
|
</h2>
|
||||||
|
<ContractDetailsTab contract={c} />
|
||||||
|
</section>
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 lg:col-span-3">
|
||||||
|
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<History className="h-4 w-4" />
|
||||||
|
Lịch sử điều chỉnh
|
||||||
|
</h2>
|
||||||
|
<ContractChangelogsTab contractId={c.id} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={actionOpen}
|
open={actionOpen}
|
||||||
@ -225,26 +226,3 @@ export function ContractDetailContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
active, onClick, icon: Icon, label,
|
|
||||||
}: {
|
|
||||||
active: boolean
|
|
||||||
onClick: () => void
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
label: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
'-mb-px flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm transition',
|
|
||||||
active
|
|
||||||
? 'border-brand-600 font-semibold text-brand-700'
|
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
// approval history live separately in WorkflowHistoryPanel (Panel 3).
|
// approval history live separately in WorkflowHistoryPanel (Panel 3).
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, Info, ListChecks, History } from 'lucide-react'
|
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, ListChecks, History } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
import { SlaTimer } from '@/components/SlaTimer'
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
@ -17,7 +17,6 @@ import { Textarea } from '@/components/ui/Textarea'
|
|||||||
import { Dialog } from '@/components/ui/Dialog'
|
import { Dialog } from '@/components/ui/Dialog'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import { cn } from '@/lib/cn'
|
|
||||||
import {
|
import {
|
||||||
ApprovalDecision,
|
ApprovalDecision,
|
||||||
ContractPhase,
|
ContractPhase,
|
||||||
@ -26,8 +25,6 @@ import {
|
|||||||
} from '@/types/contracts'
|
} from '@/types/contracts'
|
||||||
import { ContractTypeLabel } from '@/types/forms'
|
import { ContractTypeLabel } from '@/types/forms'
|
||||||
|
|
||||||
type Tab = 'overview' | 'details' | 'history'
|
|
||||||
|
|
||||||
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
|
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
|
||||||
|
|
||||||
@ -45,7 +42,6 @@ export function ContractDetailContent({
|
|||||||
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
|
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const [commentInput, setCommentInput] = useState('')
|
const [commentInput, setCommentInput] = useState('')
|
||||||
const [tab, setTab] = useState<Tab>('overview')
|
|
||||||
|
|
||||||
const transition = useMutation({
|
const transition = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@ -122,74 +118,79 @@ export function ContractDetailContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs nav — Tổng quan / Chi tiết / Lịch sử */}
|
{/* Tổng quan content — luôn hiển thị, không tabs */}
|
||||||
<nav className="flex gap-1 border-b border-slate-200">
|
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||||
<TabButton active={tab === 'overview'} onClick={() => setTab('overview')} icon={Info} label="Tổng quan" />
|
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin HĐ</h2>
|
||||||
<TabButton active={tab === 'details'} onClick={() => setTab('details')} icon={ListChecks} label={`Chi tiết (${ContractTypeLabel[c.type] ?? ''})`} />
|
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<TabButton active={tab === 'history'} onClick={() => setTab('history')} icon={History} label="Lịch sử" />
|
<div><dt className="text-slate-500">Loại</dt><dd>{ContractTypeLabel[c.type] ?? '—'}</dd></div>
|
||||||
</nav>
|
<div><dt className="text-slate-500">Giá trị</dt><dd>{fmtMoney(c.giaTri)}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
||||||
|
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<dt className="text-slate-500 mb-1">SLA</dt>
|
||||||
|
<dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{c.noiDung && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<dt className="text-sm text-slate-500">Nội dung</dt>
|
||||||
|
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{c.noiDung}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
{tab === 'overview' && (
|
<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">
|
||||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
<MessageSquare className="h-4 w-4" />
|
||||||
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin HĐ</h2>
|
Góp ý ({c.comments.length})
|
||||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
</h2>
|
||||||
<div><dt className="text-slate-500">Loại</dt><dd>{ContractTypeLabel[c.type] ?? '—'}</dd></div>
|
<div className="space-y-3">
|
||||||
<div><dt className="text-slate-500">Giá trị</dt><dd>{fmtMoney(c.giaTri)}</dd></div>
|
{c.comments.length === 0 && <div className="text-sm text-slate-400">Chưa có góp ý.</div>}
|
||||||
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
{c.comments.map(cm => (
|
||||||
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
<div key={cm.id} className="rounded-md border border-slate-100 p-3">
|
||||||
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
<div className="flex items-center justify-between text-xs text-slate-500">
|
||||||
<div className="col-span-2">
|
<span className="font-medium text-slate-700">{cm.userName}</span>
|
||||||
<dt className="text-slate-500 mb-1">SLA</dt>
|
<span>{fmt(cm.createdAt)} · {ContractPhaseLabel[cm.phase]}</span>
|
||||||
<dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd>
|
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
<div className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{cm.content}</div>
|
||||||
{c.noiDung && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<dt className="text-sm text-slate-500">Nội dung</dt>
|
|
||||||
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{c.noiDung}</dd>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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" />
|
|
||||||
Góp ý ({c.comments.length})
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{c.comments.length === 0 && <div className="text-sm text-slate-400">Chưa có góp ý.</div>}
|
|
||||||
{c.comments.map(cm => (
|
|
||||||
<div key={cm.id} className="rounded-md border border-slate-100 p-3">
|
|
||||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
|
||||||
<span className="font-medium text-slate-700">{cm.userName}</span>
|
|
||||||
<span>{fmt(cm.createdAt)} · {ContractPhaseLabel[cm.phase]}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{cm.content}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<form
|
))}
|
||||||
className="mt-4 flex gap-2"
|
</div>
|
||||||
onSubmit={(e: FormEvent) => {
|
<form
|
||||||
e.preventDefault()
|
className="mt-4 flex gap-2"
|
||||||
if (!commentInput.trim()) return
|
onSubmit={(e: FormEvent) => {
|
||||||
addComment.mutate(commentInput.trim())
|
e.preventDefault()
|
||||||
}}
|
if (!commentInput.trim()) return
|
||||||
>
|
addComment.mutate(commentInput.trim())
|
||||||
<Textarea rows={2} placeholder="Thêm góp ý…" value={commentInput} onChange={e => setCommentInput(e.target.value)} />
|
}}
|
||||||
<Button type="submit" disabled={addComment.isPending || !commentInput.trim()}>
|
>
|
||||||
Gửi
|
<Textarea rows={2} placeholder="Thêm góp ý…" value={commentInput} onChange={e => setCommentInput(e.target.value)} />
|
||||||
</Button>
|
<Button type="submit" disabled={addComment.isPending || !commentInput.trim()}>
|
||||||
</form>
|
Gửi
|
||||||
</section>
|
</Button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
|
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'details' && <ContractDetailsTab contract={c} />}
|
{/* Chi tiết + Lịch sử điều chỉnh — 7/3 grid bên dưới */}
|
||||||
{tab === 'history' && <ContractChangelogsTab contractId={c.id} />}
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-10">
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 lg:col-span-7">
|
||||||
|
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<ListChecks className="h-4 w-4" />
|
||||||
|
Chi tiết ({ContractTypeLabel[c.type] ?? '—'})
|
||||||
|
</h2>
|
||||||
|
<ContractDetailsTab contract={c} />
|
||||||
|
</section>
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 lg:col-span-3">
|
||||||
|
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<History className="h-4 w-4" />
|
||||||
|
Lịch sử điều chỉnh
|
||||||
|
</h2>
|
||||||
|
<ContractChangelogsTab contractId={c.id} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={actionOpen}
|
open={actionOpen}
|
||||||
@ -223,26 +224,3 @@ export function ContractDetailContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
active, onClick, icon: Icon, label,
|
|
||||||
}: {
|
|
||||||
active: boolean
|
|
||||||
onClick: () => void
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
label: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
'-mb-px flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm transition',
|
|
||||||
active
|
|
||||||
? 'border-brand-600 font-semibold text-brand-700'
|
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user