[CLAUDE] App+Infra+FE-Admin: seed master data + MyDashboard widgets
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
Task 1 — Seed master data unblock UAT/demo: - DbInitializer.SeedDepartmentsAsync: 9 departments từ QT-TP-NCC.docx (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) — reference data không phải demo. - DbInitializer.SeedDemoMasterDataAsync: 5 demo suppliers (NCC VLXD, NTP Xây dựng, TĐ Hoàng Nam, DV Clean, CĐT Vingroup — covers cả 5 SupplierType) + 3 demo projects (FLOCK01, SkyGarden, Industrial). Chỉ seed nếu tables empty — respect admin's real data khi họ add. Task 2 — Roles CRUD đã có sẵn trong UsersPage (Shield icon button mở dialog gán 12 roles từ AppRoles.cs). Skip. Task 3 — MyDashboard role-specific widgets: - GetMyDashboardQuery (Reports): returns DraftsInProgress (tôi là Drafter + phase soạn thảo), PendingMyApproval (phase eligible role tôi + không phải tôi drafter), DueSoon 24h, Overdue, DraftsTotalValue. - Endpoint GET /api/reports/my-dashboard. - FE MyDashboardRow ở đầu DashboardPage: 4 card hover → navigate. Admin ẩn row nếu tất cả = 0 (ERP noise reduction). 'Đang soạn thảo' + 'Chờ tôi duyệt' clickable → /contracts?filter=... (filter param để wire lần sau; row hiện chưa implement). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,79 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
|
||||
namespace SolutionErp.Application.Reports.Queries.GetMyDashboard;
|
||||
|
||||
// Per-user dashboard stats — role-aware. Drafter sees their own drafts;
|
||||
// approvers see what's waiting on their desk; everyone sees SLA pressure
|
||||
// relevant to them.
|
||||
public record GetMyDashboardQuery : IRequest<MyDashboardDto>;
|
||||
|
||||
public record MyDashboardDto(
|
||||
int DraftsInProgress, // HĐ tôi là Drafter + đang ở DangSoanThao/DangGopY/DangDamPhan
|
||||
int PendingMyApproval, // HĐ ở phase mà role tôi eligible advance (không đếm HĐ mình là Drafter)
|
||||
int DueSoon, // Trong 24h tới (intersect với 2 ở trên)
|
||||
int Overdue, // Đã quá hạn (intersect)
|
||||
decimal DraftsTotalValue);
|
||||
|
||||
public class GetMyDashboardQueryHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser,
|
||||
IDateTime clock) : IRequestHandler<GetMyDashboardQuery, MyDashboardDto>
|
||||
{
|
||||
// Same map as ContractFeatures.GetMyInboxQuery — roles eligible for each phase
|
||||
private static readonly Dictionary<ContractPhase, string[]> PhaseActorRoles = new()
|
||||
{
|
||||
[ContractPhase.DangSoanThao] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[ContractPhase.DangGopY] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
|
||||
[ContractPhase.DangDamPhan] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
[ContractPhase.DangInKy] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
[ContractPhase.DangKiemTraCCM] = [AppRoles.CostControl],
|
||||
[ContractPhase.DangTrinhKy] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[ContractPhase.DangDongDau] = [AppRoles.HrAdmin],
|
||||
};
|
||||
|
||||
public async Task<MyDashboardDto> Handle(GetMyDashboardQuery request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedException();
|
||||
var userRoles = currentUser.Roles;
|
||||
var now = clock.UtcNow;
|
||||
var dayFromNow = now.AddHours(24);
|
||||
|
||||
var eligiblePhases = PhaseActorRoles
|
||||
.Where(kv => kv.Value.Any(r => userRoles.Contains(r)))
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
var contracts = db.Contracts.AsNoTracking()
|
||||
.Where(c => c.Phase != ContractPhase.DaPhatHanh && c.Phase != ContractPhase.TuChoi);
|
||||
|
||||
// Drafts I'm authoring — I'm the Drafter + phase is pre-signing
|
||||
var inProgressPhases = new[] {
|
||||
ContractPhase.DangSoanThao, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
|
||||
};
|
||||
var myDrafts = contracts.Where(c => c.DrafterUserId == userId && inProgressPhases.Contains(c.Phase));
|
||||
|
||||
var draftsCount = await myDrafts.CountAsync(ct);
|
||||
var draftsValue = await myDrafts.SumAsync(c => (decimal?)c.GiaTri, ct) ?? 0m;
|
||||
|
||||
// Pending my approval — phase is in my eligibility + I'm NOT the drafter
|
||||
// (drafters shouldn't self-approve). Empty if user has no relevant role.
|
||||
var pendingMine = eligiblePhases.Count == 0
|
||||
? contracts.Where(c => false)
|
||||
: contracts.Where(c => eligiblePhases.Contains(c.Phase) && c.DrafterUserId != userId);
|
||||
|
||||
var pendingCount = await pendingMine.CountAsync(ct);
|
||||
|
||||
// SLA pressure across both sets — a contract I'm drafting + near deadline,
|
||||
// OR one I should approve + near deadline.
|
||||
var mine = myDrafts.Union(pendingMine);
|
||||
var dueSoon = await mine.CountAsync(c => c.SlaDeadline != null && c.SlaDeadline >= now && c.SlaDeadline < dayFromNow, ct);
|
||||
var overdue = await mine.CountAsync(c => c.SlaDeadline != null && c.SlaDeadline < now, ct);
|
||||
|
||||
return new MyDashboardDto(draftsCount, pendingCount, dueSoon, overdue, draftsValue);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user