[CLAUDE] FE-Admin: redesign Phase 1 — density-first design system (NAMGROUP-ref, giữ brand)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m24s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m24s
Tham khảo NAMGROUP density conventions, GIỮ brand #1F7DC1 + Be Vietnam Pro. - index.css: density heading ladder (semibold, drop font-bold) + .label-eyebrow util - 6 UI primitives (Button/Input/Label/Select/Textarea/Dialog): text-xs font-semibold, py-1.5 ≤36px, rounded-lg, brand focus-ring - 6 shell (DataTable sticky-thead+RowActions/Layout brand-rail/TopBar/PageHeader/PhaseBadge/EmptyState) - DashboardPage flagship: KPI cards + brand-tinted icon chips + uppercase labels Visual-only — functionality unchanged (Button variant/size keys stable 51 call-sites, props/forwardRef intact). build 0 TS err. reviewer PASS 0 blocker (2 minor slate-400 hint a11y defer). fe-admin only (fe-user mirror = Phase 3). Dashboard live-visual blocked by dev auth-rig → xem live sau deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -6,6 +6,7 @@ import { BarChart } from '@/components/BarChart'
|
||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/cn'
|
||||
import type { DashboardStats } from '@/types/reports'
|
||||
|
||||
type MyDashboard = {
|
||||
@ -22,6 +23,13 @@ const fmtMoney = (v: number) => {
|
||||
return v.toLocaleString('vi-VN')
|
||||
}
|
||||
|
||||
// Tone → tinted icon chip (brand-led, semantic accents for warn/good).
|
||||
const STAT_TONE: Record<'default' | 'warn' | 'good', { chip: string; icon: string; value: string }> = {
|
||||
default: { chip: 'bg-brand-50', icon: 'text-brand-600', value: 'text-slate-900' },
|
||||
warn: { chip: 'bg-amber-50', icon: 'text-amber-600', value: 'text-amber-700' },
|
||||
good: { chip: 'bg-emerald-50', icon: 'text-emerald-600', value: 'text-slate-900' },
|
||||
}
|
||||
|
||||
function StatCard({ icon: Icon, label, value, hint, tone = 'default' }: {
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
@ -29,19 +37,31 @@ function StatCard({ icon: Icon, label, value, hint, tone = 'default' }: {
|
||||
hint?: string
|
||||
tone?: 'default' | 'warn' | 'good'
|
||||
}) {
|
||||
const toneClass = tone === 'warn' ? 'text-amber-600' : tone === 'good' ? 'text-emerald-600' : 'text-brand-600'
|
||||
const t = STAT_TONE[tone]
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-500">{label}</div>
|
||||
<Icon className={`h-4 w-4 ${toneClass}`} />
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4 transition-colors hover:border-slate-300">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-500">{label}</div>
|
||||
<span className={cn('flex h-7 w-7 shrink-0 items-center justify-center rounded-lg', t.chip)}>
|
||||
<Icon className={cn('h-4 w-4', t.icon)} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900">{value}</div>
|
||||
{hint && <div className="mt-0.5 text-xs text-slate-400">{hint}</div>}
|
||||
<div className={cn('mt-2.5 text-2xl font-semibold tabular-nums leading-none', t.value)}>{value}</div>
|
||||
{hint && <div className="mt-1.5 text-[11px] text-slate-400">{hint}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Small section title with a brand tick — gives each band a clear, quiet anchor.
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<h2 className="mb-2.5 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500">
|
||||
<span className="h-3 w-0.5 rounded-full bg-brand-500" />
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
function MyDashboardRow() {
|
||||
const navigate = useNavigate()
|
||||
const { user } = useAuth()
|
||||
@ -60,43 +80,51 @@ function MyDashboardRow() {
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<h2 className="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500">Của tôi</h2>
|
||||
<SectionLabel>Của tôi</SectionLabel>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<button
|
||||
onClick={() => navigate('/contracts?filter=my-drafts')}
|
||||
className="rounded-xl border border-slate-200 bg-white p-4 text-left shadow-sm transition hover:border-brand-300 hover:shadow"
|
||||
className="group rounded-lg border border-slate-200 bg-white p-4 text-left transition-colors hover:border-brand-300 hover:bg-brand-50/30"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-500">Đang soạn thảo</div>
|
||||
<Pencil className="h-4 w-4 text-brand-600" />
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-500">Đang soạn thảo</div>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-brand-50">
|
||||
<Pencil className="h-4 w-4 text-brand-600" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900 tabular-nums">{d.draftsInProgress}</div>
|
||||
<div className="mt-0.5 text-xs text-slate-400">{fmtMoney(d.draftsTotalValue)} VND</div>
|
||||
<div className="mt-2.5 text-2xl font-semibold leading-none text-slate-900 tabular-nums">{d.draftsInProgress}</div>
|
||||
<div className="mt-1.5 text-[11px] text-slate-400">{fmtMoney(d.draftsTotalValue)} VND</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/contracts?filter=pending-me')}
|
||||
className="rounded-xl border border-slate-200 bg-white p-4 text-left shadow-sm transition hover:border-brand-300 hover:shadow"
|
||||
className="group rounded-lg border border-slate-200 bg-white p-4 text-left transition-colors hover:border-brand-300 hover:bg-brand-50/30"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-500">Chờ tôi duyệt</div>
|
||||
<Inbox className="h-4 w-4 text-brand-600" />
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-500">Chờ tôi duyệt</div>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-brand-50">
|
||||
<Inbox className="h-4 w-4 text-brand-600" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900 tabular-nums">{d.pendingMyApproval}</div>
|
||||
<div className="mt-0.5 text-xs text-slate-400">Click để xem hộp thư</div>
|
||||
<div className="mt-2.5 text-2xl font-semibold leading-none text-slate-900 tabular-nums">{d.pendingMyApproval}</div>
|
||||
<div className="mt-1.5 text-[11px] text-slate-400">Mở hộp thư duyệt</div>
|
||||
</button>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-500">Sắp quá hạn (24h)</div>
|
||||
<Clock className="h-4 w-4 text-amber-600" />
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-500">Sắp quá hạn (24h)</div>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-amber-50">
|
||||
<Clock className="h-4 w-4 text-amber-600" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-amber-600 tabular-nums">{d.dueSoon}</div>
|
||||
<div className="mt-2.5 text-2xl font-semibold leading-none text-amber-700 tabular-nums">{d.dueSoon}</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-500">Đã quá hạn</div>
|
||||
<AlertTriangle className="h-4 w-4 text-red-600" />
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-500">Đã quá hạn</div>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-red-50">
|
||||
<AlertTriangle className="h-4 w-4 text-red-600" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-red-600 tabular-nums">{d.overdue}</div>
|
||||
<div className="mt-2.5 text-2xl font-semibold leading-none text-red-600 tabular-nums">{d.overdue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,10 +141,10 @@ export function DashboardPage() {
|
||||
if (stats.isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageHeader title="Tổng quan" />
|
||||
<PageHeader title="Tổng quan" description="Tình hình HĐ toàn hệ thống — cập nhật real-time khi refresh." />
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<div key={i} className="h-24 animate-pulse rounded-lg bg-slate-100" />
|
||||
<div key={i} className="h-[88px] animate-pulse rounded-lg border border-slate-200 bg-slate-50" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -132,7 +160,7 @@ export function DashboardPage() {
|
||||
|
||||
<MyDashboardRow />
|
||||
|
||||
<h2 className="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500">Toàn hệ thống</h2>
|
||||
<SectionLabel>Toàn hệ thống</SectionLabel>
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
||||
<StatCard icon={FileText} label="Tổng HĐ" value={d.totalContracts} />
|
||||
@ -142,12 +170,12 @@ export function DashboardPage() {
|
||||
<StatCard icon={Coins} label="Tổng giá trị active" value={fmtMoney(d.totalValueActive)} hint="VND" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{/* By Phase */}
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-4 text-sm font-semibold text-slate-700">HĐ theo phase</h2>
|
||||
<div className="space-y-2">
|
||||
{d.byPhase.length === 0 && <div className="py-6 text-center text-sm text-slate-400">Chưa có HĐ nào</div>}
|
||||
<h2 className="mb-4 text-[13px] font-semibold text-slate-800">HĐ theo phase</h2>
|
||||
<div className="space-y-2.5">
|
||||
{d.byPhase.length === 0 && <div className="py-6 text-center text-[13px] text-slate-400">Chưa có HĐ nào</div>}
|
||||
{d.byPhase
|
||||
.slice()
|
||||
.sort((a, b) => b.count - a.count)
|
||||
@ -159,10 +187,10 @@ export function DashboardPage() {
|
||||
<div className="w-36">
|
||||
<PhaseBadge phase={p.phase} />
|
||||
</div>
|
||||
<div className="h-2 flex-1 rounded-full bg-slate-100">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-100">
|
||||
<div className="h-full rounded-full bg-brand-500" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="w-12 text-right font-mono text-xs text-slate-600">{p.count}</div>
|
||||
<div className="w-10 text-right text-xs font-medium text-slate-600 tabular-nums">{p.count}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -171,7 +199,7 @@ export function DashboardPage() {
|
||||
|
||||
{/* Monthly value */}
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-4 text-sm font-semibold text-slate-700">Giá trị HĐ theo tháng (12 tháng gần nhất)</h2>
|
||||
<h2 className="mb-4 text-[13px] font-semibold text-slate-800">Giá trị HĐ theo tháng <span className="font-normal text-slate-400">· 12 tháng gần nhất</span></h2>
|
||||
<BarChart
|
||||
data={d.monthlyValue.map(m => ({
|
||||
label: `${String(m.month).padStart(2, '0')}/${m.year}`,
|
||||
@ -184,7 +212,7 @@ export function DashboardPage() {
|
||||
|
||||
{/* Top suppliers */}
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-4 text-sm font-semibold text-slate-700">Top NCC theo số HĐ</h2>
|
||||
<h2 className="mb-4 text-[13px] font-semibold text-slate-800">Top NCC theo số HĐ</h2>
|
||||
<BarChart
|
||||
data={d.topSuppliers.map(s => ({
|
||||
label: s.supplierName,
|
||||
@ -196,7 +224,7 @@ export function DashboardPage() {
|
||||
|
||||
{/* Top projects */}
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-4 text-sm font-semibold text-slate-700">Top dự án theo số HĐ</h2>
|
||||
<h2 className="mb-4 text-[13px] font-semibold text-slate-800">Top dự án theo số HĐ</h2>
|
||||
<BarChart
|
||||
data={d.topProjects.map(p => ({
|
||||
label: p.projectName,
|
||||
|
||||
Reference in New Issue
Block a user