Files
solution-erp/fe-admin/src/pages/office/WorkflowAppsListPage.tsx
pqhuy1987 e54a22de0c
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m53s
[CLAUDE] Domain+App+Infra+Api+FE-Admin+FE-User: S38 G-O4+G-O5+G-O6+G-P1+G-H3 SKELETON full-stack
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>
2026-05-28 16:19:42 +07:00

156 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 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>
)
}