[CLAUDE] PE: V2-aware Inbox/List + 2 dropdown filter quy trình + trạng thái
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m12s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m12s
User báo: "Phiếu chưa thấy lên trong danh sách duyệt — chắc do chưa ăn vào flow. Tách thành 2 cái dropdown là list quy trình duyệt và list trạng thái. Debug trước, phân quyền rút gọn lại sau." BE — V2-aware permission + filter (Application/PurchaseEvaluations/ PurchaseEvaluationFeatures.cs): - ListPurchaseEvaluationsQuery +ApprovalWorkflowId? Guid? param + IDOR loose: phiếu pin V2 → mọi authenticated user thấy được (UAT) - GetMyPurchaseEvaluationInbox V2-aware: ResolveV2InboxIdsAsync helper precompute Set<Guid> phiếu Phase=ChoDuyet pin V2 + actor ∈ Cấp hiện tại approvers (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder match Step.Order + Level.Order). Inbox where = eligiblePhases.Contains || v2InboxIds.Contains. eligiblePhases admin +ChoDuyet. - GetById Detail loose: V2 pin → cho non-Drafter xem (skip eligiblePhases check). API Controller: - PurchaseEvaluationsController.List +approvalWorkflowId query param FE — 2 dropdown filter (cả 2 app mirror): - PurchaseEvaluationsListPage: +URL param `awId` filter quy trình - useQuery `approval-workflows-v2-filter` load list V2 active+history theo applicableType=typeFilter (chỉ enabled khi có type) - Render Select riêng "Tất cả quy trình duyệt" (chỉ show !pendingMe vì Inbox dùng API endpoint khác) + Select "Tất cả trạng thái" giữ - Display option: "QT-DN-V2-001 v01 — Tên quy trình" Verify: BE build 0 error · 2 FE builds OK. Test luồng eoffice: 1. Drafter trình phiếu V2 → Phase=ChoDuyet 2. Login NV X (approver Cấp 1) vào "Duyệt NCC > Duyệt" (?pendingMe=1) → phiếu hiện trong list 3. Login NV Y (không phải approver) → list rỗng (đúng spec) 4. Vào "Duyệt NCC > Danh sách" (không pendingMe) → 2 dropdown: - Quy trình duyệt: filter theo workflow specific - Trạng thái: filter theo Phase
This commit is contained in:
@ -254,7 +254,8 @@ public class TransitionPurchaseEvaluationCommandHandler(
|
||||
public record ListPurchaseEvaluationsQuery(
|
||||
PurchaseEvaluationType? Type = null,
|
||||
PurchaseEvaluationPhase? Phase = null,
|
||||
Guid? ProjectId = null) : PagedRequest, IRequest<PagedResult<PurchaseEvaluationListItemDto>>;
|
||||
Guid? ProjectId = null,
|
||||
Guid? ApprovalWorkflowId = null) : PagedRequest, IRequest<PagedResult<PurchaseEvaluationListItemDto>>;
|
||||
|
||||
public class ListPurchaseEvaluationsQueryHandler(
|
||||
IApplicationDbContext db,
|
||||
@ -269,17 +270,24 @@ public class ListPurchaseEvaluationsQueryHandler(
|
||||
from s in sj.DefaultIfEmpty()
|
||||
select new { e, p, s };
|
||||
|
||||
// IDOR: non-admin chỉ thấy phiếu mình là Drafter hoặc role eligible phase
|
||||
// IDOR: non-admin chỉ thấy phiếu mình là Drafter hoặc role eligible phase.
|
||||
// [Session 17 UAT loose] Phiếu pin V2 (ApprovalWorkflowId != null) →
|
||||
// tạm cho mọi authenticated user thấy để UAT (sẽ thắt chặt sau khi
|
||||
// hoàn thiện logic permission V2-aware).
|
||||
if (!currentUser.Roles.Contains(AppRoles.Admin))
|
||||
{
|
||||
var userId = currentUser.UserId;
|
||||
var eligiblePhases = GetEligiblePhases(currentUser.Roles);
|
||||
q = q.Where(x => x.e.DrafterUserId == userId || eligiblePhases.Contains(x.e.Phase));
|
||||
q = q.Where(x =>
|
||||
x.e.DrafterUserId == userId
|
||||
|| eligiblePhases.Contains(x.e.Phase)
|
||||
|| x.e.ApprovalWorkflowId != null); // V2 loose UAT
|
||||
}
|
||||
|
||||
if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);
|
||||
if (request.Phase is not null) q = q.Where(x => x.e.Phase == request.Phase);
|
||||
if (request.ProjectId is not null) q = q.Where(x => x.e.ProjectId == request.ProjectId);
|
||||
if (request.ApprovalWorkflowId is not null) q = q.Where(x => x.e.ApprovalWorkflowId == request.ApprovalWorkflowId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
@ -338,6 +346,7 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
|
||||
if (!currentUser.IsAuthenticated) throw new UnauthorizedException();
|
||||
|
||||
var userRoles = currentUser.Roles;
|
||||
var userId = currentUser.UserId;
|
||||
var isAdmin = userRoles.Contains(AppRoles.Admin);
|
||||
var eligiblePhases = isAdmin
|
||||
? [
|
||||
@ -347,16 +356,21 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
|
||||
PurchaseEvaluationPhase.ChoCCM,
|
||||
PurchaseEvaluationPhase.ChoCEODuyetPA,
|
||||
PurchaseEvaluationPhase.ChoCEODuyetNCC,
|
||||
PurchaseEvaluationPhase.ChoDuyet,
|
||||
]
|
||||
: ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(userRoles);
|
||||
|
||||
if (eligiblePhases.Count == 0) return [];
|
||||
// V2-aware (Mig 22-24): tìm phiếu pin V2 + Phase=ChoDuyet + actor là
|
||||
// approver Cấp hiện tại (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder).
|
||||
var v2InboxIds = isAdmin
|
||||
? new HashSet<Guid>()
|
||||
: await ResolveV2InboxIdsAsync(userId, ct);
|
||||
|
||||
var q = from e in db.PurchaseEvaluations.AsNoTracking()
|
||||
join p in db.Projects.AsNoTracking() on e.ProjectId equals p.Id
|
||||
join s in db.Suppliers.AsNoTracking() on e.SelectedSupplierId equals s.Id into sj
|
||||
from s in sj.DefaultIfEmpty()
|
||||
where eligiblePhases.Contains(e.Phase)
|
||||
where eligiblePhases.Contains(e.Phase) || v2InboxIds.Contains(e.Id)
|
||||
select new { e, p, s };
|
||||
|
||||
if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);
|
||||
@ -371,6 +385,45 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
|
||||
.Take(100)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
// Helper: precompute V2 phiếu IDs nơi actor là approver Cấp hiện tại.
|
||||
// In-memory join vì Step.Order vs Index 0-based không thẳng EF được.
|
||||
private async Task<HashSet<Guid>> ResolveV2InboxIdsAsync(Guid? userId, CancellationToken ct)
|
||||
{
|
||||
if (userId is null) return new HashSet<Guid>();
|
||||
var candidates = await (
|
||||
from e in db.PurchaseEvaluations.AsNoTracking()
|
||||
where e.Phase == PurchaseEvaluationPhase.ChoDuyet
|
||||
&& e.ApprovalWorkflowId != null
|
||||
&& e.CurrentWorkflowStepIndex != null
|
||||
&& e.CurrentApprovalLevelOrder != null
|
||||
select new { e.Id, e.ApprovalWorkflowId, e.CurrentWorkflowStepIndex, e.CurrentApprovalLevelOrder }
|
||||
).ToListAsync(ct);
|
||||
|
||||
if (candidates.Count == 0) return new HashSet<Guid>();
|
||||
|
||||
var wfIds = candidates.Select(c => c.ApprovalWorkflowId!.Value).Distinct().ToList();
|
||||
var workflows = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => wfIds.Contains(w.Id))
|
||||
.Include(w => w.Steps.OrderBy(s => s.Order))
|
||||
.ThenInclude(s => s.Levels.OrderBy(l => l.Order))
|
||||
.ToDictionaryAsync(w => w.Id, ct);
|
||||
|
||||
var result = new HashSet<Guid>();
|
||||
foreach (var c in candidates)
|
||||
{
|
||||
if (!workflows.TryGetValue(c.ApprovalWorkflowId!.Value, out var wf)) continue;
|
||||
var steps = wf.Steps.OrderBy(s => s.Order).ToList();
|
||||
var idx = c.CurrentWorkflowStepIndex!.Value;
|
||||
if (idx < 0 || idx >= steps.Count) continue;
|
||||
var step = steps[idx];
|
||||
var match = step.Levels.Any(l =>
|
||||
l.Order == c.CurrentApprovalLevelOrder!.Value
|
||||
&& l.ApproverUserId == userId.Value);
|
||||
if (match) result.Add(c.Id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GET detail bundle ==========
|
||||
@ -399,7 +452,10 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
{
|
||||
var isDrafter = e.DrafterUserId == currentUser.UserId;
|
||||
var eligiblePhases = ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(currentUser.Roles);
|
||||
if (!isDrafter && !eligiblePhases.Contains(e.Phase))
|
||||
// [Session 17 UAT loose] Phiếu pin V2 → cho mọi authenticated user
|
||||
// xem được (sẽ thắt chặt sau khi user UAT confirm flow).
|
||||
var isPinnedV2 = e.ApprovalWorkflowId is not null;
|
||||
if (!isDrafter && !eligiblePhases.Contains(e.Phase) && !isPinnedV2)
|
||||
throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user