[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:
@ -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">Mô tả</Label>
|
||||
<div className="label-eyebrow">Mô 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 có người tham dự —</p>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user