From 5d94bb449a3265742aff21d642ccef1262379c50 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Apr 2026 11:17:14 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PE:=20Workflow=20designer=20admin=20?= =?UTF-8?q?UI=20+=20=C3=9D=20ki=E1=BA=BFn=204=20ph=C3=B2ng=20ban=20(P1=20S?= =?UTF-8?q?ession=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ==== Task 1: PE Workflow Designer admin ==== BE (mirror Contract WorkflowAdminFeatures pattern): - Application/PurchaseEvaluations/PeWorkflowAdminFeatures.cs ~250 LOC: - GetPeWorkflowAdminOverviewQuery → list 2 EvaluationType (DuyetNcc / DuyetNccPhuongAn) với Active + History versions + count phiếu đang dùng - CreatePeWorkflowDefinitionCommand + Validator: auto-increment Version per Code, deactivate Active cũ trong cùng EvaluationType (1 active per type invariant) - DTOs: PeWorkflowStepApproverDto / PeWorkflowStepDto / PeWorkflowDefinitionDto / PeWorkflowTypeSummaryDto / PeWorkflowAdminOverviewDto - Phase validation 1..7 (state thường, không bao gồm 99=TuChoi) - Api/Controllers/PeWorkflowsController.cs: 2 endpoint GET /api/pe-workflows + POST. Reuse policy "Workflows.Read" + "Workflows.Create" (admin chung quyền cho cả 2 nhóm WF). FE: - pages/system/PeWorkflowsPage.tsx ~500 LOC mirror WorkflowsPage: - Landing 2-card grid khi /system/pe-workflows (chưa pick type) - TypePanel khi /system/pe-workflows/:typeCode (DuyetNcc / DuyetNccPhuongAn) - DefinitionCard read-only view với active badge + version + steps + approvers (Role/User chip) - PeWorkflowDesigner dialog: clone từ existing, edit Code/Name/Description, add/remove steps, +Role / +User approvers per step, save → version mới + deactivate cũ - App.tsx route /system/pe-workflows + /system/pe-workflows/:typeCode - Layout đã có resolver PeWf_ → /system/pe-workflows/ từ session 3 ==== Task 2: Ý kiến 4 phòng ban PE ==== Domain: - PurchaseEvaluationDepartmentOpinion entity (AuditableEntity) — PEId + Kind + Opinion text + SignedAt + UserId + UserName denorm - PeDepartmentKind enum (PheDuyet / Ccm / MuaHang / SmPm) - PE entity + collection navigation DepartmentOpinions Infrastructure: - PurchaseEvaluationDepartmentOpinionConfiguration EF: UNIQUE(PEId, Kind) — max 1 row per phòng ban per phiếu (UPDATE in-place) - ApplicationDbContext + IApplicationDbContext DbSet - Migration 15 AddPurchaseEvaluationDepartmentOpinions (15 migration total / 52 DB tables) Application: - PeDepartmentOpinionFeatures.cs: UpsertPeDepartmentOpinionCommand (sign=true → set SignedAt+UserId, sign=false chỉ lưu text giữ chữ ký cũ) + DeletePeDepartmentOpinionCommand - DTO bundle update: + DepartmentOpinions list trong PurchaseEvaluationDetailBundleDto - GetPurchaseEvaluationQueryHandler load DepartmentOpinions + KindLabel resolution API: - POST /api/purchase-evaluations/{id}/opinions (upsert) - DELETE /api/purchase-evaluations/{id}/opinions/{kind} FE: - types/purchaseEvaluation.ts: + PeDepartmentKind enum + PeDepartmentKindLabel + PeDepartmentOpinion type + departmentOpinions vào bundle - PeDetailTabs Section "5. Ý kiến 4 phòng ban (sign-off)" — 2x2 grid OpinionBox per kind: - Read mode (readOnly menu Duyệt): hiển thị text + chữ ký - Edit mode: textarea + 2 button "Lưu text" / "Lưu & Ký" - Badge "Đã ký" emerald + tên người ký + ngày khi signedAt != null ==== Task 3: User seed verify ==== Seed `SeedDemoUsersAsync` đã match đúng user list authoritative (5 PRO TPB+NV / 7 CCM TPB+NV / 1 ISO / 1 CEO) từ prior commit. DbInitializer reconcile sẽ tự sync khi API restart. Typo trong list user (soluttions / trương) đã fixed sensibly trong seed. ==== Build verify ==== - dotnet build clean (0 error) - fe-admin TS build pass (1 module mới PeWorkflowsPage) - fe-user TS build pass (PE detail mirror) Total: 8 file mới (BE 4 + FE 1 + Migration 2 + 1 Domain) + 13 file modified. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/App.tsx | 3 + fe-admin/src/components/pe/PeDetailTabs.tsx | 131 + fe-admin/src/pages/system/PeWorkflowsPage.tsx | 499 +++ fe-admin/src/types/purchaseEvaluation.ts | 27 + fe-user/src/components/pe/PeDetailTabs.tsx | 131 + fe-user/src/types/purchaseEvaluation.ts | 27 + .../Controllers/PeWorkflowsController.cs | 27 + .../PurchaseEvaluationsController.cs | 21 + .../Interfaces/IApplicationDbContext.cs | 1 + .../Dtos/PurchaseEvaluationDtos.cs | 10 + .../PeDepartmentOpinionFeatures.cs | 152 + .../PeWorkflowAdminFeatures.cs | 247 ++ .../PurchaseEvaluationFeatures.cs | 16 + .../PurchaseEvaluations/PurchaseEvaluation.cs | 1 + .../PurchaseEvaluationDepartmentOpinion.cs | 35 + .../Persistence/ApplicationDbContext.cs | 1 + .../PurchaseEvaluationConfiguration.cs | 19 + ...seEvaluationDepartmentOpinions.Designer.cs | 3297 +++++++++++++++++ ...AddPurchaseEvaluationDepartmentOpinions.cs | 58 + .../ApplicationDbContextModelSnapshot.cs | 68 + 20 files changed, 4771 insertions(+) create mode 100644 fe-admin/src/pages/system/PeWorkflowsPage.tsx create mode 100644 src/Backend/SolutionErp.Api/Controllers/PeWorkflowsController.cs create mode 100644 src/Backend/SolutionErp.Application/PurchaseEvaluations/PeDepartmentOpinionFeatures.cs create mode 100644 src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkflowAdminFeatures.cs create mode 100644 src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluationDepartmentOpinion.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260429041117_AddPurchaseEvaluationDepartmentOpinions.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260429041117_AddPurchaseEvaluationDepartmentOpinions.cs diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 338bddd..9ac24cc 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -12,6 +12,7 @@ import { CatalogsPage } from '@/pages/master/CatalogsPage' import { PermissionsPage } from '@/pages/system/PermissionsPage' import { RolesPage } from '@/pages/system/RolesPage' import { WorkflowsPage } from '@/pages/system/WorkflowsPage' +import { PeWorkflowsPage } from '@/pages/system/PeWorkflowsPage' import { FormsPage } from '@/pages/forms/FormsPage' import { ContractsListPage } from '@/pages/contracts/ContractsListPage' import { ContractDetailPage } from '@/pages/contracts/ContractDetailPage' @@ -47,6 +48,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index aac435d..ec81f72 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -18,12 +18,15 @@ import { cn } from '@/lib/cn' import { PeAttachmentPurpose, PeAttachmentPurposeLabel, + PeDepartmentKind, + PeDepartmentKindLabel, PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, type PeAttachment, type PeChangelog, + type PeDepartmentOpinion, type PeDetailBundle, type PeDetailRow, type PeQuote, @@ -108,6 +111,9 @@ export function PeDetailTabs({
+
+ +
) @@ -122,6 +128,131 @@ function Section({ title, children }: { title: string; children: React.ReactNode ) } +// ===== Section 5 — Ý kiến 4 phòng ban ===== +// Render 2x2 grid 4 box (Phê duyệt / CCM / MuaHàng / SM-PM). Mỗi box hiển +// thị Opinion text + chữ ký (UserName + SignedAt) nếu đã ký, hoặc form nhập +// + 2 button "Lưu" + "Lưu & Ký" khi chưa ký / readOnly=false. +function DepartmentOpinionsSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) { + const KINDS: { kind: number; label: string }[] = [ + { kind: PeDepartmentKind.PheDuyet, label: PeDepartmentKindLabel[PeDepartmentKind.PheDuyet] }, + { kind: PeDepartmentKind.Ccm, label: PeDepartmentKindLabel[PeDepartmentKind.Ccm] }, + { kind: PeDepartmentKind.MuaHang, label: PeDepartmentKindLabel[PeDepartmentKind.MuaHang] }, + { kind: PeDepartmentKind.SmPm, label: PeDepartmentKindLabel[PeDepartmentKind.SmPm] }, + ] + return ( +
+ {KINDS.map(k => { + const existing = ev.departmentOpinions.find(o => o.kind === k.kind) ?? null + return ( + + ) + })} +
+ ) +} + +function OpinionBox({ + evaluationId, + kind, + kindLabel, + existing, + readOnly, +}: { + evaluationId: string + kind: number + kindLabel: string + existing: PeDepartmentOpinion | null + readOnly: boolean +}) { + const qc = useQueryClient() + const [text, setText] = useState(existing?.opinion ?? '') + const isSigned = !!existing?.signedAt + + const save = useMutation({ + mutationFn: async (sign: boolean) => + api.post(`/purchase-evaluations/${evaluationId}/opinions`, { + kind, + opinion: text || null, + sign, + }), + onSuccess: () => { + toast.success('Đã lưu ý kiến.') + qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }) + }, + onError: e => toast.error(getErrorMessage(e)), + }) + + return ( +
+
+

{kindLabel}

+ {isSigned && ( + + Đã ký + + )} +
+ + {readOnly ? ( + <> +
+ {existing?.opinion ?? — chưa có ý kiến} +
+ {isSigned && ( +
+ Ký bởi {existing?.userName ?? '—'} · {new Date(existing!.signedAt!).toLocaleString('vi-VN')} +
+ )} + + ) : ( + <> +