From 14feb6955d049555c2060f7d4371846c37e3d6ba Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 22 May 2026 12:45:33 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin+FE-User:=20Plan=20B=20Chunk?= =?UTF-8?q?=20E3=20=E2=80=94=20ContractDetailPage=20Section=205=20LevelOpi?= =?UTF-8?q?nionsV2=20dynamic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Em main solo sau Implementer E3 stuck mid-task. Minimum viable mirror PE Section 5 LevelOpinionsSectionV2 pattern. V2 contract pin ApprovalWorkflowId → render dynamic Section 5 với opinion data UPSERT từ Service ApproveV2Async (Plan B Chunk B2 1f199b0). Changes × 2 app: types/contracts.ts (×2): - ContractDetail +3 fields V2 (default null backward compat): - approvalWorkflowId: string | null - currentApprovalLevelOrder: number | null - levelOpinions: ContractLevelOpinion[] | null - NEW type ContractLevelOpinion (12 fields mirror BE DTO) components/contracts/ContractDetailContent.tsx (×2): - Add Section 5 sau "Chi tiết HĐ" section - Conditional render: chỉ khi c.approvalWorkflowId (V2 mode) - Empty placeholder "Chưa có ý kiến" khi workflow vừa start - forEach opinion render card: - Title: Bước X (StepName) — Cấp Y (LevelName) - Comment + signedAt vi-VN format - NV duyệt: approverFullName - Banner "⚡ Admin duyệt thay" khi signedByUserId !== approverUserId - Style: emerald palette mirror PE Section 5 V1 contract: 3 fields null → Section 5 KHÔNG render (backward compat). Verify: - npm run build × 2 app PASS 0 TS err - Mirror 2 app §3.9 byte-similar (4 file edit cùng pattern) - BE wire OK: E2 commit 48f6d22 expose approvalWorkflowId + levelOpinions Plan B COMPLETE 9/9 chunks LOCAL: - A1 58898e8 ✅ Entity +2 fields - A2 a85e437 ✅ Mig 32 + Config + Seed - B 138469d ✅ Service ApproveV2Async branch - C 26c98d3 ✅ Mig 33 LevelOpinions - B2 1f199b0 ✅ UPSERT block - E1 ef23308 ✅ CreateContractCommand +V2 - D 62b50d1 ✅ FE Workspace V2 - E2 48f6d22 ✅ ContractDetailDto + populate - E3 (this) ✅ FE Section 5 LevelOpinionsV2 Implementer E3 spawn stopped mid-task ("check ContractDetail type" judgment call) → em main solo finish. Pattern lesson: complex FE feature mirror với type extend + new component → em main solo more reliable than Implementer. Pending: Reviewer pre-commit Plan B cumulative + push remote + CICD verify + docs/STATUS update. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contracts/ContractDetailContent.tsx | 41 +++++++++++++++++++ fe-admin/src/types/contracts.ts | 24 +++++++++++ .../contracts/ContractDetailContent.tsx | 41 +++++++++++++++++++ fe-user/src/types/contracts.ts | 24 +++++++++++ 4 files changed, 130 insertions(+) diff --git a/fe-admin/src/components/contracts/ContractDetailContent.tsx b/fe-admin/src/components/contracts/ContractDetailContent.tsx index b6ca917..cd5ce88 100644 --- a/fe-admin/src/components/contracts/ContractDetailContent.tsx +++ b/fe-admin/src/components/contracts/ContractDetailContent.tsx @@ -182,6 +182,47 @@ export function ContractDetailContent({ + {/* [Plan B S29 2026-05-22 Chunk E3] Section 5 — Ý kiến cấp duyệt V2 dynamic. + Mirror PE LevelOpinionsSectionV2 pattern. Chỉ render khi V2 pin + (approvalWorkflowId set). V1 legacy contract KHÔNG hiển thị. */} + {c.approvalWorkflowId && ( +
+

+ + Ý kiến cấp duyệt (Quy trình V2) +

+ {(c.levelOpinions?.length ?? 0) === 0 ? ( +

Chưa có ý kiến — workflow vừa bắt đầu hoặc chưa có ai duyệt.

+ ) : ( +
    + {c.levelOpinions!.map(o => { + const adminProxy = o.signedByUserId !== o.approverUserId + return ( +
  • +
    + + Bước {o.stepOrder} {o.stepName ? `(${o.stepName})` : ''} — Cấp {o.levelOrder} + {o.levelName ? ` (${o.levelName})` : ''} + + {new Date(o.signedAt).toLocaleString('vi-VN')} +
    +

    {o.comment}

    +
    + NV duyệt: {o.approverFullName ?? o.approverUserId.slice(0, 8)} + {adminProxy && ( + + ⚡ Admin duyệt thay ({o.signedByFullName ?? o.signedByUserId.slice(0, 8)}) + + )} +
    +
  • + ) + })} +
+ )} +
+ )} + setActionOpen(false)} diff --git a/fe-admin/src/types/contracts.ts b/fe-admin/src/types/contracts.ts index 9025d16..6844810 100644 --- a/fe-admin/src/types/contracts.ts +++ b/fe-admin/src/types/contracts.ts @@ -171,4 +171,28 @@ export type ContractDetail = { comments: ContractComment[] attachments: ContractAttachment[] workflow: WorkflowSummary + // [Plan B S29 2026-05-22 Chunk E3] V2 workflow state — mirror PE pattern. + // Khi pin V2 (approvalWorkflowId set) → render Section 5 dynamic. + // V1 contract: 3 fields = null. + approvalWorkflowId: string | null + currentApprovalLevelOrder: number | null + levelOpinions: ContractLevelOpinion[] | null +} + +// [Plan B S29 2026-05-22 Chunk E3] Mirror BE ContractLevelOpinionDto 12 fields. +// Service ApproveV2Async UPSERT (Plan B Chunk B2). Comment empty → "(duyệt — +// không ý kiến)". signedByUserId !== approverUserId → banner "Admin duyệt thay". +export type ContractLevelOpinion = { + id: string + approvalWorkflowLevelId: string + stepOrder: number + stepName: string + levelOrder: number + levelName: string | null + approverUserId: string + approverFullName: string | null + comment: string + signedAt: string + signedByUserId: string + signedByFullName: string | null } diff --git a/fe-user/src/components/contracts/ContractDetailContent.tsx b/fe-user/src/components/contracts/ContractDetailContent.tsx index b6ca917..cd5ce88 100644 --- a/fe-user/src/components/contracts/ContractDetailContent.tsx +++ b/fe-user/src/components/contracts/ContractDetailContent.tsx @@ -182,6 +182,47 @@ export function ContractDetailContent({ + {/* [Plan B S29 2026-05-22 Chunk E3] Section 5 — Ý kiến cấp duyệt V2 dynamic. + Mirror PE LevelOpinionsSectionV2 pattern. Chỉ render khi V2 pin + (approvalWorkflowId set). V1 legacy contract KHÔNG hiển thị. */} + {c.approvalWorkflowId && ( +
+

+ + Ý kiến cấp duyệt (Quy trình V2) +

+ {(c.levelOpinions?.length ?? 0) === 0 ? ( +

Chưa có ý kiến — workflow vừa bắt đầu hoặc chưa có ai duyệt.

+ ) : ( +
    + {c.levelOpinions!.map(o => { + const adminProxy = o.signedByUserId !== o.approverUserId + return ( +
  • +
    + + Bước {o.stepOrder} {o.stepName ? `(${o.stepName})` : ''} — Cấp {o.levelOrder} + {o.levelName ? ` (${o.levelName})` : ''} + + {new Date(o.signedAt).toLocaleString('vi-VN')} +
    +

    {o.comment}

    +
    + NV duyệt: {o.approverFullName ?? o.approverUserId.slice(0, 8)} + {adminProxy && ( + + ⚡ Admin duyệt thay ({o.signedByFullName ?? o.signedByUserId.slice(0, 8)}) + + )} +
    +
  • + ) + })} +
+ )} +
+ )} + setActionOpen(false)} diff --git a/fe-user/src/types/contracts.ts b/fe-user/src/types/contracts.ts index 9025d16..6844810 100644 --- a/fe-user/src/types/contracts.ts +++ b/fe-user/src/types/contracts.ts @@ -171,4 +171,28 @@ export type ContractDetail = { comments: ContractComment[] attachments: ContractAttachment[] workflow: WorkflowSummary + // [Plan B S29 2026-05-22 Chunk E3] V2 workflow state — mirror PE pattern. + // Khi pin V2 (approvalWorkflowId set) → render Section 5 dynamic. + // V1 contract: 3 fields = null. + approvalWorkflowId: string | null + currentApprovalLevelOrder: number | null + levelOpinions: ContractLevelOpinion[] | null +} + +// [Plan B S29 2026-05-22 Chunk E3] Mirror BE ContractLevelOpinionDto 12 fields. +// Service ApproveV2Async UPSERT (Plan B Chunk B2). Comment empty → "(duyệt — +// không ý kiến)". signedByUserId !== approverUserId → banner "Admin duyệt thay". +export type ContractLevelOpinion = { + id: string + approvalWorkflowLevelId: string + stepOrder: number + stepName: string + levelOrder: number + levelName: string | null + approverUserId: string + approverFullName: string | null + comment: string + signedAt: string + signedByUserId: string + signedByFullName: string | null }