[CLAUDE] PE: Workflow designer admin UI + Ý kiến 4 phòng ban (P1 Session 5)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s
==== 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_<Code> → /system/pe-workflows/<code> 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) <noreply@anthropic.com>
This commit is contained in:
@ -58,6 +58,7 @@ public interface IApplicationDbContext
|
||||
DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps { get; }
|
||||
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
||||
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
||||
DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions { get; }
|
||||
|
||||
// Module Ngân sách (Phase 7)
|
||||
DbSet<Budget> Budgets { get; }
|
||||
|
||||
@ -95,6 +95,15 @@ public record PurchaseEvaluationAttachmentDto(
|
||||
string? Note,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record PurchaseEvaluationDepartmentOpinionDto(
|
||||
Guid Id,
|
||||
PeDepartmentKind Kind,
|
||||
string KindLabel,
|
||||
string? Opinion,
|
||||
DateTime? SignedAt,
|
||||
Guid? UserId,
|
||||
string? UserName);
|
||||
|
||||
public record PurchaseEvaluationDetailBundleDto(
|
||||
Guid Id,
|
||||
string? MaPhieu,
|
||||
@ -122,4 +131,5 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
List<PurchaseEvaluationDetailDto> Details,
|
||||
List<PurchaseEvaluationApprovalDto> Approvals,
|
||||
List<PurchaseEvaluationAttachmentDto> Attachments,
|
||||
List<PurchaseEvaluationDepartmentOpinionDto> DepartmentOpinions,
|
||||
PurchaseEvaluationWorkflowSummaryDto Workflow);
|
||||
|
||||
@ -0,0 +1,152 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Contracts; // ChangelogAction
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
namespace SolutionErp.Application.PurchaseEvaluations;
|
||||
|
||||
// Ý kiến 4 phòng ban (Phê duyệt / CCM / MuaHàng / SM-PM) trên PHIẾU TRÌNH KÝ.
|
||||
// Upsert pattern — UPDATE in-place khi user đổi ý (không version), audit qua
|
||||
// PurchaseEvaluationChangelog. UNIQUE index (PEId, Kind) bảo vệ tối đa 1 row
|
||||
// mỗi loại phòng ban per phiếu.
|
||||
|
||||
// ========== UPSERT (Add nếu chưa có, Update nếu rồi) ==========
|
||||
|
||||
public record UpsertPeDepartmentOpinionCommand(
|
||||
Guid PurchaseEvaluationId,
|
||||
PeDepartmentKind Kind,
|
||||
string? Opinion,
|
||||
bool Sign) : IRequest<Guid>;
|
||||
// Sign=true → set SignedAt + UserId hiện tại (đóng dấu xác nhận).
|
||||
// Sign=false → chỉ lưu text Opinion (chưa ký).
|
||||
|
||||
public class UpsertPeDepartmentOpinionCommandValidator : AbstractValidator<UpsertPeDepartmentOpinionCommand>
|
||||
{
|
||||
public UpsertPeDepartmentOpinionCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PurchaseEvaluationId).NotEmpty();
|
||||
RuleFor(x => x.Kind).IsInEnum();
|
||||
RuleFor(x => x.Opinion).MaximumLength(2000);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpsertPeDepartmentOpinionCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser,
|
||||
UserManager<User> userManager) : IRequestHandler<UpsertPeDepartmentOpinionCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(UpsertPeDepartmentOpinionCommand request, CancellationToken ct)
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || currentUser.UserId is null)
|
||||
throw new UnauthorizedException();
|
||||
|
||||
var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(p => p.Id == request.PurchaseEvaluationId, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.PurchaseEvaluationId);
|
||||
|
||||
var existing = await db.PurchaseEvaluationDepartmentOpinions
|
||||
.FirstOrDefaultAsync(o => o.PurchaseEvaluationId == pe.Id && o.Kind == request.Kind, ct);
|
||||
|
||||
string? actorName = null;
|
||||
if (request.Sign)
|
||||
{
|
||||
var u = await userManager.FindByIdAsync(currentUser.UserId.Value.ToString());
|
||||
actorName = u?.FullName ?? u?.Email;
|
||||
}
|
||||
|
||||
Guid resultId;
|
||||
ChangelogAction action;
|
||||
if (existing == null)
|
||||
{
|
||||
var entity = new PurchaseEvaluationDepartmentOpinion
|
||||
{
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
Kind = request.Kind,
|
||||
Opinion = request.Opinion,
|
||||
SignedAt = request.Sign ? DateTime.UtcNow : null,
|
||||
UserId = request.Sign ? currentUser.UserId : null,
|
||||
UserName = request.Sign ? actorName : null,
|
||||
};
|
||||
db.PurchaseEvaluationDepartmentOpinions.Add(entity);
|
||||
resultId = entity.Id;
|
||||
action = ChangelogAction.Insert;
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Opinion = request.Opinion;
|
||||
if (request.Sign)
|
||||
{
|
||||
existing.SignedAt = DateTime.UtcNow;
|
||||
existing.UserId = currentUser.UserId;
|
||||
existing.UserName = actorName;
|
||||
}
|
||||
// Sign=false giữ nguyên SignedAt/UserId cũ (user đã ký rồi vẫn giữ chữ ký).
|
||||
resultId = existing.Id;
|
||||
action = ChangelogAction.Update;
|
||||
}
|
||||
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
EntityType = PurchaseEvaluationEntityType.Header, // không có entity type riêng cho opinion
|
||||
EntityId = resultId,
|
||||
Action = action,
|
||||
PhaseAtChange = pe.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = actorName,
|
||||
Summary = request.Sign
|
||||
? $"Ý kiến {KindLabel(request.Kind)} — đã ký"
|
||||
: $"Ý kiến {KindLabel(request.Kind)} — cập nhật text",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return resultId;
|
||||
}
|
||||
|
||||
private static string KindLabel(PeDepartmentKind k) => k switch
|
||||
{
|
||||
PeDepartmentKind.PheDuyet => "Phê duyệt",
|
||||
PeDepartmentKind.Ccm => "P.CCM",
|
||||
PeDepartmentKind.MuaHang => "P.Mua hàng",
|
||||
PeDepartmentKind.SmPm => "SM-PM",
|
||||
_ => k.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ========== DELETE (rare — admin override) ==========
|
||||
|
||||
public record DeletePeDepartmentOpinionCommand(Guid PurchaseEvaluationId, PeDepartmentKind Kind) : IRequest;
|
||||
|
||||
public class DeletePeDepartmentOpinionCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<DeletePeDepartmentOpinionCommand>
|
||||
{
|
||||
public async Task Handle(DeletePeDepartmentOpinionCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.PurchaseEvaluationDepartmentOpinions
|
||||
.FirstOrDefaultAsync(o => o.PurchaseEvaluationId == request.PurchaseEvaluationId && o.Kind == request.Kind, ct)
|
||||
?? throw new NotFoundException("PEDepartmentOpinion", request.Kind);
|
||||
|
||||
var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(p => p.Id == request.PurchaseEvaluationId, ct);
|
||||
db.PurchaseEvaluationDepartmentOpinions.Remove(entity);
|
||||
|
||||
if (pe is not null)
|
||||
{
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
EntityType = PurchaseEvaluationEntityType.Header,
|
||||
Action = ChangelogAction.Delete,
|
||||
PhaseAtChange = pe.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = $"Xóa ý kiến phòng ban ({request.Kind})",
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,247 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Contracts; // WorkflowApproverKind reuse
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
namespace SolutionErp.Application.PurchaseEvaluations;
|
||||
|
||||
// Versioned workflow management cho module Duyệt NCC (PE) — mirror Contract
|
||||
// `WorkflowAdminFeatures` pattern. Phiếu PE đã pin WorkflowDefinitionId tại
|
||||
// thời điểm tạo → vẫn chạy version cũ kể cả khi admin activate version mới.
|
||||
|
||||
public record PeWorkflowStepApproverDto(
|
||||
int Kind, // 1=Role, 2=User (reuse WorkflowApproverKind)
|
||||
string AssignmentValue,
|
||||
string? DisplayName);
|
||||
|
||||
public record PeWorkflowStepDto(
|
||||
Guid Id,
|
||||
int Order,
|
||||
int Phase,
|
||||
string PhaseLabel,
|
||||
string Name,
|
||||
int? SlaDays,
|
||||
List<PeWorkflowStepApproverDto> Approvers);
|
||||
|
||||
public record PeWorkflowDefinitionDto(
|
||||
Guid Id,
|
||||
string Code,
|
||||
int Version,
|
||||
int EvaluationType,
|
||||
string EvaluationTypeLabel,
|
||||
string Name,
|
||||
string? Description,
|
||||
bool IsActive,
|
||||
DateTime? ActivatedAt,
|
||||
DateTime CreatedAt,
|
||||
int EvaluationsUsingCount,
|
||||
List<PeWorkflowStepDto> Steps);
|
||||
|
||||
public record PeWorkflowTypeSummaryDto(
|
||||
int EvaluationType,
|
||||
string EvaluationTypeLabel,
|
||||
PeWorkflowDefinitionDto? Active,
|
||||
List<PeWorkflowDefinitionDto> History);
|
||||
|
||||
public record PeWorkflowAdminOverviewDto(List<PeWorkflowTypeSummaryDto> Types);
|
||||
|
||||
// ========== GET overview ==========
|
||||
|
||||
public record GetPeWorkflowAdminOverviewQuery : IRequest<PeWorkflowAdminOverviewDto>;
|
||||
|
||||
public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
IApplicationDbContext db,
|
||||
UserManager<User> userManager) : IRequestHandler<GetPeWorkflowAdminOverviewQuery, PeWorkflowAdminOverviewDto>
|
||||
{
|
||||
private static readonly Dictionary<PurchaseEvaluationType, string> TypeLabels = new()
|
||||
{
|
||||
[PurchaseEvaluationType.DuyetNcc] = "Duyệt NCC",
|
||||
[PurchaseEvaluationType.DuyetNccPhuongAn] = "Duyệt NCC + Giải pháp",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<PurchaseEvaluationPhase, string> PhaseLabels = new()
|
||||
{
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[PurchaseEvaluationPhase.ChoPurchasing] = "Chờ Purchasing",
|
||||
[PurchaseEvaluationPhase.ChoDuAn] = "Chờ Dự án",
|
||||
[PurchaseEvaluationPhase.ChoCCM] = "Chờ CCM",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetPA] = "Chờ CEO duyệt PA",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetNCC] = "Chờ CEO duyệt NCC",
|
||||
[PurchaseEvaluationPhase.DaDuyet] = "Đã duyệt",
|
||||
};
|
||||
|
||||
public async Task<PeWorkflowAdminOverviewDto> Handle(GetPeWorkflowAdminOverviewQuery request, CancellationToken ct)
|
||||
{
|
||||
var definitions = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
|
||||
.Include(d => d.Steps.OrderBy(s => s.Order))
|
||||
.ThenInclude(s => s.Approvers)
|
||||
.OrderByDescending(d => d.Version)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Resolve user names cho User-kind approvers
|
||||
var userIds = definitions
|
||||
.SelectMany(d => d.Steps)
|
||||
.SelectMany(s => s.Approvers)
|
||||
.Where(a => a.Kind == WorkflowApproverKind.User && Guid.TryParse(a.AssignmentValue, out _))
|
||||
.Select(a => Guid.Parse(a.AssignmentValue))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var userNames = userIds.Count == 0
|
||||
? new Dictionary<Guid, string>()
|
||||
: await userManager.Users.AsNoTracking()
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
|
||||
|
||||
// Count phiếu PE per definition
|
||||
var usageCounts = await db.PurchaseEvaluations.AsNoTracking()
|
||||
.Where(p => p.WorkflowDefinitionId != null)
|
||||
.GroupBy(p => p.WorkflowDefinitionId!.Value)
|
||||
.Select(g => new { Id = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Count, ct);
|
||||
|
||||
PeWorkflowDefinitionDto ToDto(PurchaseEvaluationWorkflowDefinition d) => new(
|
||||
d.Id,
|
||||
d.Code,
|
||||
d.Version,
|
||||
(int)d.EvaluationType,
|
||||
TypeLabels.GetValueOrDefault(d.EvaluationType, d.EvaluationType.ToString()),
|
||||
d.Name,
|
||||
d.Description,
|
||||
d.IsActive,
|
||||
d.ActivatedAt,
|
||||
d.CreatedAt,
|
||||
usageCounts.GetValueOrDefault(d.Id, 0),
|
||||
d.Steps.OrderBy(s => s.Order).Select(s => new PeWorkflowStepDto(
|
||||
s.Id,
|
||||
s.Order,
|
||||
(int)s.Phase,
|
||||
PhaseLabels.GetValueOrDefault(s.Phase, s.Phase.ToString()),
|
||||
s.Name,
|
||||
s.SlaDays,
|
||||
s.Approvers.Select(a => new PeWorkflowStepApproverDto(
|
||||
(int)a.Kind,
|
||||
a.AssignmentValue,
|
||||
ResolveDisplay(a, userNames))).ToList()
|
||||
)).ToList());
|
||||
|
||||
var types = Enum.GetValues<PurchaseEvaluationType>()
|
||||
.Select(type =>
|
||||
{
|
||||
var versions = definitions.Where(d => d.EvaluationType == type).Select(ToDto).ToList();
|
||||
return new PeWorkflowTypeSummaryDto(
|
||||
(int)type,
|
||||
TypeLabels.GetValueOrDefault(type, type.ToString()),
|
||||
versions.FirstOrDefault(v => v.IsActive),
|
||||
versions);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new PeWorkflowAdminOverviewDto(types);
|
||||
}
|
||||
|
||||
private static string? ResolveDisplay(PurchaseEvaluationWorkflowStepApprover a, Dictionary<Guid, string> userNames)
|
||||
{
|
||||
if (a.Kind == WorkflowApproverKind.Role) return a.AssignmentValue;
|
||||
if (Guid.TryParse(a.AssignmentValue, out var uid) && userNames.TryGetValue(uid, out var n)) return n;
|
||||
return a.AssignmentValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== POST new version ==========
|
||||
|
||||
public record CreatePeWorkflowStepApproverInput(int Kind, string AssignmentValue);
|
||||
|
||||
public record CreatePeWorkflowStepInput(
|
||||
int Order,
|
||||
int Phase,
|
||||
string Name,
|
||||
int? SlaDays,
|
||||
List<CreatePeWorkflowStepApproverInput> Approvers);
|
||||
|
||||
public record CreatePeWorkflowDefinitionCommand(
|
||||
PurchaseEvaluationType EvaluationType,
|
||||
string Code,
|
||||
string Name,
|
||||
string? Description,
|
||||
List<CreatePeWorkflowStepInput> Steps) : IRequest<Guid>;
|
||||
|
||||
public class CreatePeWorkflowDefinitionCommandValidator : AbstractValidator<CreatePeWorkflowDefinitionCommand>
|
||||
{
|
||||
public CreatePeWorkflowDefinitionCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.EvaluationType).IsInEnum();
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(100)
|
||||
.Matches("^[A-Za-z0-9._-]+$")
|
||||
.WithMessage("Code chỉ dùng chữ, số, và các ký tự . _ -");
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.Description).MaximumLength(1000);
|
||||
RuleFor(x => x.Steps).NotEmpty()
|
||||
.WithMessage("Quy trình phải có ít nhất 1 bước.");
|
||||
RuleForEach(x => x.Steps).ChildRules(step =>
|
||||
{
|
||||
step.RuleFor(s => s.Order).GreaterThanOrEqualTo(1);
|
||||
// Phase 1..7 thường, 99 = TuChoi (không nên dùng làm step)
|
||||
step.RuleFor(s => s.Phase).Must(p => p >= 1 && p <= 7)
|
||||
.WithMessage("Phase phải nằm trong 1..7 (state thường, không bao gồm Từ chối=99).");
|
||||
step.RuleFor(s => s.Name).NotEmpty().MaximumLength(200);
|
||||
step.RuleFor(s => s.SlaDays).GreaterThanOrEqualTo(0)
|
||||
.When(s => s.SlaDays != null);
|
||||
step.RuleForEach(s => s.Approvers).ChildRules(app =>
|
||||
{
|
||||
app.RuleFor(a => a.Kind).InclusiveBetween(1, 2);
|
||||
app.RuleFor(a => a.AssignmentValue).NotEmpty().MaximumLength(100);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class CreatePeWorkflowDefinitionCommandHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<CreatePeWorkflowDefinitionCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreatePeWorkflowDefinitionCommand request, CancellationToken ct)
|
||||
{
|
||||
var nextVersion = await db.PurchaseEvaluationWorkflowDefinitions
|
||||
.Where(w => w.Code == request.Code)
|
||||
.MaxAsync(w => (int?)w.Version, ct) ?? 0;
|
||||
nextVersion++;
|
||||
|
||||
// Deactivate active version cho EvaluationType này (only ONE active per type)
|
||||
var activeVersions = await db.PurchaseEvaluationWorkflowDefinitions
|
||||
.Where(w => w.EvaluationType == request.EvaluationType && w.IsActive)
|
||||
.ToListAsync(ct);
|
||||
foreach (var old in activeVersions) old.IsActive = false;
|
||||
|
||||
var def = new PurchaseEvaluationWorkflowDefinition
|
||||
{
|
||||
Code = request.Code,
|
||||
Version = nextVersion,
|
||||
EvaluationType = request.EvaluationType,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
IsActive = true,
|
||||
ActivatedAt = DateTime.UtcNow,
|
||||
Steps = request.Steps
|
||||
.OrderBy(s => s.Order)
|
||||
.Select(s => new PurchaseEvaluationWorkflowStep
|
||||
{
|
||||
Order = s.Order,
|
||||
Phase = (PurchaseEvaluationPhase)s.Phase,
|
||||
Name = s.Name,
|
||||
SlaDays = s.SlaDays,
|
||||
Approvers = s.Approvers.Select(a => new PurchaseEvaluationWorkflowStepApprover
|
||||
{
|
||||
Kind = (WorkflowApproverKind)a.Kind,
|
||||
AssignmentValue = a.AssignmentValue,
|
||||
}).ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
};
|
||||
db.PurchaseEvaluationWorkflowDefinitions.Add(def);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return def.Id;
|
||||
}
|
||||
}
|
||||
@ -342,6 +342,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
.Include(x => x.Details).ThenInclude(d => d.Quotes)
|
||||
.Include(x => x.Approvals)
|
||||
.Include(x => x.Attachments)
|
||||
.Include(x => x.DepartmentOpinions)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.Id);
|
||||
|
||||
@ -438,11 +439,26 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
a.Id, a.PurchaseEvaluationSupplierId, a.FileName, a.StoragePath,
|
||||
a.FileSize, a.ContentType, a.Purpose, a.Note, a.CreatedAt))
|
||||
.ToList(),
|
||||
e.DepartmentOpinions
|
||||
.OrderBy(o => (int)o.Kind)
|
||||
.Select(o => new PurchaseEvaluationDepartmentOpinionDto(
|
||||
o.Id, o.Kind, KindLabel(o.Kind),
|
||||
o.Opinion, o.SignedAt, o.UserId, o.UserName))
|
||||
.ToList(),
|
||||
new PurchaseEvaluationWorkflowSummaryDto(
|
||||
policy.Name, policy.Description,
|
||||
policy.ActivePhases.ToList(),
|
||||
policy.NextPhasesFrom(e.Phase).ToList()));
|
||||
}
|
||||
|
||||
private static string KindLabel(PeDepartmentKind k) => k switch
|
||||
{
|
||||
PeDepartmentKind.PheDuyet => "Phê duyệt",
|
||||
PeDepartmentKind.Ccm => "P.CCM",
|
||||
PeDepartmentKind.MuaHang => "P.Mua hàng",
|
||||
PeDepartmentKind.SmPm => "SM-PM",
|
||||
_ => k.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ========== DELETE ==========
|
||||
|
||||
Reference in New Issue
Block a user