All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m53s
Phase 10.3-10.4 SKELETON 5 plan combo finish — Mig 39+40 + BE skeleton 7 module + FE 2 app SHA256 IDENTICAL + 11 menu key. UAT visible end-to-end. ⚠️ SKELETON Phase 1 trade-off rõ: - Status flat 5-state WorkflowAppStatus enum share Leave/OT/Travel/Vehicle - ApproveV2 workflow advance DEFER Phase 11 (Drafter Create OK, Approve flow chưa wire) - LevelOpinions per-module DEFER Phase 11 - LeaveBalance calc + Auto-assign + SLA timer DEFER Phase 11 - CodeGen atomic + MaDonTu/MaTicket gen DEFER Phase 11 - Vehicle catalog + Driver catalog DEFER Phase 11 (free text VehicleLicense) - ItTicketComments thread DEFER Phase 11 (free text Resolution field) Mig 39 (em main solo): 5 entity Workflow Apps schema - LeaveRequest (G-O4, FK LeaveType Hrm Mig 35, ApplicableType=5) - OtRequest (G-O4, FK OtPolicy optional, ApplicableType=6) - TravelRequest (G-O4, reuse ApplicableType=4 Proposal) - VehicleBooking (G-O5, free text vehicle, ApplicableType=7) - ItTicket (G-O6, NO workflow V2 — kanban status flow) Mig 40 (em main solo): Attendance entity (G-P1) - GPS lat/long check-in/out + Source enum Web/Mobile/Device - UNIQUE composite (UserId, AttendanceDate) - WorkHours computed simple diff (NO OtPolicy multiplier yet) BE CQRS (em main solo, single mega ~1100 LOC): - WorkflowAppsFeatures.cs 7 region (5 module Create+List + Attendance CheckIn/Out/GetMonth + HrDashboard) - 7 Controller: /api/leave-requests + /ot-requests + /travel-requests + /vehicle-bookings + /it-tickets + /attendances + /hr/dashboard - Class-level [Authorize] any authenticated - 13 endpoint total FE 2 app (em main solo fallback gotcha #53 risk): - types/workflowApps.ts × 2 SHA256 IDENTICAL 77470e182a15de88 (all DTOs + Status badge) - WorkflowAppsListPage.tsx × 2 IDENTICAL 58139d0301a60ddf — generic declarative KIND_CONFIG handles 4 module (Leave/OT/Travel/Vehicle) - ItTicketsPage.tsx × 2 IDENTICAL d3062de2f54c794c — kanban 5 status column - MyAttendancePage.tsx × 2 IDENTICAL 86da48ae147db012 — GPS check-in/out + tháng calendar - HrmDashboardPage.tsx × 2 IDENTICAL d9c6c12a5a8694f8 — 4 KPI card + gender ratio + status breakdown - Pattern 16-bis 9× cumulative (App.tsx +4 routes + menuKeys +8 const + Layout staticMap +7 entry) - 7 amber banner "Skeleton Phase 1 — full feature Phase 11" rõ ràng UAT Menu seed: +11 const + SeedMenuTreeAsync 8 row (Off_DonTu sub-group + 3 leaf + Off_DatXe + Off_ItTicket + Off_ChamCong + Hrm_Dashboard). DbInitializer Sample workflow seed DEFER (workflows V2 already seeded từ S29+S37 reuse — admin clone tạo riêng per ApplicableType=5/6/7). Verify: - dotnet build PASS 0 error 2 pre-existing warning - dotnet test 130/130 PASS baseline preserve - npm build × 2 PASS clean - SHA256 verify 5 file × 2 app all IDENTICAL Plan G-* progress 11/11 ✅ (100% COMPLETE): ✅ G-H1 (S33) + G-O1 (S34) + G-H2 (S35) + G-O2 (S36) + G-O3 (S37) + ✅ G-O4 + G-O5 + G-O6 + G-P1 + G-H3 (S38 skeleton) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
// Generic Workflow Apps List Page — Phase 10.3 G-O4+G-O5+G-O6 (S38 2026-05-28).
|
||
// SKELETON Phase 1: read-only list. Create form + workflow actions DEFER Phase 11.
|
||
// Handles 4 module via URL `:kind` param: leave / ot / travel / vehicle.
|
||
// File MIRROR SHA256 identical fe-user counterpart.
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { useParams } from 'react-router-dom'
|
||
import { CalendarOff, Clock, Plane, Car, FileSignature } from 'lucide-react'
|
||
import { PageHeader } from '@/components/PageHeader'
|
||
import { api } from '@/lib/api'
|
||
import { cn } from '@/lib/cn'
|
||
import {
|
||
WORKFLOW_APP_STATUS_BADGE, WORKFLOW_APP_STATUS_LABELS,
|
||
type PagedResult,
|
||
} from '@/types/workflowApps'
|
||
|
||
type Kind = 'leave' | 'ot' | 'travel' | 'vehicle'
|
||
|
||
const KIND_CONFIG: Record<Kind, {
|
||
title: string
|
||
description: string
|
||
endpoint: string
|
||
columns: Array<{ key: string; label: string; render: (item: any) => React.ReactNode }>
|
||
}> = {
|
||
leave: {
|
||
title: 'Đơn xin nghỉ phép',
|
||
description: 'Danh sách đơn nghỉ phép — Workflow V2 ApplicableType=5',
|
||
endpoint: '/leave-requests',
|
||
columns: [
|
||
{ key: 'maDonTu', label: 'Mã', render: (x) => <span className="font-mono text-xs">{x.maDonTu ?? '—'}</span> },
|
||
{ key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName },
|
||
{ key: 'startDate', label: 'Từ', render: (x) => new Date(x.startDate).toLocaleDateString('vi-VN') },
|
||
{ key: 'endDate', label: 'Đến', render: (x) => new Date(x.endDate).toLocaleDateString('vi-VN') },
|
||
{ key: 'numDays', label: 'Ngày', render: (x) => x.numDays },
|
||
{ key: 'reason', label: 'Lý do', render: (x) => <span className="truncate max-w-xs block">{x.reason}</span> },
|
||
],
|
||
},
|
||
ot: {
|
||
title: 'Đơn đăng ký OT',
|
||
description: 'Danh sách đơn OT — Workflow V2 ApplicableType=6',
|
||
endpoint: '/ot-requests',
|
||
columns: [
|
||
{ key: 'maDonTu', label: 'Mã', render: (x) => <span className="font-mono text-xs">{x.maDonTu ?? '—'}</span> },
|
||
{ key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName },
|
||
{ key: 'otDate', label: 'Ngày OT', render: (x) => new Date(x.otDate).toLocaleDateString('vi-VN') },
|
||
{ key: 'hours', label: 'Giờ', render: (x) => x.hours },
|
||
{ key: 'reason', label: 'Lý do', render: (x) => <span className="truncate max-w-xs block">{x.reason}</span> },
|
||
],
|
||
},
|
||
travel: {
|
||
title: 'Đơn đi công tác',
|
||
description: 'Danh sách đăng ký công tác',
|
||
endpoint: '/travel-requests',
|
||
columns: [
|
||
{ key: 'maDonTu', label: 'Mã', render: (x) => <span className="font-mono text-xs">{x.maDonTu ?? '—'}</span> },
|
||
{ key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName },
|
||
{ key: 'destination', label: 'Địa điểm', render: (x) => x.destination },
|
||
{ key: 'startDate', label: 'Từ', render: (x) => new Date(x.startDate).toLocaleDateString('vi-VN') },
|
||
{ key: 'endDate', label: 'Đến', render: (x) => new Date(x.endDate).toLocaleDateString('vi-VN') },
|
||
{ key: 'purpose', label: 'Mục đích', render: (x) => <span className="truncate max-w-xs block">{x.purpose}</span> },
|
||
],
|
||
},
|
||
vehicle: {
|
||
title: 'Đặt xe công',
|
||
description: 'Danh sách booking xe — Workflow V2 ApplicableType=7',
|
||
endpoint: '/vehicle-bookings',
|
||
columns: [
|
||
{ key: 'maDonTu', label: 'Mã', render: (x) => <span className="font-mono text-xs">{x.maDonTu ?? '—'}</span> },
|
||
{ key: 'requesterFullName', label: 'Người đặt', render: (x) => x.requesterFullName },
|
||
{ key: 'vehicleLicense', label: 'Biển số', render: (x) => <span className="font-mono text-xs">{x.vehicleLicense}</span> },
|
||
{ key: 'destination', label: 'Đến', render: (x) => x.destination },
|
||
{ key: 'startAt', label: 'Bắt đầu', render: (x) => new Date(x.startAt).toLocaleString('vi-VN') },
|
||
{ key: 'driverName', label: 'Tài xế', render: (x) => x.driverName ?? '—' },
|
||
],
|
||
},
|
||
}
|
||
|
||
const ICON_MAP: Record<Kind, any> = {
|
||
leave: CalendarOff, ot: Clock, travel: Plane, vehicle: Car,
|
||
}
|
||
|
||
export function WorkflowAppsListPage() {
|
||
const { kind = 'leave' } = useParams<{ kind: Kind }>()
|
||
const config = KIND_CONFIG[kind as Kind]
|
||
const Icon = ICON_MAP[kind as Kind] ?? FileSignature
|
||
|
||
const list = useQuery({
|
||
queryKey: [config.endpoint, { page: 1 }],
|
||
queryFn: async () => (await api.get<PagedResult<any>>(config.endpoint, { params: { page: 1, pageSize: 50 } })).data,
|
||
enabled: !!config,
|
||
})
|
||
|
||
const items = list.data?.items ?? []
|
||
|
||
if (!config) {
|
||
return <div className="text-red-600">Module không tồn tại: {kind}</div>
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<PageHeader title={config.title} description={config.description} />
|
||
|
||
<div className="rounded-lg border bg-amber-50/50 p-3 text-sm text-amber-900">
|
||
⚠️ <strong>Skeleton Phase 1 (S38):</strong> Read-only list. Form tạo + workflow Approve/Reject defer Phase 11 polish.
|
||
Em chủ trì kích hoạt full ApproveV2 wire khi anh main yêu cầu.
|
||
</div>
|
||
|
||
<div className="rounded-lg border bg-card">
|
||
<table className="w-full text-sm">
|
||
<thead className="border-b bg-muted/50">
|
||
<tr>
|
||
{config.columns.map((c) => (
|
||
<th key={c.key} className="px-4 py-2 text-left font-medium">{c.label}</th>
|
||
))}
|
||
<th className="px-4 py-2 text-left font-medium">Trạng thái</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{list.isLoading && (
|
||
<tr>
|
||
<td colSpan={config.columns.length + 1} className="px-4 py-8 text-center text-muted-foreground">
|
||
Đang tải...
|
||
</td>
|
||
</tr>
|
||
)}
|
||
{!list.isLoading && items.length === 0 && (
|
||
<tr>
|
||
<td colSpan={config.columns.length + 1} className="px-4 py-8 text-center text-muted-foreground">
|
||
<Icon className="mx-auto h-8 w-8 mb-2 opacity-50" />
|
||
Chưa có dữ liệu.
|
||
</td>
|
||
</tr>
|
||
)}
|
||
{items.map((item: any) => (
|
||
<tr key={item.id} className="border-b">
|
||
{config.columns.map((c) => (
|
||
<td key={c.key} className="px-4 py-2">{c.render(item)}</td>
|
||
))}
|
||
<td className="px-4 py-2">
|
||
<span
|
||
className={cn(
|
||
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium',
|
||
WORKFLOW_APP_STATUS_BADGE[item.status],
|
||
)}
|
||
>
|
||
{WORKFLOW_APP_STATUS_LABELS[item.status]}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|