From 456c7a721ba84ea32a657ffb5faa9c337839e6fd Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 16 Jun 2026 11:35:31 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User:=20H=E1=BB=93=20s=C6=A1=20Nh?= =?UTF-8?q?=C3=A2n=20s=E1=BB=B1=20=E2=80=94=20layout=202=20c=E1=BB=99t=20(?= =?UTF-8?q?tree+list=20tr=C3=A1i,=20detail=20ph=E1=BA=A3i)=20+=20t=C3=B4?= =?UTF-8?q?=20m=C3=A0u=20chi=20ti=E1=BA=BFt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -700; rail before:content). fe-admin mirror defer. Co-Authored-By: Claude Opus 4.8 (1M context) --- fe-user/src/pages/hrm/EmployeesListPage.tsx | 577 +++++++++++--------- 1 file changed, 312 insertions(+), 265 deletions(-) diff --git a/fe-user/src/pages/hrm/EmployeesListPage.tsx b/fe-user/src/pages/hrm/EmployeesListPage.tsx index a821a7f..5ab5302 100644 --- a/fe-user/src/pages/hrm/EmployeesListPage.tsx +++ b/fe-user/src/pages/hrm/EmployeesListPage.tsx @@ -1,10 +1,15 @@ -// List + Detail Hồ sơ Nhân sự (HRM) — 3-panel master-detail: -// [Cây tổ chức] | [Danh sách + filter] | [Chi tiết 5 tab]. -// Redesign S65 (2026-06-16) theo reference NamGroup: org-tree panel (consume -// 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). KHÔNG đổi logic — 100% chức năng giữ: -// 5 satellite inline CRUD (add/edit/delete + mutex), search, filter, mọi -// TanStack query/mutation key NGUYÊN. Đây là RESTRUCTURE layout, không xoá logic. +// List + Detail Hồ sơ Nhân sự (HRM) — 2-panel master-detail (NamGroup eoffice ref): +// 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. +// CỘT PHẢI (rộng): [Chi tiết 5 tab] — avatar header + Tổng quan / Thân nhân / +// Trình độ / Kinh nghiệm / Hợp đồng. +// Refine S66 (2026-06-16) theo anh góp ý từ eoffice LIVE (3 việc): +// 1. layout 3-cột-ngang → 2-cột (tree+list xếp chồng cột trái · detail rộng phải). +// 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. import { useMemo, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' @@ -135,7 +140,7 @@ export function EmployeesListPage() { 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(() => { if (!deptFilter || !tree.data) return null let found: string | null = null @@ -156,193 +161,197 @@ export function EmployeesListPage() { } return ( -
- {/* ===================== PANEL 1 — Org tree ===================== */} - - - {/* ===================== PANEL 2 — list + filter ===================== */} -
-
-
-

Hồ sơ Nhân sự

-

- {selectedDeptName ? `Phòng: ${selectedDeptName}` : 'Tất cả phòng ban'} - {' · '} - {list.data?.total ?? 0} hồ sơ -

-
-
- {/* mobile-only tree toggle */} - - -
-
- - {/* search + status filter strip */} -
-
- - setLocalSearch(e.target.value)} - placeholder="Tìm mã NV hoặc họ tên…" - className="pl-8" - /> -
- - - {(search || statusFilter || deptFilter) && ( - - )} -
- - {/* list table */} -
- {list.isLoading ? ( -
Đang tải…
- ) : !list.data || list.data.items.length === 0 ? ( - - ) : ( - - - - - - - - - - {list.data.items.map(e => { - const active = selectedId === e.id - return ( - setParam('id', e.id)} - className={cn( - 'cursor-pointer border-b border-slate-100 transition last:border-0 hover:bg-slate-50', - active && 'bg-brand-50 hover:bg-brand-50', - )} + - - - - ) - })} - -
Nhân viênPhòng banTrạng thái
-
- -
-
{e.fullName ?? '—'}
-
{e.employeeCode}
-
-
-
{e.departmentName ?? '—'} - -
- )} -
-
+ + + +
+ {companyOpen && + (!tree.data || tree.data.length === 0 ? ( +
Chưa có phòng ban.
+ ) : ( +
    + {tree.data.map(n => ( + + ))} +
+ ))} + + + )} + + - {/* ===================== PANEL 3 — detail (5 tab) ===================== */} + {/* ---- List + filter (chiếm phần còn lại của rail, cuộn riêng) ---- */} +
+
+
+

Hồ sơ Nhân sự

+

+ {selectedDeptName ? `Phòng: ${selectedDeptName}` : 'Tất cả phòng ban'} + {' · '} + {list.data?.total ?? 0} hồ sơ +

+
+
+ {/* mobile-only tree toggle */} + + +
+
+ + {/* search + status filter strip */} +
+
+ + setLocalSearch(e.target.value)} + placeholder="Tìm mã NV hoặc họ tên…" + className="pl-8" + /> +
+ + + {(search || statusFilter || deptFilter) && ( + + )} +
+ + {/* list table */} +
+ {list.isLoading ? ( +
Đang tải…
+ ) : !list.data || list.data.items.length === 0 ? ( + + ) : ( + + + + + + + + + + {list.data.items.map(e => { + const active = selectedId === e.id + return ( + setParam('id', e.id)} + className={cn( + 'cursor-pointer border-b border-slate-100 transition last:border-0 hover:bg-slate-50', + active && 'bg-brand-50 hover:bg-brand-50', + )} + > + + + + + ) + })} + +
Nhân viênPhòng banTrạng thái
+
+ +
+
{e.fullName ?? '—'}
+
{e.employeeCode}
+
+
+
{e.departmentName ?? '—'} + +
+ )} +
+
+ + + {/* ===================== RIGHT — detail (5 tab) ===================== */}
{!selectedId ? (
@@ -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 = { + 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 ===================== const TABS = [ @@ -700,6 +727,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe { setAddingFamilyRelation(true); setEditingFamilyRelationId(null) }} disabled={addingFamilyRelation}> @@ -751,6 +779,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe { setAddingEducation(true); setEditingEducationId(null) }} disabled={addingEducation}> @@ -808,6 +837,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe { setAddingSkill(true); setEditingSkillId(null) }} disabled={addingSkill}> @@ -872,6 +902,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe { setAddingWorkHistory(true); setEditingWorkHistoryId(null) }} disabled={addingWorkHistory}> @@ -928,6 +959,7 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe { setAddingDocument(true); setEditingDocumentId(null) }} disabled={addingDocument}> @@ -996,114 +1028,115 @@ function EmployeeDetailTabs({ detail, onDelete }: { detail: EmployeeDetail; onDe } // ===================== 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 }) { return (
{/* LEFT column */}
- + - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - + + + + - + - - - - - - - + + + + + + + - + - - - - - - + + + + + +
{/* RIGHT column */}
- + - - - - - - - - + + + + + + + + - + - - - - + + + + - Ngày phép + Ngày phép - - - - + + + + - + - - - + + + - + - - - - - - + + + + + + {detail.notes && ( - +

{detail.notes}

)} @@ -1495,55 +1528,69 @@ function RowActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () => } // Card = section container for the tab body (replaces the old
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 icon: typeof IdCard count?: number action?: React.ReactNode + accent?: Accent children: React.ReactNode }) { + const a = ACCENT[accent] return ( -
-
+
+
- + -

{title}

+

{title}

{count != null && count > 0 && ( {count} )}
{action}
-
{children}
+
{children}
) } -function SubLabel({ children }: { children: React.ReactNode }) { - return
{children}
+function SubLabel({ children, accent = 'brand' }: { children: React.ReactNode; accent?: Accent }) { + return
{children}
} function Grid2({ children }: { children: React.ReactNode }) { return
{children}
} -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 value: string | number | null mono?: boolean icon?: typeof Phone full?: boolean + accent?: Accent }) { const empty = value == null || value === '' return (
-
+
{Icon && } {label}
-
+
{empty ? '—' : value}