From e7b66cd52b57315733a9ce7c4fb6d2fdb175eb09 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sat, 30 May 2026 09:44:00 +0700 Subject: [PATCH] [CLAUDE] Workflow: wire ApproveV2 + LevelOpinions cho 4 WorkflowApps module (Phase 11 P11-A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire full approval workflow V2 cho Leave/OT/Travel/Vehicle — cookie-cutter mirror Proposal (Mig 38). Trước đây skeleton Phase 1 (Create+List), giờ ApproveV2 advance-level + UPSERT LevelOpinion + atomic codegen. Schema (Mig 41 WireWorkflowAppsApprovalV2 — 84→89 tables, pure additive): - 4 bảng {Leave,Ot,Travel,Vehicle}LevelOpinions (UNIQUE composite + Cascade parent + Restrict Level — mirror ProposalLevelOpinion) - 1 bảng WorkflowAppCodeSequences (shared atomic MaDonTu, Prefix-keyed) - 4 cột RejectedFromStatus (smart return tracking) - enum ApprovalWorkflowApplicableType.TravelRequest = 9 Application (LeaveOt + TravelVehicle ApprovalFeatures.cs — 30 handler): - GetById detail (Include LevelOpinions + JOIN Step/Level) · UpdateDraft - Submit (gen MaDonTu + DaGuiDuyet + level=1, verify ApplicableType per module) - Approve (verify actor==ApproverUserId OR Admin, UPSERT opinion latest-write-wins, advance level OR terminal DaDuyet, empty comment → placeholder) - Reject (→TuChoi) · Return (→TraLai + RejectedFromStatus) Api: 4 controller +6 route mỗi cái (GET/{id}, PUT/{id}, submit/approve/reject/return) Infra: DbInitializer seed 4 workflow V2 mẫu (QT-NP/OT/CT/XE-V2-001) → UAT test ngay FE: WorkflowAppDetailPage.tsx declarative 4-kind (fe-admin+fe-user SHA256 identical) — workflow status + opinion timeline + action buttons; gỡ banner skeleton + row nav Tests: +11 WorkflowAppApproveV2Tests (130→141 PASS) — state machine + UPSERT invariant + guards + codegen + forbidden + placeholder (Leave full + Ot smoke) Verify: build 0 error · 141 test PASS · FE build ×2 · reviewer checklist (ApplicableType per-module + cross-module DbSet + [Authorize] — no copy-paste bug) Known-minor (unreachable): Reject/Return actor-check skip nếu CurrentApprovalLevelOrder null — nhưng DaGuiDuyet luôn có set (defer hardening). ItTicket KHÔNG đụng (kanban, no workflow V2). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../implementer-backend/MEMORY.md | 6 +- .../investigator-codebase/MEMORY.md | 1 + .../agent-memory/test-specialist/MEMORY.md | 3 +- fe-admin/src/App.tsx | 2 + .../pages/office/WorkflowAppDetailPage.tsx | 423 ++ .../src/pages/office/WorkflowAppsListPage.tsx | 16 +- fe-admin/src/types/workflowApps.ts | 53 + fe-user/src/App.tsx | 2 + .../pages/office/WorkflowAppDetailPage.tsx | 423 ++ .../src/pages/office/WorkflowAppsListPage.tsx | 16 +- fe-user/src/types/workflowApps.ts | 53 + .../Controllers/LeaveRequestsController.cs | 55 +- .../Controllers/OtRequestsController.cs | 56 +- .../Controllers/TravelRequestsController.cs | 56 +- .../Controllers/VehicleBookingsController.cs | 57 +- .../Interfaces/IApplicationDbContext.cs | 8 + .../Office/LeaveOtApprovalFeatures.cs | 749 ++ .../Office/TravelVehicleApprovalFeatures.cs | 773 ++ .../ApprovalWorkflowsV2/ApprovalWorkflow.cs | 1 + .../SolutionErp.Domain/Office/LeaveRequest.cs | 3 + .../Office/LeaveRequestLevelOpinion.cs | 28 + .../SolutionErp.Domain/Office/OtRequest.cs | 3 + .../Office/OtRequestLevelOpinion.cs | 28 + .../Office/TravelRequest.cs | 3 + .../Office/TravelRequestLevelOpinion.cs | 28 + .../Office/VehicleBooking.cs | 3 + .../Office/VehicleBookingLevelOpinion.cs | 28 + .../Office/WorkflowAppCodeSequence.cs | 19 + .../Persistence/ApplicationDbContext.cs | 7 + .../LeaveRequestLevelOpinionConfiguration.cs | 31 + .../OtRequestLevelOpinionConfiguration.cs | 31 + .../TravelRequestLevelOpinionConfiguration.cs | 31 + ...VehicleBookingLevelOpinionConfiguration.cs | 31 + .../WorkflowAppCodeSequenceConfiguration.cs | 19 + .../Persistence/DbInitializer.cs | 203 + ...936_WireWorkflowAppsApprovalV2.Designer.cs | 6302 +++++++++++++++++ ...260530021936_WireWorkflowAppsApprovalV2.cs | 275 + .../ApplicationDbContextModelSnapshot.cs | 357 + .../Application/WorkflowAppApproveV2Tests.cs | 443 ++ 39 files changed, 10604 insertions(+), 22 deletions(-) create mode 100644 fe-admin/src/pages/office/WorkflowAppDetailPage.tsx create mode 100644 fe-user/src/pages/office/WorkflowAppDetailPage.tsx create mode 100644 src/Backend/SolutionErp.Application/Office/LeaveOtApprovalFeatures.cs create mode 100644 src/Backend/SolutionErp.Application/Office/TravelVehicleApprovalFeatures.cs create mode 100644 src/Backend/SolutionErp.Domain/Office/LeaveRequestLevelOpinion.cs create mode 100644 src/Backend/SolutionErp.Domain/Office/OtRequestLevelOpinion.cs create mode 100644 src/Backend/SolutionErp.Domain/Office/TravelRequestLevelOpinion.cs create mode 100644 src/Backend/SolutionErp.Domain/Office/VehicleBookingLevelOpinion.cs create mode 100644 src/Backend/SolutionErp.Domain/Office/WorkflowAppCodeSequence.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/LeaveRequestLevelOpinionConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/OtRequestLevelOpinionConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/TravelRequestLevelOpinionConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/VehicleBookingLevelOpinionConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/WorkflowAppCodeSequenceConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530021936_WireWorkflowAppsApprovalV2.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530021936_WireWorkflowAppsApprovalV2.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/WorkflowAppApproveV2Tests.cs diff --git a/.claude/agent-memory/implementer-backend/MEMORY.md b/.claude/agent-memory/implementer-backend/MEMORY.md index d4f9305..89ce86a 100644 --- a/.claude/agent-memory/implementer-backend/MEMORY.md +++ b/.claude/agent-memory/implementer-backend/MEMORY.md @@ -66,7 +66,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i ## 🧠 SOLUTION_ERP BE conventions (S40) - **BE .NET 10:** PascalCase entities + DTO records + command names. CQRS+MediatR+FluentValidation+AutoMapper. Repository qua `IApplicationDbContext`. `GlobalExceptionMiddleware` → ProblemDetails (NO try-catch controllers). -- **State S40:** 40 mig (last `AddAttendances`) · 84 SQL tables · ~211 endpoints · 130 test baseline (test-specialist owns). Phase 9 UAT skip per chunk (`feedback_uat_skip_verify`). +- **State S41:** 41 mig (last `WireWorkflowAppsApprovalV2`) · 89 SQL tables · ~211 endpoints · 130 test baseline (test-specialist owns). Phase 9 UAT skip per chunk (`feedback_uat_skip_verify`). - **Build:** `dotnet build SolutionErp.slnx` clean 0 err. Commit `[CLAUDE] : ` + Co-Authored-By Claude Opus 4.8 (1M context). - **Pin (KHÔNG `*`/latest):** MediatR `12.4.1` (14 fail DI) · Swashbuckle `6.9.0` · Node CI `20.x` · LibreOffice `25.8.6` · @microsoft/signalr `8.0.7`. @@ -74,6 +74,10 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i ## 📅 Recent activity (FIFO — older → archive/git) +- **S42 P11-A SEED — 4 sample ApprovalWorkflow V2 for WorkflowApps (DbInitializer.cs only, +4 method ~210 LOC + 4 call):** Case 1 mechanical mirror `SeedSampleProposalWorkflowV2Async` EXACT × 4 (Leave5/Ot6/Travel9/Vehicle7). Each: idempotent `AnyAsync(ApplicableType==X)` guard → resolve approver `binh.le@solutions.com.vn` (SAME user as Proposal/Contract seed, null→LogWarning+return) → 1 ApprovalWorkflow (Version=1, IsActive+IsUserSelectable=true, ActivatedAt=UtcNow) + 1 Step (Order=1, Name="Cấp duyệt", DepartmentId=CCM?.Id) + 1 Level (Order=1, ApproverUserId). Codes QT-NP/OT/CT/XE-V2-001. Wired 4 calls after SeedSampleProposalWorkflowV2Async (NOT gated DemoSeed, gotcha #51 infra seed). Enum verified Grep. Build 0 err 0 warn. Bash tool = bash NOT PowerShell despite env hint (use `cd && cmd | grep`). Spec deterministic 100% → ACCEPT Case 1. NOT touched App/Controller/FE/test/Mig. Tag `[s42, p11-a, seed, mirror-proposal-exact]`. +- **S42 P11-A Wave 2b APP — wire ApproveV2 CQRS Travel+Vehicle (`TravelVehicleApprovalFeatures.cs` ~830 LOC + 2 controller edit):** Cookie-cutter mirror Wave 2a / ProposalFeatures Region 2. 1 new file ns `Application.Office`: 2 module × (DetailDto + LevelOpinionDto + GetById JOIN Step/Level metadata + UpdateDraft + Submit + Approve UPSERT+advance + Reject TuChoi + Return TraLai+RejectedFromStatus) + 1 shared `internal static TravelVehicleCodeGen.GenerateMaDonTuAsync` (Serializable tx, `WorkflowAppCodeSequences` Prefix-keyed, prefix `DT/CT/{year}` Travel & `DX/XE/{year}` Vehicle, format `{prefix}/{seq:D3}` — D3 no year segment per spec). KEY gotcha: WorkflowAppStatus enum DIFFERS from ProposalStatus int values (DaGuiDuyet=2 not 1, TraLai=3) → mirror by SEMANTIC enum member not literal. Owner = `RequesterUserId` (not DrafterUserId). Submit verify wf.ApplicableType==Travel9/Vehicle7 else Conflict. 2 controller +6 route each (GET{id}/PUT/submit/approve/reject/return) nested body records, CreatedAtAction. KHÔNG sửa WorkflowAppsFeatures.cs/Leave/Ot/FE/test/seed. Build 0 err. Spec deterministic ~98% em main → ACCEPT Case 1/2. Tag `[s42, p11-a, wave-2b, mirror-proposal-region2]`. +- **S42 P11-A Wave 2a APP — wire ApproveV2 CQRS Leave+Ot (`LeaveOtApprovalFeatures.cs` ~770 LOC + 2 controller edit):** Pattern 4 (UPSERT in Approve, 0 opinion endpoint) + cookie-cutter mirror ProposalFeatures Region 2. 1 new file ns `Application.Office`: 2 module × (DetailDto + LevelOpinionDto + GetById JOIN Step/Level metadata + UpdateDraft + Submit + Approve UPSERT+advance + Reject TuChoi + Return TraLai+RejectedFromStatus) + 1 shared `internal static WorkflowAppCodeGen.GenerateMaDonTuAsync` (Serializable tx + `WorkflowAppCodeSequences` Prefix-keyed, prefix DT/LR & DT/OT, format `{prefix}/{year}/{seq:D3}`). Approve: flatten allLevels OrderBy Step→Level, currentSlot=allLevels[order-1], actor==ApproverUserId OR Admin, comment empty→placeholder, advance OR terminal DaDuyet. Submit verify wf.ApplicableType==Leave5/Ot6 else Conflict + gen MaDonTu nếu null. 2 controller +6 route each (GET{id}/PUT/submit/approve/reject/return) mirror ProposalsController nested body records (`WorkflowActionBody`). KHÔNG sửa WorkflowAppsFeatures.cs (Region 1 Create/List ở đó). Build 0 err (2 warn DocxRenderer pre-existing). Spec deterministic 100% em main → ACCEPT Case 1. Travel/Vehicle (Wave 2b) + test (Wave 4) deferred. Tag `[s42, p11-a, wave-2a, mirror-proposal-region2]`. +- **S41 P11-A Wave 1 SCHEMA — wire ApproveV2+LevelOpinions 4 WorkflowApps (Mig 41 `WireWorkflowAppsApprovalV2`):** Pattern 12-bis cookie-cutter mirror Proposal Mig 38, 13× cumulative. 11 file: 5 entity (4 `{Leave,Ot,Travel,Vehicle}RequestLevelOpinion` + shared `WorkflowAppCodeSequence` Prefix-PK) + 5 EF config (auto-discover ApplyConfigurationsFromAssembly, no manual register) + edit 4 parent (nav LevelOpinions + `WorkflowAppStatus? RejectedFromStatus`) + edit enum (`TravelRequest=9`) + 2 DbSet (IAppDbContext+ApplicationDbContext, 5 each). Mig diff CLEAN: 5 CreateTable + 4 AddColumn (no drift). FK Cascade parent + Restrict Level + UNIQUE composite ({Parent}Id, ApprovalWorkflowLevelId). Applied BOTH DB (Dev + Design). Build 0 err. Wave 2 (App/Controller) + Wave 4 (test) deferred per spec. Spec deterministic 100% (em main chose schema) → ACCEPT Case 1. Tag `[s41, p11-a, 12-bis-13x, mig41]`. - **S35 G-H2 BE CRUD 4 catalog (HrmConfigFeatures.cs 372 LOC + Controller 134 LOC, 16 endpoint):** Pattern 12-bis 3rd application catalog-mega. 4 sub-resource × 4 verb. KEY: HRM no HasQueryFilter → `.Where(!IsDeleted)` manual; Validator MaxLength = EF source-of-truth (Code=50 not spec 20). 130 test baseline preserve. ACCEPT clean spec 95%. Tag `[s35, be-crud, hrm, 12-bis-3x]`. - **S29 Plan B Chunk C Contract V2 mirror (Mig 33 ContractLevelOpinions):** Pattern 12-bis 1st — 8 file +4265 LOC (Designer autogen 95%, handcraft ~232 LOC). Em main spec deterministic 100% → ACCEPT. Tag `[s29, plan-b, 12-bis]`. - **Archived FE/test + older BE entries → `archive/2026-05-q4.md` + git d2f52ba (S40 curate):** S35 FE inline forms 5 satellite (→ frontend domain) · S34 test bundle +10 [Fact] 130 PASS (→ test-specialist domain) · S33 Task 5 EmployeesListPage · S32 wrap/startup. KEY absorbed in Patterns above + split pointers. diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index dac2058..5861d00 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -70,6 +70,7 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte ## 📅 Recent activity (FIFO — older → archive/git) +- **2026-05-30 (P11-A WorkflowApps wire pre-flight):** 4 module Leave/OT/Travel/Vehicle. Schema pin ĐÃ CÓ SẴN (Mig 39): `Office/{Module}.cs` đều có `ApprovalWorkflowId?`+`CurrentApprovalLevelOrder?`+`WorkflowAppStatus` (5-state khớp ProposalStatus). SKELETON tại `Application/Office/WorkflowAppsFeatures.cs:11-15` (chỉ Create+List, KHÔNG Approve/Reject/Return/GetById). **Proposal = mirror HOÀN HẢO** (cùng Office ns, Mig 38): `ProposalFeatures.cs:403-486` ApproveHandler = flatten `Steps.OrderBy(Order).SelectMany(Levels.OrderBy(Order))` global level index → `allLevels[CurrentApprovalLevelOrder-1]` → actor match `Level.ApproverUserId==uid||Admin` → UPSERT LevelOpinion → advance++/DaDuyet. ⚠️ `ApprovalWorkflow.cs:72` nói Level **KHÔNG OR-of-N** (1 ApproverUserId/Level) — KHÁC memory cũ "OR-of-N", verify lại. Gap: 4 bảng `{Module}LevelOpinion` mới (Mig 41+), 3 route/controller, 4 seed WF, FE Detail+Opinion (chỉ có WorkflowAppsListPage chung). ⚠️ enum `ApplicableType` THIẾU Travel (có Leave=5/OT=6/Vehicle=7/ItTicket=8); `ExtendApplicableTypeForWorkflowApps` mig empty Up/Down (enum-only). Tag `[pre-flight, p11-a, workflowapps]`. - **2026-05-29 (S40 STATE GROUNDING):** 7 metric verify. ✅ Migrations=**40** (path `.../Persistence/Migrations/*.cs`, last `AddAttendances`). ✅ Gotchas=**55** (`### N.`). ✅ git clean. DbSet=77 nhưng **SQL tables=84** (em main verify `.ToTable()` ModelSnapshot — 77+7 Identity, "84 docs ĐÚNG", DbSet count sai −7). Endpoints=**211** (docs ~223). FE pages fe-admin **36**+fe-user 29=**65** (docs 53 under-count). Menu keys=**53** const (docs 85 over-count). 3 số tin cậy nhất = mig/gotcha/git. Lesson: tables phải count ToTable KHÔNG DbSet. Tag `[state-grounding, docs-drift, s40]`. - **2026-05-29 (S39 BVAAU 7-agent extract):** Đọc 8 file BVAAU `.claude/agents/` ~22K. Split 4→7 trục research(2)/implement(2)/quality(3). Boundary: repo interface=domain, EF config=infra, test=test-specialist. Tool: cả 7 agent 5 RAG MCP (+search_code BM25 +store_memory +list_projects). BVAAU Phase 0 codebase RỖNG → aspirational template chưa battle-test; SOLUTION_ERP giữ 6 skill + backend/frontend split (thay domain/infra cho 2-FE fit). VIPIX guide claim KHÔNG verify được (file miss). Tag `[cross-project, bvaau, port]`. - **2026-05-28 (S37 G-O3 Proposal pre-flight):** PE pattern mirror cho Mig 38. LevelOpinion UNIQUE per-LEVEL `(EntityId, LevelId)` FK Cascade+Restrict. CodeSequence 1 row/Prefix (`DX/YYYY` 3 col Prefix PK+LastSeq, tx SERIALIZABLE). ApproveV2Async 7 step (group Levels by Order=Cấp OR-of-N → match actor.Id∈ApproverUserId + Admin bypass → UPSERT LevelOpinion sync → F2 skipToFinal → advance levelOrder++ → DaDuyet). NamGroup TblRequest generic → **SOL clean-room MẠNH HƠN**. CategoryId nullable FK + free text fallback (lesson Plan C 8 FK ZERO populated). Tag `[pre-flight, g-o3, proposal]`. diff --git a/.claude/agent-memory/test-specialist/MEMORY.md b/.claude/agent-memory/test-specialist/MEMORY.md index 84ba86c..d1c6590 100644 --- a/.claude/agent-memory/test-specialist/MEMORY.md +++ b/.claude/agent-memory/test-specialist/MEMORY.md @@ -15,7 +15,7 @@ WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQ - ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix - ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority -## 📊 Baseline 130 PASS (58 Domain + 72 Infra) +## 📊 Baseline 141 PASS (58 Domain + 83 Infra) ← S42 +11 WorkflowApp ApproveV2 Run: `dotnet test SolutionErp.slnx --nologo --verbosity minimal` ## ⏱️ Timing rules (docs/rules.md §7) @@ -51,6 +51,7 @@ Test theo CODE (single source truth), document mismatch header comment + report. - **2026-05-29 (S39 agent split setup):** NEW dedicated agent. Seeded test patterns (10 reflection authz + 11 infra helper + 12 InternalsVisibleTo + #48 SQLite tie-break + spec drift S34). Inherited coverage gap backlog 4 priority items từ S36 Reviewer audit (130 PASS baseline). First spawn pending em main S39+ test bundle task (recommend Gap 1 Holiday composite UNIQUE first). - **2026-05-29 (S40 baseline audit smoke):** CONFIRMED 130 PASS (Domain 58 + Infra 72), 0 fail/skip, ~15s. Runner count authoritative; raw `[Fact]/[Theory]` attr = 48+70 (Theory→InlineData expand). Infra spread 15 files. Gap re-verified vs prod: EmployeesController+HrmConfigsController EXIST, authz regression chỉ ApprovalWorkflowsV2Controller (gotcha #44 gap real). Proposal = Domain entity + EF config only, CHƯA có ApproveV2Async service (S37 skeleton, defer đúng). Agent load OK. AUDIT-only, no write. +- **2026-05-30 (S42 P11-A Wave4):** +11 test `tests/.../Application/WorkflowAppApproveV2Tests.cs` → **141 PASS** (Infra 72→83). LeaveRequest 8 case full (Submit happy/guard×2, Approve advance/terminal/UPSERT-invariant/forbidden/empty-comment-placeholder, Reject→TuChoi, Return→TraLai+RejectedFromStatus) + OtRequest smoke (submit→approve single-level→DaDuyet). **No prod bug** — LeaveOt ApproveV2 wire correct, all PASS first run. **NEW Pattern:** WorkflowApps handlers = CQRS MediatR (KHÔNG service) → instantiate handler trực tiếp `new ApproveLeaveRequestHandler(db, AsUser(u), clock).Handle(cmd,ct)`, chỉ 3 dep (IApplicationDbContext + TestCurrentUser + FixedDateTime) — nhẹ hơn 6-dep Contract service. MaDonTu format "DT/LR/2026/001". Gap #4 (Workflow Apps) PARTIAL done — Travel/Vehicle mirror pending. ⚠️ Lesson: CWD drift (fe-user) → ghi MEMORY nhầm path, em main relocate. Verify CWD root trước Write memory. --- diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index c870079..cdab029 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -36,6 +36,7 @@ import { ProposalCreatePage } from '@/pages/office/ProposalCreatePage' import { ProposalDetailPage } from '@/pages/office/ProposalDetailPage' import { ProposalsListPage } from '@/pages/office/ProposalsListPage' import { WorkflowAppsListPage } from '@/pages/office/WorkflowAppsListPage' +import { WorkflowAppDetailPage } from '@/pages/office/WorkflowAppDetailPage' import { ItTicketsPage } from '@/pages/office/ItTicketsPage' import { MyAttendancePage } from '@/pages/office/MyAttendancePage' import { HrmDashboardPage } from '@/pages/hrm/HrmDashboardPage' @@ -96,6 +97,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx b/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx new file mode 100644 index 0000000..1e51fd8 --- /dev/null +++ b/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx @@ -0,0 +1,423 @@ +// Generic Workflow App Detail page — Phase 11 P11-A Wave 3a (S42 2026-05-30). +// Declarative KIND_CONFIG Record mirror WorkflowAppsListPage — 4 module +// leave / ot / travel / vehicle. Workflow status + Ý kiến cấp duyệt timeline + +// Submit/Approve/Reject/Return actions (mirror ProposalDetailPage cấu trúc). +// File MIRROR SHA256 identical với fe-user counterpart. +import { useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useNavigate, useParams } from 'react-router-dom' +import { + ArrowLeft, Ban, CalendarOff, Car, CheckCircle2, Clock, Plane, RotateCcw, Send, +} from 'lucide-react' +import { toast } from 'sonner' +import { PageHeader } from '@/components/PageHeader' +import { Button } from '@/components/ui/Button' +import { Dialog } from '@/components/ui/Dialog' +import { Label } from '@/components/ui/Label' +import { Textarea } from '@/components/ui/Textarea' +import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' +import { cn } from '@/lib/cn' +import { + WORKFLOW_APP_STATUS_BADGE, WORKFLOW_APP_STATUS_LABELS, + WorkflowAppStatus, type WorkflowAppDetail, +} from '@/types/workflowApps' + +type Kind = 'leave' | 'ot' | 'travel' | 'vehicle' +type ActionKind = 'approve' | 'reject' | 'return' + +interface WorkflowOption { id: string; code: string; name: string } + +function formatDate(iso?: string): string { + if (!iso) return '—' + return new Date(iso).toLocaleDateString('vi-VN') +} + +function formatDateTime(iso?: string): string { + if (!iso) return '—' + return new Date(iso).toLocaleString('vi-VN') +} + +function formatVnd(n: number | null | undefined): string { + if (n === null || n === undefined) return '—' + return n.toLocaleString('vi-VN') + ' đ' +} + +const ACTION_LABEL: Record = { + approve: { text: 'Duyệt', tone: 'bg-emerald-600 hover:bg-emerald-700' }, + reject: { text: 'Từ chối', tone: 'bg-red-600 hover:bg-red-700' }, + return: { text: 'Trả lại', tone: 'bg-orange-600 hover:bg-orange-700' }, +} + +const KIND_CONFIG: Record React.ReactNode }> +}> = { + leave: { + title: 'Đơn xin nghỉ phép', + endpoint: '/leave-requests', + applicableType: 5, + icon: CalendarOff, + detailFields: [ + { label: 'Người xin', render: (x) => x.requesterFullName }, + { label: 'Từ ngày', render: (x) => formatDate(x.startDate) }, + { label: 'Đến ngày', render: (x) => formatDate(x.endDate) }, + { label: 'Số ngày', render: (x) => x.numDays ?? '—' }, + { label: 'Lý do', render: (x) => x.reason || '—' }, + ], + }, + ot: { + title: 'Đơn đăng ký OT', + endpoint: '/ot-requests', + applicableType: 6, + icon: Clock, + detailFields: [ + { label: 'Người xin', render: (x) => x.requesterFullName }, + { label: 'Ngày OT', render: (x) => formatDate(x.otDate) }, + { label: 'Giờ bắt đầu', render: (x) => x.startTime ?? '—' }, + { label: 'Giờ kết thúc', render: (x) => x.endTime ?? '—' }, + { label: 'Số giờ', render: (x) => x.hours ?? '—' }, + { label: 'Lý do', render: (x) => x.reason || '—' }, + ], + }, + travel: { + title: 'Đơn đi công tác', + endpoint: '/travel-requests', + applicableType: 9, + icon: Plane, + detailFields: [ + { label: 'Người xin', render: (x) => x.requesterFullName }, + { label: 'Địa điểm', render: (x) => x.destination || '—' }, + { label: 'Từ ngày', render: (x) => formatDate(x.startDate) }, + { label: 'Đến ngày', render: (x) => formatDate(x.endDate) }, + { label: 'Số ngày', render: (x) => x.numDays ?? '—' }, + { label: 'Dự toán chi phí', render: (x) => formatVnd(x.estimatedCost) }, + { label: 'Mục đích', render: (x) => x.purpose || '—' }, + ], + }, + vehicle: { + title: 'Đặt xe công', + endpoint: '/vehicle-bookings', + applicableType: 7, + icon: Car, + detailFields: [ + { label: 'Người đặt', render: (x) => x.requesterFullName }, + { label: 'Biển số', render: (x) => {x.vehicleLicense ?? '—'} }, + { label: 'Tên xe', render: (x) => x.vehicleName || '—' }, + { label: 'Bắt đầu', render: (x) => formatDateTime(x.startAt) }, + { label: 'Kết thúc', render: (x) => formatDateTime(x.endAt) }, + { label: 'Địa điểm đến', render: (x) => x.destination || '—' }, + { label: 'Tài xế', render: (x) => x.driverName || '—' }, + { label: 'Mục đích', render: (x) => x.purpose || '—' }, + ], + }, +} + +export function WorkflowAppDetailPage() { + const { kind = 'leave', id } = useParams<{ kind: Kind; id: string }>() + const navigate = useNavigate() + const qc = useQueryClient() + const config = KIND_CONFIG[kind as Kind] + + const [actionDialog, setActionDialog] = useState(null) + const [comment, setComment] = useState('') + const [pickedWorkflowId, setPickedWorkflowId] = useState('') + + const detail = useQuery({ + queryKey: [config?.endpoint, id], + queryFn: async () => + (await api.get(`${config.endpoint}/${id}`)).data, + enabled: !!config && !!id, + }) + + const d = detail.data + const status = d?.status + const isDraft = status === WorkflowAppStatus.Nhap || status === WorkflowAppStatus.TraLai + const isInWorkflow = status === WorkflowAppStatus.DaGuiDuyet + const hasWorkflow = !!d?.approvalWorkflowId + + // Workflow picker — chỉ fetch khi draft chưa pin workflow. + const workflows = useQuery({ + queryKey: ['approval-workflows-v2', { applicableType: config?.applicableType, isUserSelectable: true }], + queryFn: async () => + (await api.get('/approval-workflows-v2', { + params: { applicableType: config.applicableType, isUserSelectable: true }, + })).data, + enabled: !!config && isDraft && !hasWorkflow, + }) + + const invalidate = () => { + qc.invalidateQueries({ queryKey: [config.endpoint, id] }) + qc.invalidateQueries({ queryKey: [config.endpoint] }) + } + + const pinWorkflow = useMutation({ + mutationFn: async (workflowId: string) => { + await api.put(`${config.endpoint}/${id}`, { approvalWorkflowId: workflowId }) + }, + onSuccess: () => { + toast.success('Đã chọn quy trình duyệt') + invalidate() + }, + onError: (e) => toast.error(getErrorMessage(e)), + }) + + const submit = useMutation({ + mutationFn: async () => { + await api.post(`${config.endpoint}/${id}/submit`, {}) + }, + onSuccess: () => { + toast.success('Đã gửi duyệt') + invalidate() + }, + onError: (e) => toast.error(getErrorMessage(e)), + }) + + const action = useMutation({ + mutationFn: async (k: ActionKind) => { + await api.post(`${config.endpoint}/${id}/${k}`, { comment: comment.trim() || null }) + }, + onSuccess: (_, k) => { + toast.success(`Đã ${ACTION_LABEL[k].text.toLowerCase()}`) + setActionDialog(null) + setComment('') + invalidate() + }, + onError: (e) => toast.error(getErrorMessage(e)), + }) + + if (!config) { + return
Module không tồn tại: {kind}
+ } + + if (detail.isLoading) { + return ( +
+ +
+ ) + } + + if (detail.isError || !d) { + return ( +
+ navigate(`/workflow-apps/${kind}`)}> + + Quay lại + + } + /> +
+ Không tải được dữ liệu đơn từ. +
+
+ ) + } + + const Icon = config.icon + + return ( +
+ navigate(`/workflow-apps/${kind}`)}> + + Danh sách + + } + /> + + {/* Status row + action buttons */} +
+
+ + {WORKFLOW_APP_STATUS_LABELS[d.status]} + + {d.currentApprovalLevelOrder != null && ( + + Cấp hiện tại: {d.currentApprovalLevelOrder} + + )} + {d.workflowCode && ( + + Quy trình: {d.workflowCode} + + )} +
+
+ {isDraft && !hasWorkflow && ( + <> + + + + )} + {isDraft && ( + + )} + {isInWorkflow && ( + <> + + + + + )} +
+
+ + {isDraft && !hasWorkflow && ( +
+ Đơn chưa gắn quy trình duyệt. Vui lòng chọn quy trình rồi bấm Lưu quy trình trước khi gửi duyệt. +
+ )} + + {/* Section 1: Thông tin */} +
+

+ + 1. Thông tin +

+
+ {config.detailFields.map((f) => ( +
+ +
{f.render(d)}
+
+ ))} +
+ +
{formatDateTime(d.createdAt)}
+
+
+
+ + {/* Section 2: Quy trình duyệt */} +
+

2. Quy trình duyệt

+
+
+ +
+ {d.workflowCode ? ( + <> + {d.workflowCode} - {d.workflowName} + + ) : '— Chưa chọn —'} +
+
+
+ +
{d.currentApprovalLevelOrder ?? '—'}
+
+
+
+ + {/* Section 3: Ý kiến cấp duyệt V2 dynamic */} +
+

3. Ý kiến cấp duyệt

+ {d.levelOpinions.length === 0 ? ( +
Chưa có ý kiến.
+ ) : ( +
+ {[...d.levelOpinions] + .sort((a, b) => + (a.stepOrder ?? 0) - (b.stepOrder ?? 0) || + (a.levelOrder ?? 0) - (b.levelOrder ?? 0)) + .map((o) => ( +
+
+ + Bước {o.stepOrder} {o.stepName} · Cấp {o.levelOrder} + + {formatDateTime(o.signedAt)} +
+
{o.signedByFullName}
+
{o.comment ?? '(duyệt — không ý kiến)'}
+
+ ))} +
+ )} +
+ + {/* Action confirm dialog */} + { + setActionDialog(null) + setComment('') + }} + title={actionDialog ? `${ACTION_LABEL[actionDialog].text} đơn từ` : ''} + > +
+ +