[CLAUDE] FE: Văn phòng số re-skin toàn module — 10 page PURO layout + CSS Hồ sơ NS (PageHeader/KpiCard/WidgetCard), phẫu thuật giữ nguyên logic
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m42s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m42s
Re-skin TRỌN module Office sang bố cục PURO (NamGroup) + ngôn ngữ thị giác Hồ sơ Nhân sự, tái dùng 3 shared component foundation. Phẫu thuật trình bày — logic byte-identical (reviewer verify mọi api.get/post/put/delete + queryKey zero-delta HEAD vs working tree, cả 2 app build PASS). 10 page (9 fe-user → mirror fe-admin SHA256-identical + AttendanceReport fe-admin-only): - Danh bạ nội bộ — PageHeader + KpiCard tổng hợp (NV/phòng ban) + card icon-chip. - Phòng họp (lịch + quản lý phòng) — PageHeader amberx + calendar/table trong card-accent. - Đề xuất (List/Create/Detail) — List: status filter → KpiCard row (6 trạng thái + inbox "Cần tôi duyệt"); Create/Detail: card-accent section + Field idiom. - Đơn từ/Đặt xe (List/Detail, :kind leave/ot/travel/vehicle) — PageHeader teal + KpiCard status filter (client-side view over fetched) + card-accent detail. - Ticket CNTT — PageHeader violet + KpiCard 5-status filter + Quá hạn SLA + kanban card-accent. - Báo cáo chấm công (fe-admin only) — PageHeader + KpiCard tổng hợp + bảng card-accent + Excel-export giữ nguyên. - Accent chỉ dùng stop hợp lệ (teal/violet/amberx/greenx 50/100/500/600/700; brand 50-900); gotcha #66 clean. a11y giữ/nâng (focus-visible, KpiCard role/aria-pressed/keyboard). - Build PASS x2 (fe-user index-C8-p69Kn / fe-admin index-yFhLO2Wp). reviewer PASS 0 blocker; 2 concern cosmetic (badge dup ProposalDetail header+row; KpiCard filter lọc trên trang đầu đã fetch — giới hạn pagination có sẵn). - Office VẪN ẨN non-Admin (chưa golive). 9 page fe-user↔fe-admin SHA256-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,15 +1,20 @@
|
||||
// Generic Workflow Apps List Page — Phase 10.3 G-O4+G-O5+G-O6 (S38 2026-05-28).
|
||||
// Wave 3a (S42 2026-05-30): row click → Detail page (workflow actions + opinion timeline).
|
||||
// Re-skin S69 (2026-06-17): PURO layout (ui/PageHeader teal + KpiCard status-filter row)
|
||||
// + Hồ sơ Nhân sự visual language (accent rail card, slate table chrome). Status filter is
|
||||
// a CLIENT-SIDE view over the already-fetched items (no query/endpoint/navigation change).
|
||||
// Handles 4 module via URL `:kind` param: leave / ot / travel / vehicle.
|
||||
// File MIRROR SHA256 identical fe-user counterpart.
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { CalendarOff, Clock, Plane, Car, FileSignature } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { CalendarOff, Clock, Plane, Car, FileSignature, Layers, Send, RotateCcw, CheckCircle2 } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/ui/PageHeader'
|
||||
import { KpiCard } from '@/components/ui/KpiCard'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/cn'
|
||||
import {
|
||||
WORKFLOW_APP_STATUS_BADGE, WORKFLOW_APP_STATUS_LABELS,
|
||||
WORKFLOW_APP_STATUS_BADGE, WORKFLOW_APP_STATUS_LABELS, WorkflowAppStatus,
|
||||
type PagedResult,
|
||||
} from '@/types/workflowApps'
|
||||
|
||||
@ -78,12 +83,18 @@ const ICON_MAP: Record<Kind, any> = {
|
||||
leave: CalendarOff, ot: Clock, travel: Plane, vehicle: Car,
|
||||
}
|
||||
|
||||
// Status filter chips (presentation): null = Tất cả. Each maps to a KpiCard accent.
|
||||
type StatusFilter = number | null
|
||||
|
||||
export function WorkflowAppsListPage() {
|
||||
const { kind = 'leave' } = useParams<{ kind: Kind }>()
|
||||
const navigate = useNavigate()
|
||||
const config = KIND_CONFIG[kind as Kind]
|
||||
const Icon = ICON_MAP[kind as Kind] ?? FileSignature
|
||||
|
||||
// Client-side status filter — a view over the fetched list (no extra query / endpoint).
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>(null)
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: [config.endpoint, { page: 1 }],
|
||||
queryFn: async () => (await api.get<PagedResult<any>>(config.endpoint, { params: { page: 1, pageSize: 50 } })).data,
|
||||
@ -92,50 +103,108 @@ export function WorkflowAppsListPage() {
|
||||
|
||||
const items = list.data?.items ?? []
|
||||
|
||||
// Counts per status + the visible (filtered) rows — derived presentation only.
|
||||
const counts = useMemo(() => {
|
||||
const c = { all: items.length, submitted: 0, returned: 0, approved: 0 }
|
||||
for (const it of items) {
|
||||
if (it.status === WorkflowAppStatus.DaGuiDuyet) c.submitted++
|
||||
else if (it.status === WorkflowAppStatus.TraLai) c.returned++
|
||||
else if (it.status === WorkflowAppStatus.DaDuyet) c.approved++
|
||||
}
|
||||
return c
|
||||
}, [items])
|
||||
|
||||
const visibleItems = useMemo(
|
||||
() => (statusFilter == null ? items : items.filter((it: any) => it.status === statusFilter)),
|
||||
[items, statusFilter],
|
||||
)
|
||||
|
||||
if (!config) {
|
||||
return <div className="text-red-600">Module không tồn tại: {kind}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader title={config.title} description={config.description} />
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
eyebrow="Văn phòng số · Đơn từ"
|
||||
title={config.title}
|
||||
subtitle={config.description}
|
||||
icon={<Icon className="h-5 w-5" />}
|
||||
accent="teal"
|
||||
/>
|
||||
|
||||
<div className="rounded-lg border bg-card">
|
||||
{/* Status filter — row of KpiCards (PURO pattern, replaces tabs) */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<KpiCard
|
||||
label="Tất cả"
|
||||
value={counts.all}
|
||||
icon={<Layers className="h-4 w-4" />}
|
||||
accent="teal"
|
||||
active={statusFilter == null}
|
||||
onClick={() => setStatusFilter(null)}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Đã gửi duyệt"
|
||||
value={counts.submitted}
|
||||
icon={<Send className="h-4 w-4" />}
|
||||
accent="amberx"
|
||||
active={statusFilter === WorkflowAppStatus.DaGuiDuyet}
|
||||
onClick={() => setStatusFilter(WorkflowAppStatus.DaGuiDuyet)}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Trả lại"
|
||||
value={counts.returned}
|
||||
icon={<RotateCcw className="h-4 w-4" />}
|
||||
accent="violet"
|
||||
active={statusFilter === WorkflowAppStatus.TraLai}
|
||||
onClick={() => setStatusFilter(WorkflowAppStatus.TraLai)}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Đã duyệt"
|
||||
value={counts.approved}
|
||||
icon={<CheckCircle2 className="h-4 w-4" />}
|
||||
accent="greenx"
|
||||
active={statusFilter === WorkflowAppStatus.DaDuyet}
|
||||
onClick={() => setStatusFilter(WorkflowAppStatus.DaDuyet)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b bg-muted/50">
|
||||
<thead className="border-b border-slate-200 bg-slate-50">
|
||||
<tr>
|
||||
{config.columns.map((c) => (
|
||||
<th key={c.key} className="px-4 py-2 text-left font-medium">{c.label}</th>
|
||||
<th key={c.key} className="px-4 py-2.5 text-left text-[11px] font-semibold uppercase tracking-wide text-slate-500">{c.label}</th>
|
||||
))}
|
||||
<th className="px-4 py-2 text-left font-medium">Trạng thái</th>
|
||||
<th className="px-4 py-2.5 text-left text-[11px] font-semibold uppercase tracking-wide text-slate-500">Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.isLoading && (
|
||||
<tr>
|
||||
<td colSpan={config.columns.length + 1} className="px-4 py-8 text-center text-muted-foreground">
|
||||
<td colSpan={config.columns.length + 1} className="px-4 py-10 text-center text-slate-400">
|
||||
Đang tải...
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!list.isLoading && items.length === 0 && (
|
||||
{!list.isLoading && visibleItems.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={config.columns.length + 1} className="px-4 py-8 text-center text-muted-foreground">
|
||||
<Icon className="mx-auto h-8 w-8 mb-2 opacity-50" />
|
||||
Chưa có dữ liệu.
|
||||
<td colSpan={config.columns.length + 1} className="px-4 py-12 text-center text-slate-400">
|
||||
<Icon className="mx-auto mb-2 h-8 w-8 opacity-40" />
|
||||
{items.length === 0 ? 'Chưa có dữ liệu.' : 'Không có đơn nào ở trạng thái này.'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map((item: any) => (
|
||||
{visibleItems.map((item: any) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="border-b cursor-pointer hover:bg-muted/40"
|
||||
className="cursor-pointer border-b border-slate-100 transition hover:bg-teal-50/50"
|
||||
onClick={() => navigate(`/workflow-apps/${kind}/${item.id}`)}
|
||||
>
|
||||
{config.columns.map((c) => (
|
||||
<td key={c.key} className="px-4 py-2">{c.render(item)}</td>
|
||||
<td key={c.key} className="px-4 py-2.5 text-slate-700">{c.render(item)}</td>
|
||||
))}
|
||||
<td className="px-4 py-2">
|
||||
<td className="px-4 py-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium',
|
||||
|
||||
Reference in New Issue
Block a user