Compare commits

...

2 Commits

Author SHA1 Message Date
b04a11a62f [CLAUDE] FE-PE: S22+5 Chunk B — Designer checkbox +AllowApproverEditBudget per slot + Section read flag (mirror 2 app)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m23s
FE Admin Designer (ApprovalWorkflowsV2Page.tsx):
- LevelDto + EditLevelEntry +allowApproverEditBudget
- copyFromDefinition + makeDefaultLevelEntry propagate default false
- POST body include allowApproverEditBudget cho mỗi Level slot
- NEW checkbox UI per Level inline panel:
  "Cho phép chỉnh sửa Section ngân sách lúc đang duyệt"
  (col-span-2, mirror pattern allowApproverEditDetails Mig 29)

FE Types mirror 2 app:
- fe-admin + fe-user `ApprovalWorkflowOptions` +allowApproverEditBudget

FE BudgetAdjustSection refactor (mirror 2 app):
- Trước: isApproverChoDuyet = phase ChoDuyet + actor in approvers
- Sau: isApproverChoDuyet = phase ChoDuyet + actor in approvers
  + currentLevelOptions.allowApproverEditBudget=true (per slot opt-in)
- Drafter scope Nháp/Trả lại unchanged
- Admin bypass unchanged

UX impact:
- Admin Designer phải tick checkbox cho NV slot mới được edit ngân sách lúc duyệt
- Nếu KHÔNG tick → button "Điều chỉnh" trong Section 5 KHÔNG hiện cho approver
- Drafter vẫn edit bình thường khi phiếu Nháp/Trả lại

Verify:
- npm run build fe-admin — 569ms pass
- npm run build fe-user — 528ms pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:12:43 +07:00
b079b27343 [CLAUDE] PE-Workflow: S22+5 Chunk A — Mig 30 +AllowApproverEditBudget per-Level slot
Bro clarify spec S22+4:
- KHÔNG đổi logic edit ngân sách (Drafter Nháp/TraLai vẫn duy nhất default)
- Thêm flag per-NV slot trong Designer: "Cho phép NV này edit Section ngân sách
  lúc đang duyệt" (mirror pattern F3 AllowApproverEditDetails Mig 29)

Mig 30 `AddAllowApproverEditBudgetToLevels`:
- ALTER ApprovalWorkflowLevels +AllowApproverEditBudget bit NOT NULL DEFAULT 0
- 3-file rule (mig.cs + Designer.cs + Snapshot.cs)
- Apply LocalDB Dev + Design

Domain entity ApprovalWorkflowLevel +AllowApproverEditBudget (default false).
EF config HasDefaultValue(false). DTO AwLevelDto + ApprovalWorkflowOptionsDto
+ CreateAwLevelInput all extend +AllowApproverEditBudget.

PE GET handler populate currentLevelOptions thêm AllowApproverEditBudget từ
curLevel slot. Admin Designer GET/POST handler propagate flag.

AdjustBudgetCommand handler refactor ChoDuyet branch:
- Trước: check actor match level.ApproverUserId (cho phép mặc định)
- Sau: check level.AllowApproverEditBudget=true AND actor match ApproverUserId
  → throw ConflictException nếu slot chưa được cấp quyền

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Mig 30 applied Dev + Design DB

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:09:48 +07:00
13 changed files with 4034 additions and 12 deletions

View File

@ -974,9 +974,13 @@ function BudgetAdjustSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: b
const isDrafter = currentUser?.id != null && ev.drafterUserId === currentUser.id
const isDrafterPhase = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|| ev.phase === PurchaseEvaluationPhase.TraLai
// Approver currentLevel match — phase ChoDuyet + actor in current approval level
// F4 Approver scope (Mig 30): phase ChoDuyet + actor in currentApproval.approvers
// + currentLevel có flag AllowApproverEditBudget=true (admin Designer tick per slot).
const actorInCurrentLevel = ev.currentApproval?.approvers?.some(a => a.userId === currentUser?.id) ?? false
const isApproverChoDuyet = ev.phase === PurchaseEvaluationPhase.ChoDuyet && actorInCurrentLevel
const approverEditBudgetAllowed = ev.currentLevelOptions?.allowApproverEditBudget ?? false
const isApproverChoDuyet = ev.phase === PurchaseEvaluationPhase.ChoDuyet
&& actorInCurrentLevel
&& approverEditBudgetAllowed
const canAdjust = !readOnly && (isAdmin || (isDrafter && isDrafterPhase) || isApproverChoDuyet)

View File

@ -42,11 +42,13 @@ type LevelDto = {
approverUserName: string | null
approverEmail: string | null
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
allowApproverEditBudget: boolean
}
type StepDto = {
id: string
@ -86,11 +88,13 @@ type EditLevelEntry = {
approverUserId: string
// Mig 29 (S21 t5) — 5 Allow* per slot (default backward compat S17: chỉ
// AllowReturnToDrafter=true, 4 còn lại false).
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách (default false).
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
allowApproverEditBudget: boolean
}
type EditStep = { name: string; departmentId: string | null; levelEntries: EditLevelEntry[] }
@ -137,6 +141,7 @@ function copyFromDefinition(d: DefinitionDto): EditStep[] {
allowReturnToAssignee: l.allowReturnToAssignee ?? false,
allowReturnToDrafter: l.allowReturnToDrafter ?? true,
allowApproverEditDetails: l.allowApproverEditDetails ?? false,
allowApproverEditBudget: l.allowApproverEditBudget ?? false,
})),
}))
}
@ -152,6 +157,7 @@ function makeDefaultLevelEntry(order: LevelOrder, approverUserId: string): EditL
allowReturnToAssignee: false,
allowReturnToDrafter: true,
allowApproverEditDetails: false,
allowApproverEditBudget: false,
}
}
@ -553,6 +559,7 @@ function Designer({
allowReturnToAssignee: e.allowReturnToAssignee,
allowReturnToDrafter: e.allowReturnToDrafter,
allowApproverEditDetails: e.allowApproverEditDetails,
allowApproverEditBudget: e.allowApproverEditBudget,
})),
})),
})
@ -911,6 +918,15 @@ function Designer({
/>
<span>Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá) lúc đang duyệt</span>
</label>
<label className="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowApproverEditBudget}
onChange={e => updateField('allowApproverEditBudget', e.target.checked)}
/>
<span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
</label>
</div>
</div>
)

View File

@ -355,6 +355,7 @@ export type ApprovalWorkflowOptions = {
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
}
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE

View File

@ -981,8 +981,12 @@ function BudgetAdjustSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: b
const isDrafter = currentUser?.id != null && ev.drafterUserId === currentUser.id
const isDrafterPhase = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|| ev.phase === PurchaseEvaluationPhase.TraLai
// F4 Approver scope (Mig 30): ChoDuyet + actor in approvers + flag tick.
const actorInCurrentLevel = ev.currentApproval?.approvers?.some(a => a.userId === currentUser?.id) ?? false
const isApproverChoDuyet = ev.phase === PurchaseEvaluationPhase.ChoDuyet && actorInCurrentLevel
const approverEditBudgetAllowed = ev.currentLevelOptions?.allowApproverEditBudget ?? false
const isApproverChoDuyet = ev.phase === PurchaseEvaluationPhase.ChoDuyet
&& actorInCurrentLevel
&& approverEditBudgetAllowed
const canAdjust = !readOnly && (isAdmin || (isDrafter && isDrafterPhase) || isApproverChoDuyet)

View File

@ -353,6 +353,7 @@ export type ApprovalWorkflowOptions = {
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
}
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE

View File

@ -28,12 +28,14 @@ public record AwLevelDto(
string? ApproverUserName,
string? ApproverEmail,
// Mig 29 (S21 t5) — 5 advanced options per slot Approver (F1 mode Trả lại
// + F3 Edit Section 2). Mỗi NV trong workflow có quyền riêng.
// + F3 Edit Section 2). Mig 30 (S22+5) — F4 +AllowApproverEditBudget.
// Mỗi NV trong workflow có quyền riêng.
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowApproverEditDetails);
bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record AwStepDto(
Guid Id,
@ -152,7 +154,7 @@ public class GetAwAdminOverviewQueryHandler(
// Mig 29 (S21 t5) — 5 Allow* flag per slot Level
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email,
l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee,
l.AllowReturnToDrafter, l.AllowApproverEditDetails);
l.AllowReturnToDrafter, l.AllowApproverEditDetails, l.AllowApproverEditBudget);
}).ToList()
)).ToList());
@ -184,12 +186,13 @@ public record CreateAwLevelInput(
Guid ApproverUserId,
// Mig 29 (S21 t5) — 5 Allow* options per slot. Admin Designer tick per
// Level row. Default backward compat: AllowReturnToDrafter=true, 4 còn lại
// false (admin opt-in từng slot).
// false (admin opt-in từng slot). Mig 30 (S22+5) — F4 AllowApproverEditBudget.
bool AllowReturnOneLevel = false,
bool AllowReturnOneStep = false,
bool AllowReturnToAssignee = false,
bool AllowReturnToDrafter = true,
bool AllowApproverEditDetails = false);
bool AllowApproverEditDetails = false,
bool AllowApproverEditBudget = false);
public record CreateAwStepInput(
int Order,
@ -315,6 +318,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
AllowReturnToAssignee = l.AllowReturnToAssignee,
AllowReturnToDrafter = l.AllowReturnToDrafter,
AllowApproverEditDetails = l.AllowApproverEditDetails,
AllowApproverEditBudget = l.AllowApproverEditBudget,
}).ToList(),
})
.ToList(),

View File

@ -82,12 +82,14 @@ public record PurchaseEvaluationChangelogDto(
// FE eOffice filter Trả lại dropdown + Edit Section 2 enabled theo flag của
// Cấp hiện tại NV đang duyệt. Null nếu phiếu V1 legacy hoặc không ChoDuyet.
// F2 (Drafter skip) đã move sang `PeDetailBundleDto.DrafterAllowSkipToFinal`.
// Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách".
public record ApprovalWorkflowOptionsDto(
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowApproverEditDetails);
bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record PurchaseEvaluationWorkflowSummaryDto(
string PolicyName,

View File

@ -290,7 +290,9 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler(
}
else if (entity.Phase == PurchaseEvaluationPhase.ChoDuyet)
{
// Approver scope — actor phải là ApproverUserId của currentLevel
// F4 (Mig 30 — S22+5) — Approver scope chỉ accept khi
// currentLevel.AllowApproverEditBudget=true (admin Designer tick
// per slot) AND actor match ApproverUserId.
if (entity.ApprovalWorkflowId is not Guid awId)
throw new ConflictException("Phiếu V1 legacy không hỗ trợ điều chỉnh ngân sách lúc đang duyệt.");
if (entity.CurrentWorkflowStepIndex is not int csi || entity.CurrentApprovalLevelOrder is not int curLvl)
@ -307,7 +309,14 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler(
throw new ConflictException("Pointer step out of range — schema lỗi.");
var step = stepsOrdered[csi];
var level = step.Levels.FirstOrDefault(l => l.Order == curLvl);
if (level?.ApproverUserId != actorId)
if (level is null)
throw new ConflictException("Cấp duyệt không tìm thấy — schema lỗi.");
if (!level.AllowApproverEditBudget)
throw new ConflictException(
$"Cấp Approver hiện tại (Bước {step.Order} / Cấp {curLvl}) " +
"không được cấp quyền chỉnh sửa Section ngân sách. " +
"Liên hệ Admin Designer cấp quyền slot.");
if (level.ApproverUserId != actorId)
throw new ForbiddenException(
$"Chỉ NV phụ trách Bước {step.Order} / Cấp {curLvl} mới được điều chỉnh ngân sách lúc đang duyệt.");
actorTag = $"[Approver Bước {step.Order}/Cấp {curLvl}]";
@ -761,7 +770,8 @@ public class GetPurchaseEvaluationQueryHandler(
curLevel.AllowReturnOneStep,
curLevel.AllowReturnToAssignee,
curLevel.AllowReturnToDrafter,
curLevel.AllowApproverEditDetails);
curLevel.AllowApproverEditDetails,
curLevel.AllowApproverEditBudget);
}
}

View File

@ -98,5 +98,11 @@ public class ApprovalWorkflowLevel : BaseEntity
/// duyệt. KHÔNG đụng PE Header, KHÔNG reset workflow.
public bool AllowApproverEditDetails { get; set; }
/// F4 (Mig 30 — S22+5) — Cho phép NV này edit Section "Điều chỉnh ngân sách"
/// (BudgetId / BudgetManualName / BudgetManualAmount) lúc đang duyệt. Default
/// false (admin opt-in per slot). Logic edit ngân sách giữ nguyên Drafter
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
public bool AllowApproverEditBudget { get; set; }
public ApprovalWorkflowStep? Step { get; set; }
}

View File

@ -77,5 +77,9 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
e.Property(x => x.AllowReturnToAssignee).HasDefaultValue(false);
e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true);
e.Property(x => x.AllowApproverEditDetails).HasDefaultValue(false);
// Mig 30 (S22+5) — F4 per-NV: cho phép edit Section "Điều chỉnh ngân sách"
// lúc đang duyệt. Default false (admin opt-in).
e.Property(x => x.AllowApproverEditBudget).HasDefaultValue(false);
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAllowApproverEditBudgetToLevels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AllowApproverEditBudget",
table: "ApprovalWorkflowLevels",
type: "bit",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AllowApproverEditBudget",
table: "ApprovalWorkflowLevels");
}
}
}

View File

@ -188,6 +188,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("AllowApproverEditBudget")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("AllowApproverEditDetails")
.ValueGeneratedOnAdd()
.HasColumnType("bit")