[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

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:
pqhuy1987
2026-04-23 10:39:07 +07:00
parent b3762afbc3
commit ad0652d590
2 changed files with 140 additions and 184 deletions

View File

@ -4,7 +4,7 @@
// trong WorkflowHistoryPanel (Panel 3).
import { useState, type FormEvent } from 'react'
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 { PhaseBadge } from '@/components/PhaseBadge'
import { SlaTimer } from '@/components/SlaTimer'
@ -17,7 +17,6 @@ import { Textarea } from '@/components/ui/Textarea'
import { Dialog } from '@/components/ui/Dialog'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn'
import {
ApprovalDecision,
ContractPhase,
@ -29,8 +28,6 @@ import { ContractTypeLabel } from '@/types/forms'
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
type Tab = 'overview' | 'details' | 'history'
export function ContractDetailContent({
contract: c,
onBack,
@ -45,7 +42,6 @@ export function ContractDetailContent({
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
const [comment, setComment] = useState('')
const [commentInput, setCommentInput] = useState('')
const [tab, setTab] = useState<Tab>('overview')
const transition = useMutation({
mutationFn: async () => {
@ -122,15 +118,7 @@ export function ContractDetailContent({
</div>
</div>
{/* Tabs nav — Tổng quan / Chi tiết / Lịch sử */}
<nav className="flex gap-1 border-b border-slate-200">
<TabButton active={tab === 'overview'} onClick={() => setTab('overview')} icon={Info} label="Tổng quan" />
<TabButton active={tab === 'details'} onClick={() => setTab('details')} icon={ListChecks} label={`Chi tiết (${ContractTypeLabel[c.type] ?? ''})`} />
<TabButton active={tab === 'history'} onClick={() => setTab('history')} icon={History} label="Lịch sử" />
</nav>
{tab === 'overview' && (
<>
{/* Tổng quan content — luôn hiển thị, không tabs */}
<section className="rounded-lg border border-slate-200 bg-white p-5">
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin </h2>
<dl className="grid grid-cols-2 gap-3 text-sm">
@ -187,11 +175,24 @@ export function ContractDetailContent({
</section>
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
</>
)}
{tab === 'details' && <ContractDetailsTab contract={c} />}
{tab === 'history' && <ContractChangelogsTab contractId={c.id} />}
{/* Chi tiết + Lịch sử điều chỉnh — 7/3 grid bên dưới */}
<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
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>
)
}

View File

@ -4,7 +4,7 @@
// 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, Info, ListChecks, History } from 'lucide-react'
import { ArrowLeft, CheckCircle2, MessageSquare, XCircle, ListChecks, History } from 'lucide-react'
import { toast } from 'sonner'
import { PhaseBadge } from '@/components/PhaseBadge'
import { SlaTimer } from '@/components/SlaTimer'
@ -17,7 +17,6 @@ import { Textarea } from '@/components/ui/Textarea'
import { Dialog } from '@/components/ui/Dialog'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn'
import {
ApprovalDecision,
ContractPhase,
@ -26,8 +25,6 @@ import {
} from '@/types/contracts'
import { ContractTypeLabel } from '@/types/forms'
type Tab = 'overview' | 'details' | 'history'
const fmt = (s: string) => new Date(s).toLocaleString('vi-VN')
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') + ' VND'
@ -45,7 +42,6 @@ export function ContractDetailContent({
const [decision, setDecision] = useState<number>(ApprovalDecision.Approve)
const [comment, setComment] = useState('')
const [commentInput, setCommentInput] = useState('')
const [tab, setTab] = useState<Tab>('overview')
const transition = useMutation({
mutationFn: async () => {
@ -122,15 +118,7 @@ export function ContractDetailContent({
</div>
</div>
{/* Tabs nav — Tổng quan / Chi tiết / Lịch sử */}
<nav className="flex gap-1 border-b border-slate-200">
<TabButton active={tab === 'overview'} onClick={() => setTab('overview')} icon={Info} label="Tổng quan" />
<TabButton active={tab === 'details'} onClick={() => setTab('details')} icon={ListChecks} label={`Chi tiết (${ContractTypeLabel[c.type] ?? ''})`} />
<TabButton active={tab === 'history'} onClick={() => setTab('history')} icon={History} label="Lịch sử" />
</nav>
{tab === 'overview' && (
<>
{/* Tổng quan content — luôn hiển thị, không tabs */}
<section className="rounded-lg border border-slate-200 bg-white p-5">
<h2 className="mb-3 text-sm font-semibold text-slate-700">Thông tin </h2>
<dl className="grid grid-cols-2 gap-3 text-sm">
@ -185,11 +173,24 @@ export function ContractDetailContent({
</section>
<ContractAttachmentsSection contractId={c.id} attachments={c.attachments} />
</>
)}
{tab === 'details' && <ContractDetailsTab contract={c} />}
{tab === 'history' && <ContractChangelogsTab contractId={c.id} />}
{/* Chi tiết + Lịch sử điều chỉnh — 7/3 grid bên dưới */}
<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
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>
)
}