[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

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:
pqhuy1987
2026-06-17 09:57:46 +07:00
parent a8bbdaeeea
commit c556f6cfa2
22 changed files with 1907 additions and 929 deletions

View File

@ -6,9 +6,9 @@
// Pattern 16-bis 4-place mirror foundation (7× cumulative).
import { useMemo, useState, type FormEvent } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Clock, MapPin, Plus, Trash2, Users as UsersIcon, X } from 'lucide-react'
import { ChevronLeft, ChevronRight, CalendarDays, Clock, MapPin, Plus, Trash2, Users as UsersIcon, X } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader'
import { PageHeader } from '@/components/ui/PageHeader'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
@ -259,13 +259,11 @@ export function MeetingCalendarPage() {
return (
<div className="p-6">
<PageHeader
title={
<span className="flex items-center gap-2">
<CalendarIcon className="h-5 w-5" />
Đt phòng họp
</span>
}
description={bookings.isLoading ? 'Đang tải…' : `${totalThisWeek} booking tuần này`}
eyebrow="Văn phòng số"
title="Lịch phòng họp"
subtitle={bookings.isLoading ? 'Đang tải…' : `${totalThisWeek} booking tuần này`}
icon={<CalendarDays className="h-5 w-5" />}
accent="amberx"
actions={
<Button onClick={openCreateBlank}>
<Plus className="h-4 w-4" />
@ -304,7 +302,10 @@ export function MeetingCalendarPage() {
</div>
{/* Calendar grid */}
<div className="overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm">
<div
className="card-accent overflow-x-auto"
style={{ ['--accent' as string]: 'var(--color-amberx-500)' }}
>
<div className="min-w-[900px]">
{/* Header row: 7 days */}
<div className="sticky top-0 z-10 grid grid-cols-[60px_repeat(7,minmax(0,1fr))] border-b border-slate-200 bg-slate-50">
@ -526,52 +527,69 @@ export function MeetingCalendarPage() {
>
{detailBooking && (
<div className="space-y-3 text-sm">
<div>
<h3 className="text-base font-semibold text-slate-900">{detailBooking.title}</h3>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs">
<span className={cn('rounded-full px-2 py-0.5', statusBadgeClass(detailBooking.status))}>
{statusLabel(detailBooking.status)}
</span>
<span className="text-slate-500">
Người tạo: <span className="font-medium text-slate-700">{detailBooking.bookedByFullName}</span>
</span>
<div className="flex items-start gap-3">
<span
className="icon-chip mt-0.5 shrink-0"
style={{ ['--chip-bg' as string]: 'var(--color-amberx-50)', ['--chip-fg' as string]: 'var(--color-amberx-700)' }}
aria-hidden
>
<CalendarDays className="h-4 w-4" />
</span>
<div className="min-w-0">
<h3 className="text-base font-semibold tracking-tight text-brand-800">{detailBooking.title}</h3>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs">
<span className={cn('rounded-full px-2 py-0.5', statusBadgeClass(detailBooking.status))}>
{statusLabel(detailBooking.status)}
</span>
<span className="text-slate-500">
Người tạo: <span className="font-medium text-brand-800">{detailBooking.bookedByFullName}</span>
</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2 rounded-md bg-slate-50 p-3 sm:grid-cols-2">
<div className="flex items-center gap-2 text-slate-700">
<MapPin className="h-4 w-4 text-slate-400" />
<span>{detailBooking.roomCode} {detailBooking.roomName}</span>
<div className="grid grid-cols-1 gap-3 rounded-xl border border-slate-200 bg-slate-50/70 p-3.5 sm:grid-cols-2">
<div className="min-w-0">
<div className="label-eyebrow flex items-center gap-1">
<MapPin className="h-3 w-3" />
Phòng họp
</div>
<div className="mt-0.5 break-words font-medium text-brand-800">
{detailBooking.roomCode} {detailBooking.roomName}
</div>
</div>
<div className="flex items-center gap-2 text-slate-700">
<Clock className="h-4 w-4 text-slate-400" />
<span>
<div className="min-w-0">
<div className="label-eyebrow flex items-center gap-1">
<Clock className="h-3 w-3" />
Thời gian
</div>
<div className="mt-0.5 break-words font-medium text-brand-800">
{new Date(detailBooking.startAt).toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })}
{' '}
{new Date(detailBooking.endAt).toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })}
</span>
</div>
</div>
</div>
{detailBooking.description && (
<div>
<Label className="text-xs text-slate-500"> tả</Label>
<div className="label-eyebrow"> tả</div>
<p className="mt-1 whitespace-pre-wrap text-sm text-slate-700">{detailBooking.description}</p>
</div>
)}
{detailBooking.note && (
<div>
<Label className="text-xs text-slate-500">Ghi chú</Label>
<div className="label-eyebrow">Ghi chú</div>
<p className="mt-1 text-sm text-slate-700">{detailBooking.note}</p>
</div>
)}
<div>
<Label className="flex items-center gap-1 text-xs text-slate-500">
<UsersIcon className="h-3.5 w-3.5" />
<div className="label-eyebrow flex items-center gap-1">
<UsersIcon className="h-3 w-3" />
Người tham dự ({detailBooking.attendees.length})
</Label>
</div>
{detailBooking.attendees.length === 0 ? (
<p className="mt-1 text-xs text-slate-400"> Không người tham dự </p>
) : (