[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,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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user