[CLAUDE] App+Infra+FE-Admin: seed master data + MyDashboard widgets
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:
pqhuy1987
2026-04-21 22:06:28 +07:00
parent 4690cc3a81
commit 6197c841bb
4 changed files with 226 additions and 1 deletions

View File

@ -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);
}
}