[CLAUDE] Hrm: P11-C Vehicle+Driver catalogs (Mig 44) + gotcha #57 filtered-unique 3 HRM catalog (Mig 45)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m18s

P11-C: extend HrmConfigs +2 kind (Vehicle/Driver) declarative. Mig 44 AddVehicleAndDriverCatalogs (2 table filtered-unique Code, tables 91->93). Domain entity + EF config (filtered day-1) + 2 DbSet + HrmConfigFeatures Region5/6 CRUD + Controller +2 route-group (GET public / write Roles=Admin) + MenuKeys +2 +All (auto Admin perm) + DbInitializer 2 menu leaf + idempotent seed 2 veh/2 drv. FE declarative KIND_CONFIG +2 kind x2 app (SHA256 mirror) + 4-place (types/page/menuKeys/Layout staticMap), :kind-driven no new route.

gotcha #57 (bundled; OtPolicy missed in backlog, caught via grep) - Mig 45 FilterHrmCatalogUniqueIndexesByIsDeleted: LeaveType+ShiftPattern+OtPolicy bare .IsUnique() -> .HasFilter([IsDeleted]=0) (recreate-on-soft-deleted-slot 500 fix, mirror Holiday Mig 43). Tests +5 HrmConfigFilteredUniqueTests (181->186 PASS) test-before RED->GREEN. Reviewer caught FE<->BE Driver required-field mismatch -> fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-08 10:32:28 +07:00
parent f8179c5fbd
commit 30a99aa03f
27 changed files with 14144 additions and 16 deletions

View File

@ -61,6 +61,9 @@ function resolvePath(key: string): string | null {
Hrm_Config_Holidays: '/hrm/configs/holidays',
Hrm_Config_Shifts: '/hrm/configs/shifts',
Hrm_Config_OtPolicies: '/hrm/configs/ot-policies',
// [P11-C S51] danh mục xe công + tài xế — cùng page :kind-driven.
Hrm_Config_Vehicles: '/hrm/configs/vehicles',
Hrm_Config_Drivers: '/hrm/configs/drivers',
// [Phase 10.2 G-O1 S34 2026-05-27] Module Văn phòng số — Danh bạ nội bộ.
// 4-place mirror Pattern 16-bis: types/ + pages/ + App.tsx + menuKeys + staticMap.
Off_DanhBa: '/directory',

View File

@ -40,6 +40,9 @@ export const MenuKeys = {
HrmConfigHolidays: 'Hrm_Config_Holidays',
HrmConfigShifts: 'Hrm_Config_Shifts',
HrmConfigOtPolicies: 'Hrm_Config_OtPolicies',
// P11-C (S51) — danh mục xe công + tài xế (dùng khi đặt xe)
HrmConfigVehicles: 'Hrm_Config_Vehicles',
HrmConfigDrivers: 'Hrm_Config_Drivers',
// Module Văn phòng số — Danh bạ nội bộ (Phase 10.2 G-O1 Session 34, 2026-05-27)
Off: 'Off',
OffDanhBa: 'Off_DanhBa',

View File

@ -6,7 +6,7 @@
import { useState, type ComponentType, type FormEvent } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useParams, useNavigate } from 'react-router-dom'
import { Settings2, Calendar, Clock, Pencil, Plus, Plane, Repeat, Trash2, Search } from 'lucide-react'
import { Settings2, Calendar, Clock, Pencil, Plus, Plane, Repeat, Trash2, Search, Car, IdCard } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader'
import { Button } from '@/components/ui/Button'
@ -109,9 +109,36 @@ const KIND_CONFIG: Record<Kind, {
],
columns: ['Mã', 'Tên', 'Hệ số WD/WE/HL', 'Max h/year', 'Trạng thái'],
},
'vehicles': {
label: 'Xe công',
description: 'Danh mục xe công ty — dùng khi đăng ký đặt xe.',
icon: Car,
fields: [
{ key: 'code', label: 'Mã *', type: 'text', required: true, placeholder: 'XE-01...' },
{ key: 'name', label: 'Tên xe *', type: 'text', required: true, placeholder: 'Toyota Innova 7 chỗ' },
{ key: 'licensePlate', label: 'Biển số *', type: 'text', required: true, placeholder: '30A-12345' },
{ key: 'seatCount', label: 'Số chỗ', type: 'number', placeholder: '7' },
{ key: 'description', label: 'Mô tả', type: 'textarea' },
],
columns: ['Mã', 'Tên xe', 'Biển số', 'Số chỗ', 'Trạng thái'],
},
'drivers': {
label: 'Tài xế',
description: 'Danh mục tài xế — dùng khi đăng ký đặt xe.',
icon: IdCard,
fields: [
{ key: 'code', label: 'Mã *', type: 'text', required: true, placeholder: 'TX-01...' },
{ key: 'name', label: 'Họ tên *', type: 'text', required: true, placeholder: 'Nguyễn Văn Tài' },
{ key: 'phoneNumber', label: 'SĐT *', type: 'text', required: true, placeholder: '0901xxxxxx' },
{ key: 'licenseNumber', label: 'Số GPLX *', type: 'text', required: true, placeholder: '012345678' },
{ key: 'licenseClass', label: 'Hạng *', type: 'text', required: true, placeholder: 'B2, C, D, E' },
{ key: 'description', label: 'Mô tả', type: 'textarea' },
],
columns: ['Mã', 'Họ tên', 'SĐT', 'Số GPLX', 'Hạng', 'Trạng thái'],
},
}
const KINDS: Kind[] = ['leave-types', 'holidays', 'shifts', 'ot-policies']
const KINDS: Kind[] = ['leave-types', 'holidays', 'shifts', 'ot-policies', 'vehicles', 'drivers']
type ConfigRow = Record<string, unknown> & { id: string; isActive?: boolean }
@ -427,6 +454,29 @@ function renderCells(kind: Kind, row: ConfigRow) {
</>
)
}
if (kind === 'vehicles') {
return (
<>
<td className="px-3 py-2 font-mono text-xs">{row.code as string}</td>
<td className="px-3 py-2">{row.name as string}</td>
<td className="px-3 py-2 font-mono text-xs text-slate-600">{row.licensePlate as string}</td>
<td className="px-3 py-2 text-xs text-slate-600">{(row.seatCount as number) ?? ''}</td>
<td className="px-3 py-2">{statusBadge}</td>
</>
)
}
if (kind === 'drivers') {
return (
<>
<td className="px-3 py-2 font-mono text-xs">{row.code as string}</td>
<td className="px-3 py-2">{row.name as string}</td>
<td className="px-3 py-2 text-xs text-slate-600">{(row.phoneNumber as string) || ''}</td>
<td className="px-3 py-2 font-mono text-xs text-slate-600">{(row.licenseNumber as string) || ''}</td>
<td className="px-3 py-2 text-xs text-slate-600">{(row.licenseClass as string) || ''}</td>
<td className="px-3 py-2">{statusBadge}</td>
</>
)
}
// ot-policies
const mw = Number(row.multiplierWeekday ?? 0)
const me = Number(row.multiplierWeekend ?? 0)

View File

@ -1,7 +1,7 @@
// Types cho HRM Config module — mirror BE Domain.Hrm.* entities.
// Phase 10.2 G-H2 (Mig 35 — S34 schema + S35 BE CRUD wire + FE this file).
export type HrmConfigKind = 'leave-types' | 'holidays' | 'shifts' | 'ot-policies'
export type HrmConfigKind = 'leave-types' | 'holidays' | 'shifts' | 'ot-policies' | 'vehicles' | 'drivers'
// ========== DTOs (mirror BE DTOs HrmConfigFeatures.cs records) ==========
@ -53,6 +53,28 @@ export type OtPolicyDto = {
description: string | null
}
// Phase 11 P11-C (S51) — danh mục xe công + tài xế, dùng khi đăng ký đặt xe.
export type VehicleDto = {
id: string
code: string
name: string
licensePlate: string
seatCount: number | null
isActive: boolean
description: string | null
}
export type DriverDto = {
id: string
code: string
name: string
phoneNumber: string | null
licenseNumber: string | null
licenseClass: string | null
isActive: boolean
description: string | null
}
// ========== Create/Update Input Commands (mirror BE record Command shape) ==========
export type CreateLeaveTypeInput = {
@ -102,3 +124,24 @@ export type CreateOtPolicyInput = {
}
export type UpdateOtPolicyInput = CreateOtPolicyInput & { id: string; isActive: boolean }
export type CreateVehicleInput = {
code: string
name: string
licensePlate: string
seatCount: number | null
description: string | null
}
export type UpdateVehicleInput = CreateVehicleInput & { id: string; isActive: boolean }
export type CreateDriverInput = {
code: string
name: string
phoneNumber: string | null
licenseNumber: string | null
licenseClass: string | null
description: string | null
}
export type UpdateDriverInput = CreateDriverInput & { id: string; isActive: boolean }