[CLAUDE] Domain+App+Api+Infra+FE-Admin+FE-User: S34 Plan 2 G-O1 Danh bạ nội bộ
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m46s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m46s
Phase 10.2 Văn phòng số — Internal Directory (1 endpoint reuse Users + EmployeeProfiles + Departments, FE card grid avatar/dept/email/phone/Ext). BE Task 1+2 (em main solo): - Application/Office/DirectoryFeatures.cs — GetDirectoryQuery + DirectoryItemDto 12 field LEFT JOIN Users.IsActive + Departments + EmployeeProfiles - Api/Controllers/DirectoryController.cs — GET /api/directory?search=&departmentId= class-level [Authorize] (mọi authenticated NV tra cứu danh bạ nội bộ) - MenuKeys.cs +Off+OffDanhBa const + All[] update - DbInitializer.SeedMenuTreeAsync Off Order=29 + OffDanhBa Order=1 dưới Off FE Task 3 (Implementer Case 2 Pattern 16-bis 4-place mirror cross-app — 5×): - types/directory.ts SHA256 7349d9f64e78 × 2 app IDENTICAL - pages/office/InternalDirectoryPage.tsx SHA256 2aa7e0eed2c8 × 2 app IDENTICAL Card grid responsive 1/2/3/4 col + filter dept dropdown + search input Avatar 14×14 initials gradient PALETTE 6 màu (Pattern 14 Tailwind JIT) EmployeeCode badge + Department emerald badge + email mailto + phone tel Internal phone Ext: amber badge + empty/loading state Vietnamese 100% - App.tsx route /directory × 2 app - lib/menuKeys.ts Off+OffDanhBa const × 2 app - components/Layout.tsx resolvePath staticMap Off_DanhBa:/directory × 2 app (gotcha #50 — 5 places mirror crossapp DON'T MISS) Verify: - dotnet build PASS (2 warn DocxRenderer existing, 0 error) - dotnet test 120/120 PASS (58 Domain + 62 Infra baseline preserve) - npm build × 2 app PASS 0 TS err (fe-admin 1436KB / fe-user 1350KB) Implementer MEMORY Pattern 16-bis reinforced 5× cumulative (S29 Plan CA HF1 + S29 Plan B Chunk D + S33 Plan B G-H1 Task 5 + S34 Plan G-O1 Task 3). Endpoint smoke pending CICD post-deploy Stage 4 (Run #XXX expected ~3m30s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SolutionErp.Application.Office;
|
||||
|
||||
namespace SolutionErp.Api.Controllers;
|
||||
|
||||
// Phase 10.2 G-O1 (S34) — Danh bạ nội bộ.
|
||||
// 1 endpoint readonly. Class-level [Authorize] — mọi authenticated user thấy được
|
||||
// (không restrict admin-only vì danh bạ nội bộ default open cho NV tra cứu nhau).
|
||||
[ApiController]
|
||||
[Route("api/directory")]
|
||||
[Authorize]
|
||||
public class DirectoryController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<DirectoryItemDto>>> Get(
|
||||
[FromQuery] string? search = null,
|
||||
[FromQuery] Guid? departmentId = null,
|
||||
CancellationToken ct = default)
|
||||
=> Ok(await mediator.Send(new GetDirectoryQuery(search, departmentId), ct));
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
|
||||
namespace SolutionErp.Application.Office;
|
||||
|
||||
// Phase 10.2 G-O1 (S34) — Danh bạ nội bộ.
|
||||
// 1 endpoint readonly query JOIN Users + EmployeeProfiles + Departments.
|
||||
// FE card grid hiển thị avatar/name/dept/position/email/phone/internal-phone.
|
||||
// Reuse existing data — KHÔNG mở schema mới.
|
||||
|
||||
public sealed record DirectoryItemDto(
|
||||
Guid UserId,
|
||||
string FullName,
|
||||
string? Position,
|
||||
string? PhotoUrl,
|
||||
Guid? DepartmentId,
|
||||
string? DepartmentName,
|
||||
string? EmployeeCode,
|
||||
string? Email,
|
||||
string? Phone,
|
||||
string? InternalPhone,
|
||||
string? PersonalEmail,
|
||||
string? WorkLocation);
|
||||
|
||||
public sealed record GetDirectoryQuery(
|
||||
string? Search = null,
|
||||
Guid? DepartmentId = null) : IRequest<List<DirectoryItemDto>>;
|
||||
|
||||
public sealed class GetDirectoryQueryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<GetDirectoryQuery, List<DirectoryItemDto>>
|
||||
{
|
||||
public async Task<List<DirectoryItemDto>> Handle(GetDirectoryQuery request, CancellationToken ct)
|
||||
{
|
||||
// Active users only — Department LEFT join (admin/system user có thể null dept).
|
||||
// EmployeeProfile LEFT join (user chưa được seed EmployeeProfile vẫn hiện trong danh bạ
|
||||
// với phone/internalPhone null — sau Phase 10.1 G-H1 thì tất cả 33 prod user đều có).
|
||||
var query =
|
||||
from u in db.Users.AsNoTracking().Where(x => x.IsActive)
|
||||
join d in db.Departments.AsNoTracking() on u.DepartmentId equals d.Id into deptJoin
|
||||
from d in deptJoin.DefaultIfEmpty()
|
||||
join ep in db.EmployeeProfiles.AsNoTracking() on u.Id equals ep.UserId into epJoin
|
||||
from ep in epJoin.DefaultIfEmpty()
|
||||
select new
|
||||
{
|
||||
u.Id,
|
||||
u.FullName,
|
||||
u.Position,
|
||||
u.Email,
|
||||
u.DepartmentId,
|
||||
DepartmentName = d != null ? d.Name : null,
|
||||
EmployeeCode = ep != null ? ep.EmployeeCode : null,
|
||||
Phone = ep != null ? ep.Phone : null,
|
||||
InternalPhone = ep != null ? ep.InternalPhone : null,
|
||||
PersonalEmail = ep != null ? ep.PersonalEmail : null,
|
||||
PhotoUrl = ep != null ? ep.PhotoUrl : null,
|
||||
WorkLocation = ep != null ? ep.WorkLocation : null,
|
||||
};
|
||||
|
||||
if (request.DepartmentId is Guid deptId)
|
||||
{
|
||||
query = query.Where(x => x.DepartmentId == deptId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
var term = request.Search.Trim();
|
||||
query = query.Where(x =>
|
||||
x.FullName.Contains(term) ||
|
||||
(x.Email != null && x.Email.Contains(term)) ||
|
||||
(x.Phone != null && x.Phone.Contains(term)) ||
|
||||
(x.InternalPhone != null && x.InternalPhone.Contains(term)) ||
|
||||
(x.EmployeeCode != null && x.EmployeeCode.Contains(term)));
|
||||
}
|
||||
|
||||
var rows = await query
|
||||
.OrderBy(x => x.DepartmentName)
|
||||
.ThenBy(x => x.FullName)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return rows
|
||||
.Select(x => new DirectoryItemDto(
|
||||
x.Id,
|
||||
x.FullName,
|
||||
x.Position,
|
||||
x.PhotoUrl,
|
||||
x.DepartmentId,
|
||||
x.DepartmentName,
|
||||
x.EmployeeCode,
|
||||
x.Email,
|
||||
x.Phone,
|
||||
x.InternalPhone,
|
||||
x.PersonalEmail,
|
||||
x.WorkLocation))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@ -85,6 +85,15 @@ public static class MenuKeys
|
||||
public const string Hrm = "Hrm"; // root group
|
||||
public const string HrmHoSo = "Hrm_HoSo"; // Hồ sơ Nhân sự (list + detail + edit)
|
||||
|
||||
// ============================================================
|
||||
// Module Văn phòng số (Phase 10.2 G-O1+ S34 2026-05-27).
|
||||
// 1 root group `Off` + leaf con: Off_DanhBa (G-O1 Danh bạ nội bộ).
|
||||
// Future Phase 10.2+ add: Off_PhongHop (G-O2 booking) +
|
||||
// workflow apps Off_DeXuat / Off_DonTu / Off_DatXe / Off_ItTicket.
|
||||
// ============================================================
|
||||
public const string Off = "Off"; // root group văn phòng số
|
||||
public const string OffDanhBa = "Off_DanhBa"; // Danh bạ nội bộ (card grid)
|
||||
|
||||
public static readonly string[] PurchaseEvaluationTypeCodes =
|
||||
["DuyetNcc", "DuyetNccPhuongAn"];
|
||||
|
||||
@ -110,6 +119,7 @@ public static class MenuKeys
|
||||
PurchaseEvaluations,
|
||||
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||
Hrm, HrmHoSo, // Mig 34 — Phase 10.1
|
||||
Off, OffDanhBa, // Phase 10.2 G-O1 — Văn phòng số
|
||||
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
|
||||
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22
|
||||
];
|
||||
|
||||
@ -1483,6 +1483,11 @@ public static class DbInitializer
|
||||
// Phase 1 minimal. Phase 1.5 + G-H2/G-H3 thêm Config/Dashboard.
|
||||
(MenuKeys.Hrm, "Nhân sự", null, 28, "UserCircle"),
|
||||
(MenuKeys.HrmHoSo, "Hồ sơ Nhân sự", MenuKeys.Hrm, 1, "ContactRound"),
|
||||
|
||||
// Module Văn phòng số (Phase 10.2 G-O1+ S34). 1 root + leaf Danh bạ.
|
||||
// Future leaf: Off_PhongHop (G-O2) + workflow apps Off_DeXuat/DonTu/DatXe/ItTicket.
|
||||
(MenuKeys.Off, "Văn phòng số", null, 29, "Briefcase"),
|
||||
(MenuKeys.OffDanhBa, "Danh bạ nội bộ", MenuKeys.Off, 1, "BookUser"),
|
||||
};
|
||||
|
||||
// Per-type sub-menu under Contracts: 1 group + 3 leaves each
|
||||
|
||||
Reference in New Issue
Block a user