[CLAUDE] FE-User: Hồ sơ Nhân sự — layout 2 cột (tree+list trái, detail phải) + tô màu chi tiết
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m19s

Anh góp ý từ eoffice live (3 việc): (1) layout 2 cột giống NamGroup — cột trái dọc =
Cây tổ chức (trên) + Danh sách NV (dưới) chồng nhau, cột phải = chi tiết rộng (panel
list chuyển từ giữa xuống dưới tree); (2)+(3) tô màu panel chi tiết — hệ accent 5 tone
(brand/teal/violet/amberx/greenx) icon-chip + heading -700 + rail màu + nhãn field
brand-tint (trước slate-400 đơn điệu). Responsive <lg = 1 cột.
GIỮ 100%: 5 satellite CRUD (16 endpoint), cây SOLUTION COMPANY, 5 tab, search/filter,
query keys (grep + tsc verified). Build PASS fe-user. Designer tự bắt 2 bug (accent -800
không tồn tại palette -> -700; rail before:content). fe-admin mirror defer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 11:35:31 +07:00
parent ec517f7174
commit 456c7a721b

View File

@ -1,10 +1,15 @@
// List + Detail Hồ sơ Nhân sự (HRM) — 3-panel master-detail: // List + Detail Hồ sơ Nhân sự (HRM) — 2-panel master-detail (NamGroup eoffice ref):
// [Cây tổ chức] | [Danh sách + filter] | [Chi tiết 5 tab]. // CỘT TRÁI dọc (hẹp): [Cây tổ chức] (trên) + [Danh sách + filter] (dưới), cuộn độc lập.
// Redesign S65 (2026-06-16) theo reference NamGroup: org-tree panel (consume // CỘT PHẢI (rộng): [Chi tiết 5 tab] — avatar header + Tổng quan / Thân nhân /
// GET /api/departments/tree) + avatar header lớn + 5 tab (Tổng quan / Thân nhân // Trình độ / Kinh nghiệm / Hợp đồng.
// / Trình độ / Kinh nghiệm / Hợp đồng). KHÔNG đổi logic — 100% chức năng giữ: // Refine S66 (2026-06-16) theo anh góp ý từ eoffice LIVE (3 việc):
// 5 satellite inline CRUD (add/edit/delete + mutex), search, filter, mọi // 1. layout 3-cột-ngang → 2-cột (tree+list xếp chồng cột trái · detail rộng phải).
// TanStack query/mutation key NGUYÊN. Đây là RESTRUCTURE layout, không xoá logic. // 2+3. tô màu panel chi tiết: section header có accent (icon-chip nền màu nhạt +
// heading đậm) + nhãn field brand-tint, dùng palette teal/violet/amberx/greenx.
// GIỮ brand #1F7DC1 + Be Vietnam Pro · avatar header gradient brand giữ.
// KHÔNG đổi logic — 100% chức năng giữ: 5 satellite inline CRUD (add/edit/delete +
// mutex), search, filter, cây gốc "SOLUTION COMPANY" + TreeNode đệ quy, mọi TanStack
// query/mutation key NGUYÊN (employees-list / employee-detail / departments-tree-hrm).
// URL params: id (selected), q (search), status, deptId. // URL params: id (selected), q (search), status, deptId.
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
@ -135,7 +140,7 @@ export function EmployeesListPage() {
setSp(new URLSearchParams(), { replace: true }) setSp(new URLSearchParams(), { replace: true })
} }
// Name of the currently filtered department (for the middle-panel subtitle). // Name of the currently filtered department (for the list-panel subtitle).
const selectedDeptName = useMemo(() => { const selectedDeptName = useMemo(() => {
if (!deptFilter || !tree.data) return null if (!deptFilter || !tree.data) return null
let found: string | null = null let found: string | null = null
@ -156,17 +161,20 @@ export function EmployeesListPage() {
} }
return ( return (
<div className="grid h-full grid-cols-1 gap-4 p-4 lg:grid-cols-[244px_352px_1fr]"> // 2-column shell: left rail (tree + list stacked) | right detail (flex-1).
{/* ===================== PANEL 1 — Org tree ===================== */} // <lg: single column (tree → list → detail), tree collapses to a toggle.
<div className="grid h-full grid-cols-1 gap-4 p-4 lg:grid-cols-[22rem_1fr] xl:grid-cols-[24rem_1fr]">
{/* ===================== LEFT RAIL — tree (top) + list (bottom) ===================== */}
<div className="flex min-h-0 flex-col gap-4">
{/* ---- Org tree (cố định cao theo nội dung, tối đa ~44% rail) ---- */}
<aside <aside
className={cn( className={cn(
'flex min-h-0 flex-col rounded-xl border border-slate-200 bg-white shadow-sm', 'flex min-h-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm',
// collapse to a slide-down on narrow screens 'lg:!flex lg:max-h-[44%] lg:shrink-0',
'lg:!block', treeOpenMobile ? 'flex' : 'hidden',
treeOpenMobile ? 'block' : 'hidden',
)} )}
> >
<header className="flex items-center gap-2 border-b border-slate-100 px-4 py-3"> <header className="flex shrink-0 items-center gap-2 border-b border-slate-100 px-4 py-3">
<span className="icon-chip" style={{ height: '2rem', width: '2rem' }}> <span className="icon-chip" style={{ height: '2rem', width: '2rem' }}>
<Building2 className="h-4 w-4" /> <Building2 className="h-4 w-4" />
</span> </span>
@ -229,8 +237,8 @@ export function EmployeesListPage() {
</div> </div>
</aside> </aside>
{/* ===================== PANEL 2 — list + filter ===================== */} {/* ---- List + filter (chiếm phần còn lại của rail, cuộn riêng) ---- */}
<section className="flex min-h-0 flex-col gap-3"> <section className="flex min-h-0 flex-1 flex-col gap-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<h2 className="truncate text-base font-semibold tracking-tight text-slate-900">Hồ Nhân sự</h2> <h2 className="truncate text-base font-semibold tracking-tight text-slate-900">Hồ Nhân sự</h2>
@ -240,7 +248,7 @@ export function EmployeesListPage() {
<span className="font-medium text-slate-700">{list.data?.total ?? 0}</span> hồ <span className="font-medium text-slate-700">{list.data?.total ?? 0}</span> hồ
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
{/* mobile-only tree toggle */} {/* mobile-only tree toggle */}
<Button <Button
type="button" type="button"
@ -341,8 +349,9 @@ export function EmployeesListPage() {
)} )}
</div> </div>
</section> </section>
</div>
{/* ===================== PANEL 3 — detail (5 tab) ===================== */} {/* ===================== RIGHT — detail (5 tab) ===================== */}
<section className="flex min-h-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"> <section className="flex min-h-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
{!selectedId ? ( {!selectedId ? (
<div className="flex h-full flex-col items-center justify-center gap-2 p-10 text-center"> <div className="flex h-full flex-col items-center justify-center gap-2 p-10 text-center">
@ -485,6 +494,24 @@ function StatusBadge({ status }: { status: number }) {
) )
} }
// ===================== Section accent system (việc 2+3) =====================
// Mỗi section nhận MỘT accent từ palette (teal/violet/amberx/greenx + brand). Accent
// tô icon-chip (nền nhạt + chữ đậm), heading, và rail trái — tinh tế, KHÔNG loè loẹt.
// Mọi cặp bg-{x}-50 + text-{x}-700 đã đạt WCAG-AA (xem index.css §ACCENT PALETTE).
type Accent = 'brand' | 'teal' | 'violet' | 'amberx' | 'greenx'
// NOTE: accent palettes (teal/violet/amberx/greenx) ship stops 50/100/500/600/700
// ONLY — no -800 (see index.css §ACCENT PALETTE). So headings use -700 across the
// board (all clear WCAG-AA on white). Using a non-existent stop would silently emit
// no class in Tailwind v4 → uncolored heading.
const ACCENT: Record<Accent, { chipBg: string; chipFg: string; head: string; rail: string; labelText: string }> = {
brand: { chipBg: 'var(--color-brand-50)', chipFg: 'var(--color-brand-600)', head: 'text-brand-700', rail: 'before:bg-brand-500', labelText: 'text-brand-700' },
teal: { chipBg: 'var(--color-teal-50)', chipFg: 'var(--color-teal-700)', head: 'text-teal-700', rail: 'before:bg-teal-500', labelText: 'text-teal-700' },
violet: { chipBg: 'var(--color-violet-50)', chipFg: 'var(--color-violet-700)', head: 'text-violet-700', rail: 'before:bg-violet-500', labelText: 'text-violet-700' },
amberx: { chipBg: 'var(--color-amberx-50)', chipFg: 'var(--color-amberx-700)', head: 'text-amberx-700', rail: 'before:bg-amberx-500', labelText: 'text-amberx-700' },
greenx: { chipBg: 'var(--color-greenx-50)', chipFg: 'var(--color-greenx-700)', head: 'text-greenx-700', rail: 'before:bg-greenx-500', labelText: 'text-greenx-700' },
}
// ===================== Detail with 5 tabs + satellite CRUD ===================== // ===================== Detail with 5 tabs + satellite CRUD =====================
const TABS = [ const TABS = [
@ -700,6 +727,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
<Card <Card
title="Quan hệ gia đình" title="Quan hệ gia đình"
icon={Users} icon={Users}
accent="violet"
count={detail.familyRelations.length} count={detail.familyRelations.length}
action={ action={
<Button size="sm" variant="outline" onClick={() => { setAddingFamilyRelation(true); setEditingFamilyRelationId(null) }} disabled={addingFamilyRelation}> <Button size="sm" variant="outline" onClick={() => { setAddingFamilyRelation(true); setEditingFamilyRelationId(null) }} disabled={addingFamilyRelation}>
@ -751,6 +779,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
<Card <Card
title="Quá trình đào tạo" title="Quá trình đào tạo"
icon={GraduationCap} icon={GraduationCap}
accent="teal"
count={detail.educations.length} count={detail.educations.length}
action={ action={
<Button size="sm" variant="outline" onClick={() => { setAddingEducation(true); setEditingEducationId(null) }} disabled={addingEducation}> <Button size="sm" variant="outline" onClick={() => { setAddingEducation(true); setEditingEducationId(null) }} disabled={addingEducation}>
@ -808,6 +837,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
<Card <Card
title="Kỹ năng" title="Kỹ năng"
icon={ShieldCheck} icon={ShieldCheck}
accent="greenx"
count={detail.skills.length} count={detail.skills.length}
action={ action={
<Button size="sm" variant="outline" onClick={() => { setAddingSkill(true); setEditingSkillId(null) }} disabled={addingSkill}> <Button size="sm" variant="outline" onClick={() => { setAddingSkill(true); setEditingSkillId(null) }} disabled={addingSkill}>
@ -872,6 +902,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
<Card <Card
title="Quá trình công tác" title="Quá trình công tác"
icon={Briefcase} icon={Briefcase}
accent="amberx"
count={detail.workHistories.length} count={detail.workHistories.length}
action={ action={
<Button size="sm" variant="outline" onClick={() => { setAddingWorkHistory(true); setEditingWorkHistoryId(null) }} disabled={addingWorkHistory}> <Button size="sm" variant="outline" onClick={() => { setAddingWorkHistory(true); setEditingWorkHistoryId(null) }} disabled={addingWorkHistory}>
@ -928,6 +959,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
<Card <Card
title="Hồ sơ & hợp đồng giấy tờ" title="Hồ sơ & hợp đồng giấy tờ"
icon={FileText} icon={FileText}
accent="brand"
count={detail.documents.length} count={detail.documents.length}
action={ action={
<Button size="sm" variant="outline" onClick={() => { setAddingDocument(true); setEditingDocumentId(null) }} disabled={addingDocument}> <Button size="sm" variant="outline" onClick={() => { setAddingDocument(true); setEditingDocumentId(null) }} disabled={addingDocument}>
@ -996,114 +1028,115 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe
} }
// ===================== Overview tab (2-column layout) ===================== // ===================== Overview tab (2-column layout) =====================
// Mỗi card mang một accent từ palette → section header có màu rõ, dễ scan, tinh tế.
function OverviewTab({ detail }: { detail: EmployeeDetail }) { function OverviewTab({ detail }: { detail: EmployeeDetail }) {
return ( return (
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2"> <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
{/* LEFT column */} {/* LEFT column */}
<div className="space-y-4"> <div className="space-y-4">
<Card title="Thông tin chung" icon={IdCard}> <Card title="Thông tin chung" icon={IdCard} accent="brand">
<Grid2> <Grid2>
<Field label="Ngày sinh" value={fmtDate(detail.dateOfBirth)} icon={CalendarDays} /> <Field label="Ngày sinh" value={fmtDate(detail.dateOfBirth)} icon={CalendarDays} accent="brand" />
<Field label="Giới tính" value={detail.gender != null ? GenderLabel[detail.gender] : null} /> <Field label="Giới tính" value={detail.gender != null ? GenderLabel[detail.gender] : null} accent="brand" />
<Field label="Tình trạng hôn nhân" value={detail.maritalStatus != null ? MaritalStatusLabel[detail.maritalStatus] : null} /> <Field label="Tình trạng hôn nhân" value={detail.maritalStatus != null ? MaritalStatusLabel[detail.maritalStatus] : null} accent="brand" />
<Field label="Dân tộc" value={detail.ethnicity} /> <Field label="Dân tộc" value={detail.ethnicity} accent="brand" />
<Field label="Tôn giáo" value={detail.religion} /> <Field label="Tôn giáo" value={detail.religion} accent="brand" />
<Field label="Quốc tịch" value={detail.nationality} /> <Field label="Quốc tịch" value={detail.nationality} accent="brand" />
<Field label="Nơi sinh" value={detail.birthPlace} /> <Field label="Nơi sinh" value={detail.birthPlace} accent="brand" />
<Field label="Quê quán" value={detail.hometown} /> <Field label="Quê quán" value={detail.hometown} accent="brand" />
<Field label="SĐT" value={detail.phone} icon={Phone} /> <Field label="SĐT" value={detail.phone} icon={Phone} accent="brand" />
<Field label="SĐT nội bộ" value={detail.internalPhone} /> <Field label="SĐT nội bộ" value={detail.internalPhone} accent="brand" />
<Field label="Email" value={detail.email} icon={Mail} /> <Field label="Email" value={detail.email} icon={Mail} accent="brand" />
<Field label="Email cá nhân" value={detail.personalEmail} /> <Field label="Email cá nhân" value={detail.personalEmail} accent="brand" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Sức khoẻ" icon={HeartPulse}> <Card title="Sức khoẻ" icon={HeartPulse} accent="greenx">
<Grid2> <Grid2>
<Field label="Chiều cao" value={detail.heightCm != null ? `${detail.heightCm} cm` : null} /> <Field label="Chiều cao" value={detail.heightCm != null ? `${detail.heightCm} cm` : null} accent="greenx" />
<Field label="Cân nặng" value={detail.weightKg != null ? `${detail.weightKg} kg` : null} /> <Field label="Cân nặng" value={detail.weightKg != null ? `${detail.weightKg} kg` : null} accent="greenx" />
<Field label="Nhóm máu" value={detail.bloodType} /> <Field label="Nhóm máu" value={detail.bloodType} accent="greenx" />
<Field label="Nơi đăng ký KCB" value={detail.medicalRegistrationPlace} /> <Field label="Nơi đăng ký KCB" value={detail.medicalRegistrationPlace} accent="greenx" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Liên hệ & địa chỉ" icon={MapPin}> <Card title="Liên hệ & địa chỉ" icon={MapPin} accent="teal">
<Grid2> <Grid2>
<Field label="Thường trú" value={detail.permanentAddressText} /> <Field label="Thường trú" value={detail.permanentAddressText} accent="teal" />
<Field label="Số nhà / Đường (Thường trú)" value={detail.streetAddressPermanent} /> <Field label="Số nhà / Đường (Thường trú)" value={detail.streetAddressPermanent} accent="teal" />
<Field label="Tạm trú" value={detail.temporaryAddressText} /> <Field label="Tạm trú" value={detail.temporaryAddressText} accent="teal" />
<Field label="Số nhà / Đường (Tạm trú)" value={detail.streetAddressTemporary} /> <Field label="Số nhà / Đường (Tạm trú)" value={detail.streetAddressTemporary} accent="teal" />
<Field label="Liên hệ khẩn cấp" value={detail.emergencyContactName} /> <Field label="Liên hệ khẩn cấp" value={detail.emergencyContactName} accent="teal" />
<Field label="SĐT khẩn cấp" value={detail.emergencyContactPhone} /> <Field label="SĐT khẩn cấp" value={detail.emergencyContactPhone} accent="teal" />
<Field label="Địa chỉ khẩn cấp" value={detail.emergencyContactAddress} full /> <Field label="Địa chỉ khẩn cấp" value={detail.emergencyContactAddress} full accent="teal" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Giấy tờ tuỳ thân" icon={IdCard}> <Card title="Giấy tờ tuỳ thân" icon={IdCard} accent="violet">
<Grid2> <Grid2>
<Field label="CMND/CCCD" value={detail.idCardNumber} mono /> <Field label="CMND/CCCD" value={detail.idCardNumber} mono accent="violet" />
<Field label="Ngày cấp" value={fmtDate(detail.idCardIssueDate)} /> <Field label="Ngày cấp" value={fmtDate(detail.idCardIssueDate)} accent="violet" />
<Field label="Nơi cấp" value={detail.idCardIssuePlace} full /> <Field label="Nơi cấp" value={detail.idCardIssuePlace} full accent="violet" />
<Field label="Hộ chiếu" value={detail.passportNumber} mono /> <Field label="Hộ chiếu" value={detail.passportNumber} mono accent="violet" />
<Field label="MST cá nhân" value={detail.taxCode} mono /> <Field label="MST cá nhân" value={detail.taxCode} mono accent="violet" />
<Field label="Số BHXH" value={detail.socialInsuranceNumber} mono /> <Field label="Số BHXH" value={detail.socialInsuranceNumber} mono accent="violet" />
</Grid2> </Grid2>
</Card> </Card>
</div> </div>
{/* RIGHT column */} {/* RIGHT column */}
<div className="space-y-4"> <div className="space-y-4">
<Card title="Công việc & chế độ" icon={Briefcase}> <Card title="Công việc & chế độ" icon={Briefcase} accent="amberx">
<Grid2> <Grid2>
<Field label="Mã NV" value={detail.employeeCode} mono /> <Field label="Mã NV" value={detail.employeeCode} mono accent="amberx" />
<Field label="Loại NV" value={detail.employeeType != null ? EmployeeTypeLabel[detail.employeeType] : null} /> <Field label="Loại NV" value={detail.employeeType != null ? EmployeeTypeLabel[detail.employeeType] : null} accent="amberx" />
<Field label="Phòng ban" value={detail.departmentName} /> <Field label="Phòng ban" value={detail.departmentName} accent="amberx" />
<Field label="Vị trí công tác" value={detail.workLocation} /> <Field label="Vị trí công tác" value={detail.workLocation} accent="amberx" />
<Field label="Ngày vào làm" value={fmtDate(detail.hireDate)} icon={CalendarDays} /> <Field label="Ngày vào làm" value={fmtDate(detail.hireDate)} icon={CalendarDays} accent="amberx" />
<Field label="Ngày nghỉ việc" value={fmtDate(detail.resignDate)} /> <Field label="Ngày nghỉ việc" value={fmtDate(detail.resignDate)} accent="amberx" />
<Field label="Mã chấm công" value={detail.timekeepingCode} mono /> <Field label="Mã chấm công" value={detail.timekeepingCode} mono accent="amberx" />
<Field label="Trình độ chuyên môn" value={detail.qualification} /> <Field label="Trình độ chuyên môn" value={detail.qualification} accent="amberx" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Lương & bảo hiểm" icon={Wallet}> <Card title="Lương & bảo hiểm" icon={Wallet} accent="greenx">
<Grid2> <Grid2>
<Field label="Lương cơ bản" value={fmtMoney(detail.baseSalary)} /> <Field label="Lương cơ bản" value={fmtMoney(detail.baseSalary)} accent="greenx" />
<Field label="Tổng lương" value={fmtMoney(detail.totalSalary)} /> <Field label="Tổng lương" value={fmtMoney(detail.totalSalary)} accent="greenx" />
<Field label="BHXH bắt đầu" value={fmtDate(detail.socialInsuranceStartDate)} /> <Field label="BHXH bắt đầu" value={fmtDate(detail.socialInsuranceStartDate)} accent="greenx" />
<Field label="Số BHXH" value={detail.socialInsuranceNumber} mono /> <Field label="Số BHXH" value={detail.socialInsuranceNumber} mono accent="greenx" />
</Grid2> </Grid2>
<SubLabel>Ngày phép</SubLabel> <SubLabel accent="greenx">Ngày phép</SubLabel>
<Grid2> <Grid2>
<Field label="Phép năm" value={detail.annualLeaveDays != null ? `${detail.annualLeaveDays} ngày` : null} /> <Field label="Phép năm" value={detail.annualLeaveDays != null ? `${detail.annualLeaveDays} ngày` : null} accent="greenx" />
<Field label="Phép còn lại" value={detail.remainingLeaveDays != null ? `${detail.remainingLeaveDays} ngày` : null} /> <Field label="Phép còn lại" value={detail.remainingLeaveDays != null ? `${detail.remainingLeaveDays} ngày` : null} accent="greenx" />
<Field label="Phép bù" value={detail.compensatoryLeaveDays != null ? `${detail.compensatoryLeaveDays} ngày` : null} /> <Field label="Phép bù" value={detail.compensatoryLeaveDays != null ? `${detail.compensatoryLeaveDays} ngày` : null} accent="greenx" />
<Field label="Phép thâm niên" value={detail.seniorityLeaveDays != null ? `${detail.seniorityLeaveDays} ngày` : null} /> <Field label="Phép thâm niên" value={detail.seniorityLeaveDays != null ? `${detail.seniorityLeaveDays} ngày` : null} accent="greenx" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Ngân hàng" icon={Landmark}> <Card title="Ngân hàng" icon={Landmark} accent="teal">
<Grid2> <Grid2>
<Field label="Số tài khoản" value={detail.bankAccount} mono /> <Field label="Số tài khoản" value={detail.bankAccount} mono accent="teal" />
<Field label="Ngân hàng" value={detail.bankName} /> <Field label="Ngân hàng" value={detail.bankName} accent="teal" />
<Field label="Chi nhánh" value={detail.bankBranch} full /> <Field label="Chi nhánh" value={detail.bankBranch} full accent="teal" />
</Grid2> </Grid2>
</Card> </Card>
<Card title="Đoàn thể" icon={ShieldCheck}> <Card title="Đoàn thể" icon={ShieldCheck} accent="violet">
<Grid2> <Grid2>
<Field label="Đảng viên" value={detail.isCommunistParty ? 'Có' : 'Không'} /> <Field label="Đảng viên" value={detail.isCommunistParty ? 'Có' : 'Không'} accent="violet" />
<Field label="Ngày kết nạp Đảng" value={fmtDate(detail.communistPartyJoinDate)} /> <Field label="Ngày kết nạp Đảng" value={fmtDate(detail.communistPartyJoinDate)} accent="violet" />
<Field label="Đoàn viên" value={detail.isYouthUnion ? 'Có' : 'Không'} /> <Field label="Đoàn viên" value={detail.isYouthUnion ? 'Có' : 'Không'} accent="violet" />
<Field label="Ngày kết nạp Đoàn" value={fmtDate(detail.youthUnionJoinDate)} /> <Field label="Ngày kết nạp Đoàn" value={fmtDate(detail.youthUnionJoinDate)} accent="violet" />
<Field label="Công đoàn" value={detail.isTradeUnion ? 'Có' : 'Không'} /> <Field label="Công đoàn" value={detail.isTradeUnion ? 'Có' : 'Không'} accent="violet" />
<Field label="Ngày kết nạp CĐ" value={fmtDate(detail.tradeUnionJoinDate)} /> <Field label="Ngày kết nạp CĐ" value={fmtDate(detail.tradeUnionJoinDate)} accent="violet" />
</Grid2> </Grid2>
</Card> </Card>
{detail.notes && ( {detail.notes && (
<Card title="Ghi chú" icon={FileText}> <Card title="Ghi chú" icon={FileText} accent="brand">
<p className="whitespace-pre-wrap text-sm text-slate-700">{detail.notes}</p> <p className="whitespace-pre-wrap text-sm text-slate-700">{detail.notes}</p>
</Card> </Card>
)} )}
@ -1495,55 +1528,69 @@ function RowActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () =>
} }
// Card = section container for the tab body (replaces the old <details> Section). // Card = section container for the tab body (replaces the old <details> Section).
function Card({ title, icon: Icon, count, action, children }: { // việc 2+3: nhận accent → icon-chip nền màu nhạt + chữ đậm, heading màu đậm, rail trái.
function Card({ title, icon: Icon, count, action, accent = 'brand', children }: {
title: string title: string
icon: typeof IdCard icon: typeof IdCard
count?: number count?: number
action?: React.ReactNode action?: React.ReactNode
accent?: Accent
children: React.ReactNode children: React.ReactNode
}) { }) {
const a = ACCENT[accent]
return ( return (
<section className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"> <section
<header className="flex items-center justify-between gap-2 border-b border-slate-100 px-4 py-2.5"> className={cn(
'relative overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm',
// colored left rail (pseudo-element clips to the rounded corner cleanly)
"before:absolute before:inset-y-0 before:left-0 before:w-1 before:content-['']", a.rail,
)}
>
<header className="flex items-center justify-between gap-2 border-b border-slate-100 px-4 py-2.5 pl-5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="icon-chip" style={{ height: '1.75rem', width: '1.75rem' }}> <span
className="icon-chip"
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: a.chipBg, ['--chip-fg' as string]: a.chipFg }}
>
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</span> </span>
<h3 className="text-sm font-semibold tracking-tight text-slate-900">{title}</h3> <h3 className={cn('text-sm font-semibold tracking-tight', a.head)}>{title}</h3>
{count != null && count > 0 && ( {count != null && count > 0 && (
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">{count}</span> <span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">{count}</span>
)} )}
</div> </div>
{action} {action}
</header> </header>
<div className="p-4">{children}</div> <div className="p-4 pl-5">{children}</div>
</section> </section>
) )
} }
function SubLabel({ children }: { children: React.ReactNode }) { function SubLabel({ children, accent = 'brand' }: { children: React.ReactNode; accent?: Accent }) {
return <div className="mb-1 mt-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{children}</div> return <div className={cn('mb-1 mt-3 text-xs font-semibold uppercase tracking-wide', ACCENT[accent].labelText)}>{children}</div>
} }
function Grid2({ children }: { children: React.ReactNode }) { function Grid2({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">{children}</div> return <div className="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">{children}</div>
} }
function Field({ label, value, mono, icon: Icon, full }: { // Field — nhãn uppercase brand/accent-tint (việc 3), value đậm rõ. Empty = dấu —.
function Field({ label, value, mono, icon: Icon, full, accent = 'brand' }: {
label: string label: string
value: string | number | null value: string | number | null
mono?: boolean mono?: boolean
icon?: typeof Phone icon?: typeof Phone
full?: boolean full?: boolean
accent?: Accent
}) { }) {
const empty = value == null || value === '' const empty = value == null || value === ''
return ( return (
<div className={cn('min-w-0 text-sm', full && 'sm:col-span-2')}> <div className={cn('min-w-0 text-sm', full && 'sm:col-span-2')}>
<div className="flex items-center gap-1 text-[11px] font-medium uppercase tracking-wide text-slate-400"> <div className={cn('flex items-center gap-1 text-[11px] font-semibold uppercase tracking-wide', ACCENT[accent].labelText)}>
{Icon && <Icon className="h-3 w-3" />} {Icon && <Icon className="h-3 w-3" />}
{label} {label}
</div> </div>
<div className={cn('mt-0.5 break-words', empty ? 'text-slate-300' : 'text-slate-800', mono && !empty && 'font-mono text-xs')}> <div className={cn('mt-0.5 break-words', empty ? 'text-slate-300' : 'font-medium text-slate-900', mono && !empty && 'font-mono text-xs')}>
{empty ? '—' : value} {empty ? '—' : value}
</div> </div>
</div> </div>