[CLAUDE] Backout: Chunk D — K5 cleanup F2 zombie endpoint + UsersPage column + DTO field

Reviewer K2 Major #1: PATCH /api/users/{id}/allow-skip-final endpoint Admin tick =
NoOp swallow silent (K1 sentinel → confusion UX). Full backout Plan D S22 stack:

BE drop (7 files):
- UsersController.cs: DELETE PATCH /allow-skip-final endpoint + SetAllowDrafterSkipToFinalBody record
- UserFeatures.cs: DELETE SetUserAllowDrafterSkipToFinalCommand + Handler
                    + UserDto.AllowDrafterSkipToFinal field
                    + list/get DTO mapping sentinel-false references
- ApprovalWorkflow.cs: REWRITE stale narrative line 78-80 (Reviewer Major #2 Mig 31 semantic)
                       + docstring AllowApproverSkipToFinal line 108 clean stale Users storage ref
- PurchaseEvaluationFeatures.cs: REWRITE Command DTO comment line 401 (Reviewer Minor #3)
- ApprovalWorkflowConfiguration.cs: APPEND Mig 31 narrative line 22-24 (Reviewer Minor #4)
                                     + clean storage move comment line 87
- ApprovalWorkflowV2AdminFeatures.cs: clean DTO comment line 58 stale "F2 xuống User table"
- IPurchaseEvaluationWorkflowService.cs + PurchaseEvaluationDtos.cs: clean stale
  "storage Users.AllowDrafterSkipToFinal" comments

FE Admin drop (2 files):
- UsersPage.tsx: DELETE "Skip cuối" column + FastForward badge + FastForward import
                  + allowSkipMut mutation hook + FastForward toggle button
- types/users.ts: DELETE allowDrafterSkipToFinal field

fe-user KHÔNG đụng (no UsersPage admin-only; K6 sẽ handle Workspace Drafter checkbox).
FE Designer page KHÔNG đụng (K3 done; 2 stale comment leftover deferred K6).

Plan K refactor F2 storage Users → Levels (Mig 31) complete cumulative cleanup.
Pattern reusable: post-refactor full cleanup (BE endpoint + Command + DTO + FE column
+ types + stale narratives) atomic 1 commit thay vì leak zombie state.

Verify:
- dotnet build production projects 0 err (2 pre-existing DocxRenderer warn)
- npm build fe-admin 0 TS err (no new warning)
- Grep AllowDrafterSkipToFinal + allow-skip-final + allowDrafterSkipToFinal zero results
  across src/Backend (excl Migrations history) + fe-admin/src

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-14 23:27:12 +07:00
parent dd52d16ca9
commit 2ea8977d0f
11 changed files with 37 additions and 92 deletions

View File

@ -204,6 +204,7 @@ KHÔNG `*` / `latest`. Critical pins:
## 📅 Recent activity (last 10 FIFO) ## 📅 Recent activity (last 10 FIFO)
- **2026-05-14 (S23 t1, K5 Chunk D PASS):** Cleanup zombie F2 endpoint + UsersPage column + DTO field + stale narrative comments (Reviewer Major #1 + Major #2 + Minor #3 + Minor #4). Pattern post-refactor full cleanup atomic 1 commit. BE 7 file (UsersController.cs DELETE PATCH /allow-skip-final endpoint + SetAllowDrafterSkipToFinalBody record; UserFeatures.cs DELETE UserDto field + SetUserAllowDrafterSkipToFinalCommand + Handler + sentinel-false mappings cleanup; ApprovalWorkflow.cs REWRITE stale narrative line 78-80 Mig 31 semantic + docstring line 108; PurchaseEvaluationFeatures.cs REWRITE Command DTO comment line 401; ApprovalWorkflowConfiguration.cs APPEND Mig 31 narrative line 22-24 + clean storage move comment line 87; ApprovalWorkflowV2AdminFeatures.cs clean DTO comment line 58; IPurchaseEvaluationWorkflowService.cs + PurchaseEvaluationDtos.cs clean stale "storage Users.AllowDrafterSkipToFinal" references) + FE Admin 2 file (UsersPage.tsx DELETE "Skip cuối" column TableHeader/TableCell + FastForward import + allowSkipMut mutation hook + FastForward toggle button; types/users.ts DELETE allowDrafterSkipToFinal field). fe-user KHÔNG đụng (no UsersPage admin-only + K6 sẽ handle Workspace Drafter checkbox), FE Designer page KHÔNG đụng (K3 done — 2 stale comment line 75 + 504 leftover deferred K6). Grep `AllowDrafterSkipToFinal` + `allow-skip-final` + `allowDrafterSkipToFinal` + `Skip cuối` + `FastForward` ZERO results across src/Backend (excl migrations) + fe-admin/src. Build BE production projects clean (0 err, 2 pre-existing DocxRenderer warn). Build fe-admin clean (0 TS err, 0 new warn). Diff +42/-94 LOC trên 9 file. Token ~12k. K6 Workspace Drafter checkbox cleanup next.
- **2026-05-14 (S23 t1, K3 Chunk C PASS):** FE Admin Designer 7th checkbox AllowApproverSkipToFinal + banner rewrite. Pattern Mig 29/30 admin opt-in per-slot mirror **reinforced 3×** cumulative (Mig 29 F1+F3 5 checkbox + Mig 30 F4 1 checkbox + Mig 31 F2-refactor 1 checkbox = 7 checkbox total per slot). Cookie-cutter 1 file fe-admin only (`ApprovalWorkflowsV2Page.tsx`, fe-user no Designer per Investigator K0 S1). 7 sub-items atomic: (1) LevelDto type +`allowApproverSkipToFinal: boolean`, (2) EditLevelEntry type +same, (3) `makeDefaultLevelEntry` default false, (4) `copyFromDefinition` propagate `?? false`, (5) inline checkbox row position **cuối list** sau F4 AllowApproverEditBudget logical grouping (Edit Section 2 → Edit Budget → Skip to Final), (6) banner rewrite line ~623 từ "F2 cấu hình ở User Management" (Plan D S22 stale) → "Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới (Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối)", (7) POST/PATCH mutation body `levels.map` +allowApproverSkipToFinal. Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395.74 KB (unchanged trivial vs baseline). Diff +26/-7 LOC. Token ~6k. K5 next chunk cleanup zombie endpoint + UsersPage column. - **2026-05-14 (S23 t1, K3 Chunk C PASS):** FE Admin Designer 7th checkbox AllowApproverSkipToFinal + banner rewrite. Pattern Mig 29/30 admin opt-in per-slot mirror **reinforced 3×** cumulative (Mig 29 F1+F3 5 checkbox + Mig 30 F4 1 checkbox + Mig 31 F2-refactor 1 checkbox = 7 checkbox total per slot). Cookie-cutter 1 file fe-admin only (`ApprovalWorkflowsV2Page.tsx`, fe-user no Designer per Investigator K0 S1). 7 sub-items atomic: (1) LevelDto type +`allowApproverSkipToFinal: boolean`, (2) EditLevelEntry type +same, (3) `makeDefaultLevelEntry` default false, (4) `copyFromDefinition` propagate `?? false`, (5) inline checkbox row position **cuối list** sau F4 AllowApproverEditBudget logical grouping (Edit Section 2 → Edit Budget → Skip to Final), (6) banner rewrite line ~623 từ "F2 cấu hình ở User Management" (Plan D S22 stale) → "Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới (Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối)", (7) POST/PATCH mutation body `levels.map` +allowApproverSkipToFinal. Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395.74 KB (unchanged trivial vs baseline). Diff +26/-7 LOC. Token ~6k. K5 next chunk cleanup zombie endpoint + UsersPage column.
- **2026-05-14 (S23 t1, K1 Chunk A PASS):** Mig 31 schema swap F2 storage Users → ApprovalWorkflowLevels. Pattern Mig 29 ADD-DROP no-BACKFILL Option A (accept lose 4 prod user value `fin.pp` + `pm.nv` + `nv.test` + `truong.nguyen`). Cookie-cutter 6 BE file (User.cs -1 prop + ApprovalWorkflow.cs +1 prop `AllowApproverSkipToFinal` per-Approver-slot + ApprovalWorkflowConfiguration.cs +HasDefaultValue + PurchaseEvaluationWorkflowService.cs surgical -37 LOC F2 Drafter SUBMIT branch line 121-157 stub + Mig 3-file). TransitionAsync `bool skipToFinal` 8th param KEPT cho K2 repurpose APPROVE STEP. 4 Application compile-break sites (UserFeatures.cs LIST + GET DTO mapping + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures.cs drafter flag = false) patched với sentinel `false` + K2 marker comment (DTO/Command signature unchanged per spec — K2 sẽ refactor). Mig 31 Up() manual reorder ADD-DROP correct (no BACKFILL). Both DBs Dev + Design applied successful. Build production projects clean 0 err 0 warn. Test compile error `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` left for K7 chunk (spec exclude test scope). Pattern `feedback_per_nv_permission_scope.md` reinforced 3× cumulative (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2-refactor). UserConfiguration.cs file không tồn tại — User entity configured inline `ApplicationDbContext.OnModelCreating` ~line 86, không có HasDefaultValue cho `AllowDrafterSkipToFinal`, EF picks prop change tự động. - **2026-05-14 (S23 t1, K1 Chunk A PASS):** Mig 31 schema swap F2 storage Users → ApprovalWorkflowLevels. Pattern Mig 29 ADD-DROP no-BACKFILL Option A (accept lose 4 prod user value `fin.pp` + `pm.nv` + `nv.test` + `truong.nguyen`). Cookie-cutter 6 BE file (User.cs -1 prop + ApprovalWorkflow.cs +1 prop `AllowApproverSkipToFinal` per-Approver-slot + ApprovalWorkflowConfiguration.cs +HasDefaultValue + PurchaseEvaluationWorkflowService.cs surgical -37 LOC F2 Drafter SUBMIT branch line 121-157 stub + Mig 3-file). TransitionAsync `bool skipToFinal` 8th param KEPT cho K2 repurpose APPROVE STEP. 4 Application compile-break sites (UserFeatures.cs LIST + GET DTO mapping + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures.cs drafter flag = false) patched với sentinel `false` + K2 marker comment (DTO/Command signature unchanged per spec — K2 sẽ refactor). Mig 31 Up() manual reorder ADD-DROP correct (no BACKFILL). Both DBs Dev + Design applied successful. Build production projects clean 0 err 0 warn. Test compile error `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` left for K7 chunk (spec exclude test scope). Pattern `feedback_per_nv_permission_scope.md` reinforced 3× cumulative (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2-refactor). UserConfiguration.cs file không tồn tại — User entity configured inline `ApplicationDbContext.OnModelCreating` ~line 86, không có HasDefaultValue cho `AllowDrafterSkipToFinal`, EF picks prop change tự động.
- **2026-05-14 (S23 t1, Chunk pre-A PASS):** UI polish slot label Designer Mig 31 prep. File `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` line 873 replaced `Quyền duyệt NV #{ei + 1}``Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}`. Pattern: lookup user fullName từ `usersList` query (Option A — DTO `EditLevelEntry` không có `approverFullName` field, chỉ `LevelDto` BE response có `approverUserName` nhưng edit state dùng `EditLevelEntry`). `usersList` đã in scope ~line 500, pattern lookup `.find(x => x.id === e.approverUserId)` đã dùng tại line 535 cho validation. Cookie-cutter 1 file fe-admin only (fe-user KHÔNG có Designer per Investigator K0 S1). Tailwind classes preserved (`mb-1 text-[10px] font-medium uppercase text-amber-700`). Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395 KB (unchanged trivial). Token ~5k. - **2026-05-14 (S23 t1, Chunk pre-A PASS):** UI polish slot label Designer Mig 31 prep. File `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` line 873 replaced `Quyền duyệt NV #{ei + 1}``Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}`. Pattern: lookup user fullName từ `usersList` query (Option A — DTO `EditLevelEntry` không có `approverFullName` field, chỉ `LevelDto` BE response có `approverUserName` nhưng edit state dùng `EditLevelEntry`). `usersList` đã in scope ~line 500, pattern lookup `.find(x => x.id === e.approverUserId)` đã dùng tại line 535 cho validation. Cookie-cutter 1 file fe-admin only (fe-user KHÔNG có Designer per Investigator K0 S1). Tailwind classes preserved (`mb-1 text-[10px] font-medium uppercase text-amber-700`). Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395 KB (unchanged trivial). Token ~5k.

View File

@ -1,6 +1,6 @@
import { useState, type FormEvent } from 'react' import { useState, type FormEvent } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck, FastForward } from 'lucide-react' import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
import { DataTable, Pagination, type Column } from '@/components/DataTable' import { DataTable, Pagination, type Column } from '@/components/DataTable'
@ -175,18 +175,9 @@ export function UsersPage() {
onError: err => toast.error(getErrorMessage(err)), onError: err => toast.error(getErrorMessage(err)),
}) })
// F2 per-Drafter (Mig 29): toggle AllowDrafterSkipToFinal. Khi true, Drafter // Mig 31 (S23 t1 Plan K Chunk D) — DELETED allowSkipMut. F2 semantic + storage
// được tick "Gửi thẳng Cấp cuối" trong PE Workspace để skip Bước/Cấp trung // refactor sang ApprovalWorkflowLevels (per-Approver slot, admin opt-in qua
// gian và bay thẳng tới Cấp cuối workflow. // Workflow Designer page). KHÔNG còn per-Drafter user toggle ở User Management.
const allowSkipMut = useMutation({
mutationFn: (u: User) =>
api.patch(`/users/${u.id}/allow-skip-final`, { allowDrafterSkipToFinal: !u.allowDrafterSkipToFinal }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['users'] })
toast.success('Đã cập nhật quyền gửi thẳng Cấp cuối')
},
onError: err => toast.error(getErrorMessage(err)),
})
function nextPositionLevel(current: number | null): number | null { function nextPositionLevel(current: number | null): number | null {
// Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null // Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null
@ -302,21 +293,6 @@ export function UsersPage() {
<span className="text-xs text-slate-400"></span> <span className="text-xs text-slate-400"></span>
), ),
}, },
{
key: 'allowDrafterSkipToFinal',
header: 'Skip cuối',
width: 'w-20',
align: 'center',
render: u =>
u.allowDrafterSkipToFinal ? (
<span title="Drafter được gửi PE thẳng Cấp cuối workflow (skip Bước/Cấp trung gian)" className="inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] text-violet-700">
<FastForward className="h-3 w-3" />
skip
</span>
) : (
<span className="text-xs text-slate-400"></span>
),
},
{ key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) }, { key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) },
{ {
key: 'actions', key: 'actions',
@ -362,14 +338,6 @@ export function UsersPage() {
{u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'} {u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'}
</span> </span>
</Button> </Button>
<Button
size="sm"
variant="ghost"
onClick={() => allowSkipMut.mutate(u)}
title={u.allowDrafterSkipToFinal ? 'Tắt quyền skip — Drafter phải tuần tự qua mọi Bước/Cấp' : 'Bật quyền skip — Drafter được gửi PE thẳng Cấp cuối workflow'}
>
<FastForward className={`h-3.5 w-3.5 ${u.allowDrafterSkipToFinal ? 'text-violet-600' : 'text-slate-400'}`} />
</Button>
<Button size="sm" variant="ghost" onClick={() => toggleActiveMut.mutate(u)} title={u.isActive ? 'Vô hiệu hóa' : 'Kích hoạt'}> <Button size="sm" variant="ghost" onClick={() => toggleActiveMut.mutate(u)} title={u.isActive ? 'Vô hiệu hóa' : 'Kích hoạt'}>
{u.isActive ? <XCircle className="h-3.5 w-3.5 text-red-500" /> : <CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />} {u.isActive ? <XCircle className="h-3.5 w-3.5 text-red-500" /> : <CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />}
</Button> </Button>

View File

@ -11,7 +11,6 @@ export type User = {
position: string | null position: string | null
canBypassReview: boolean canBypassReview: boolean
positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external
allowDrafterSkipToFinal: boolean // Mig 29 — F2: Drafter được gửi thẳng Cấp cuối workflow PE
} }
// Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step. // Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step.

View File

@ -85,21 +85,13 @@ public class UsersController(IMediator mediator) : ControllerBase
return NoContent(); return NoContent();
} }
// Mig 29 F2 per-Drafter: admin toggle AllowDrafterSkipToFinal cho user. Khi // Mig 31 (S23 t1 Plan K Chunk D) — DELETED PATCH F2 endpoint per-Drafter.
// true, Drafter có thể tick "Gửi thẳng Cấp cuối" trong PE Workspace để bay // F2 semantic + storage refactor sang ApprovalWorkflowLevels per-Approver
// thẳng tới Cấp cuối workflow. Mặc định false. // slot (AllowApproverSkipToFinal, admin opt-in qua Workflow Designer).
[HttpPatch("{id:guid}/allow-skip-final")] // Plan K backout zombie endpoint NoOp.
[Authorize(Policy = "Users.Update")]
public async Task<IActionResult> SetAllowDrafterSkipToFinal(
Guid id, [FromBody] SetAllowDrafterSkipToFinalBody body, CancellationToken ct)
{
await mediator.Send(new SetUserAllowDrafterSkipToFinalCommand(id, body.AllowDrafterSkipToFinal), ct);
return NoContent();
}
} }
public record AssignRolesBody(List<string> Roles); public record AssignRolesBody(List<string> Roles);
public record ResetPasswordBody(string NewPassword); public record ResetPasswordBody(string NewPassword);
public record SetBypassReviewBody(bool CanBypassReview); public record SetBypassReviewBody(bool CanBypassReview);
public record SetPositionLevelBody(int? PositionLevel); public record SetPositionLevelBody(int? PositionLevel);
public record SetAllowDrafterSkipToFinalBody(bool AllowDrafterSkipToFinal);

View File

@ -55,9 +55,10 @@ public record AwDefinitionDto(
string? Description, string? Description,
bool IsActive, bool IsActive,
bool IsUserSelectable, bool IsUserSelectable,
// Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: 5 flag (F1+F3) xuống // Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: F1+F3 xuống AwLevelDto
// AwLevelDto (per slot Approver), F2 AllowDrafterSkipToFinal xuống User table // (per slot Approver). Workflow-level Mig 28 dropped.
// (per-Drafter user). Workflow-level Mig 28 dropped. // Mig 31 (S23 t1) — F2 cũng refactor xuống Level slot (AllowApproverSkipToFinal
// trong AwLevelDto) per-Approver scope ChoDuyet — KHÔNG còn per-Drafter user scope.
DateTime? ActivatedAt, DateTime? ActivatedAt,
DateTime CreatedAt, DateTime CreatedAt,
List<AwStepDto> Steps); List<AwStepDto> Steps);

View File

@ -84,7 +84,7 @@ public record PurchaseEvaluationChangelogDto(
// Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách". // Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách".
// Mig 31 (S23 t1) — F2 refactor sang Approver scope ChoDuyet: +AllowApproverSkipToFinal // Mig 31 (S23 t1) — F2 refactor sang Approver scope ChoDuyet: +AllowApproverSkipToFinal
// cho phép Approver duyệt thẳng Cấp cuối (admin opt-in per slot). Storage cũ // cho phép Approver duyệt thẳng Cấp cuối (admin opt-in per slot). Storage cũ
// Users.AllowDrafterSkipToFinal đã drop, semantic Drafter-from-Nháp deprecated. // trên Users table đã drop, semantic Drafter-from-Nháp deprecated.
public record ApprovalWorkflowOptionsDto( public record ApprovalWorkflowOptionsDto(
bool AllowReturnOneLevel, bool AllowReturnOneLevel,
bool AllowReturnOneStep, bool AllowReturnOneStep,

View File

@ -398,7 +398,8 @@ public record TransitionPurchaseEvaluationCommand(
// Mig 28 (S21 t4) — F1 mode Trả lại (optional, null = default Drafter) // Mig 28 (S21 t4) — F1 mode Trả lại (optional, null = default Drafter)
WorkflowReturnMode? ReturnMode = null, WorkflowReturnMode? ReturnMode = null,
Guid? ReturnTargetUserId = null, Guid? ReturnTargetUserId = null,
// F2 — Drafter skip thẳng Cấp cuối khi trình duyệt (optional, default false) // F2 — Approver skip thẳng Cấp cuối lúc duyệt ChoDuyet (Mig 31 admin opt-in
// per slot, AllowApproverSkipToFinal). Default false.
bool SkipToFinal = false) : IRequest; bool SkipToFinal = false) : IRequest;
public class TransitionPurchaseEvaluationCommandValidator : AbstractValidator<TransitionPurchaseEvaluationCommand> public class TransitionPurchaseEvaluationCommandValidator : AbstractValidator<TransitionPurchaseEvaluationCommand>

View File

@ -16,7 +16,7 @@ public interface IPurchaseEvaluationWorkflowService
// - skipToFinal: F2 Approver during ChoDuyet duyệt thẳng Cấp cuối → set Phase=DaDuyet // - skipToFinal: F2 Approver during ChoDuyet duyệt thẳng Cấp cuối → set Phase=DaDuyet
// terminal trực tiếp, clear pointer. Mig 31 (S23 t1) refactor sang Approver scope: // terminal trực tiếp, clear pointer. Mig 31 (S23 t1) refactor sang Approver scope:
// matchingLevel.AllowApproverSkipToFinal phải true (admin opt-in per slot). // matchingLevel.AllowApproverSkipToFinal phải true (admin opt-in per slot).
// Semantic cũ Drafter-from-Nháp đã deprecated, storage Users.AllowDrafterSkipToFinal dropped. // Semantic cũ Drafter-from-Nháp đã deprecated + storage cũ trên Users table đã drop.
Task TransitionAsync( Task TransitionAsync(
PurchaseEvaluation evaluation, PurchaseEvaluation evaluation,
PurchaseEvaluationPhase targetPhase, PurchaseEvaluationPhase targetPhase,

View File

@ -22,8 +22,7 @@ public record UserDto(
string? DepartmentName, string? DepartmentName,
string? Position, string? Position,
bool CanBypassReview, bool CanBypassReview,
int? PositionLevel, // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user. int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
bool AllowDrafterSkipToFinal); // Mig 29 — F2 per-Drafter: cho phép Drafter gửi thẳng Cấp cuối khi tạo PE.
// ========== LIST ========== // ========== LIST ==========
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>; public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
@ -62,9 +61,7 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IApplicationDb
var roles = await userManager.GetRolesAsync(u); var roles = await userManager.GetRolesAsync(u);
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now; var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null; string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels. items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel));
// DTO field kept transiently (K2 sẽ refactor DTO + drop field). Sentinel false.
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, false));
} }
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize); return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
@ -86,9 +83,7 @@ public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbCo
string? deptName = null; string? deptName = null;
if (u.DepartmentId is { } did) if (u.DepartmentId is { } did)
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct); deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels. return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel);
// DTO field kept transiently (K2 sẽ refactor DTO + drop field). Sentinel false.
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, false);
} }
} }
@ -328,23 +323,7 @@ public class SetUserPositionLevelCommandHandler(UserManager<User> userManager)
} }
} }
// ========== SET ALLOW DRAFTER SKIP TO FINAL (Mig 29 — F2 per-Drafter) ========== // Mig 31 (S23 t1 Plan K Chunk D) — DELETED F2 Command + Handler per-Drafter.
// Admin toggle AllowDrafterSkipToFinal cho 1 user. Khi true, user (Drafter) được // F2 semantic + storage refactor sang ApprovalWorkflowLevels (per-Approver
// dùng checkbox "Gửi thẳng Cấp cuối" trong PE Workspace để skip toàn bộ Bước/Cấp // slot, admin opt-in qua Workflow Designer). Plan K backout zombie Command.
// trung gian và bay thẳng tới Cấp cuối. Mặc định false (an toàn — Drafter phải // Replaced bởi Workflow Designer per-Level toggle trong ApprovalWorkflowV2AdminFeatures.
// tuần tự qua mọi Bước/Cấp theo workflow).
public record SetUserAllowDrafterSkipToFinalCommand(Guid Id, bool AllowDrafterSkipToFinal) : IRequest;
public class SetUserAllowDrafterSkipToFinalCommandHandler(UserManager<User> userManager)
: IRequestHandler<SetUserAllowDrafterSkipToFinalCommand>
{
public async Task Handle(SetUserAllowDrafterSkipToFinalCommand request, CancellationToken ct)
{
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels.AllowApproverSkipToFinal
// (per-Approver slot, set via Workflow Designer). Command kept transiently NoOp until K2
// sẽ replace với SetApprovalWorkflowLevelAllowApproverSkipToFinalCommand + FE remove toggle.
_ = await userManager.FindByIdAsync(request.Id.ToString())
?? throw new NotFoundException("User", request.Id);
await Task.CompletedTask;
}
}

View File

@ -75,8 +75,9 @@ public class ApprovalWorkflowLevel : BaseEntity
// tick stick per Level row (KHÔNG còn workflow-level cũ Mig 28). // tick stick per Level row (KHÔNG còn workflow-level cũ Mig 28).
// //
// F1 (4 mode Trả lại) + F3 (Edit Section 2) = quyền của Approver Level. // F1 (4 mode Trả lại) + F3 (Edit Section 2) = quyền của Approver Level.
// F2 (Drafter skip) đã move sang Users.AllowDrafterSkipToFinal (per-Drafter // F2 đã move xuống Level slot này (Mig 31 — xem prop AllowApproverSkipToFinal).
// user — không liên quan slot Approver). // Semantic mới: Approver during ChoDuyet skip thẳng Cấp cuối (admin opt-in
// per slot, KHÔNG còn per-Drafter user scope cũ).
// //
// Backfill Mig 29: copy từ workflow-level Allow* cũ → all Levels của workflow. // Backfill Mig 29: copy từ workflow-level Allow* cũ → all Levels của workflow.
// Default backward compat: AllowReturnToDrafter=true (S17 fallback). 4 flag // Default backward compat: AllowReturnToDrafter=true (S17 fallback). 4 flag
@ -104,12 +105,12 @@ public class ApprovalWorkflowLevel : BaseEntity
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet. /// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
public bool AllowApproverEditBudget { get; set; } public bool AllowApproverEditBudget { get; set; }
/// F2 (Mig 31 — S23 t1 Plan K) — REFACTOR semantic + storage từ /// F2 (Mig 31 — S23 t1 Plan K) — REFACTOR semantic + storage từ per-Drafter
/// `Users.AllowDrafterSkipToFinal`. Cho phép NV slot này (khi đang duyệt /// (cũ Mig 29 ở Users table, đã DROP) sang per-Approver slot. Cho phép NV slot
/// ChoDuyet) Approve skip thẳng Cấp cuối, bỏ qua mọi Bước/Cấp trung gian /// này (khi đang duyệt ChoDuyet) Approve skip thẳng Cấp cuối, bỏ qua mọi
/// còn lại. Default false (admin opt-in per slot). Mirror F3+F4 admin opt-in /// Bước/Cấp trung gian còn lại. Default false (admin opt-in per slot).
/// per-Approver pattern (Mig 29 + Mig 30) reinforced 3× cumulative. /// Mirror F3+F4 admin opt-in per-Approver pattern (Mig 29 + Mig 30)
/// NO BACKFILL — 4 prod user lose value cũ per bro Option A. /// reinforced 3× cumulative. NO BACKFILL — 4 prod user lose value cũ per bro Option A.
public bool AllowApproverSkipToFinal { get; set; } public bool AllowApproverSkipToFinal { get; set; }
public ApprovalWorkflowStep? Step { get; set; } public ApprovalWorkflowStep? Step { get; set; }

View File

@ -22,6 +22,9 @@ public class ApprovalWorkflowConfiguration : IEntityTypeConfiguration<ApprovalWo
// Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 (S21 t5) — refactor sang // Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 (S21 t5) — refactor sang
// per-NV (Level table cho F1+F3, Users table cho F2). Backfill bulk SQL // per-NV (Level table cho F1+F3, Users table cho F2). Backfill bulk SQL
// preserve config admin từ S21 t4 trước khi drop. // preserve config admin từ S21 t4 trước khi drop.
// + Mig 31 (S23 t1) F2 refactor sang Approver scope per-Level slot —
// AllowApproverSkipToFinal trên Level table (admin opt-in per slot,
// KHÔNG còn per-Drafter user scope cũ Mig 29).
} }
} }
@ -84,7 +87,7 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
// Mig 31 (S23 t1 Plan K Chunk A) — F2 per-NV REFACTOR: cho phép Approver // Mig 31 (S23 t1 Plan K Chunk A) — F2 per-NV REFACTOR: cho phép Approver
// slot này skip thẳng Cấp cuối lúc đang duyệt ChoDuyet. Default false // slot này skip thẳng Cấp cuối lúc đang duyệt ChoDuyet. Default false
// (admin opt-in). Semantic + storage move từ Users.AllowDrafterSkipToFinal. // (admin opt-in). Semantic + storage cũ ở Users table per-Drafter đã drop.
e.Property(x => x.AllowApproverSkipToFinal).HasDefaultValue(false); e.Property(x => x.AllowApproverSkipToFinal).HasDefaultValue(false);
} }
} }