[CLAUDE] App+Api: S34 Plan 3 Item 3 BE 5 satellite CRUD scaffold (15 endpoint)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m36s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m36s
Phase 1.5 Item 3 BE — Implementer Case 2 cookie-cutter 5-entity scaffold
mirror parent EmployeeProfile CRUD pattern (Pattern 12-ter NEW saved).
New file: Application/Hrm/EmployeeSatelliteFeatures.cs (621 LOC):
- 5 region (#region {EntityName}) — WorkHistory/Education/FamilyRelation/Skill/Document
- Each region: Create/Update/Delete Command + Validator + Handler
- Verify parent EmployeeProfile.Exists pattern AnyAsync(!IsDeleted)
- Soft delete IsDeleted=true + DeletedAt=UtcNow + DeletedBy=currentUser
- Validator nullable enum IsInEnum().When(HasValue) pattern
Modified file: Api/Controllers/EmployeesController.cs (70 → 234 LOC, +15 endpoint):
- 5 region × 3 verb (POST/PUT/DELETE) = 15 satellite endpoint
- Path per satellite:
- /api/employees/{id}/work-history
- /api/employees/{id}/education
- /api/employees/{id}/family-relations
- /api/employees/{id}/skills
- /api/employees/{id}/documents
- Per-action policy: Hrm_HoSo.Create/Update/Delete (override class-level Read)
- BadRequest guard: id != cmd.{EmployeeProfileId|Id} → "ID không khớp"
Document satellite metadata-only — file upload IFileStorage body wire defer S35.
Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS (58 Domain + 72 Infra baseline preserve, no test add)
- Endpoints count: ~154 → 169 (+15)
- LOC delta: +785 BE (621 features + 164 controller)
Pattern 12-ter NEW (Implementer MEMORY): 5× satellite CRUD scaffold cookie-cutter
within-module (different from Pattern 12-bis cross-module mirror). Reusable
cho future N-satellite parent (vd Contract attachments, PE quotes, Budget details).
Defer S35:
- FE inline edit forms 5 satellite section (~1.5h em main solo)
- Test bundle satellite CRUD (~30 phút Implementer Case 3)
- IFileStorage Document body upload wire (multipart form-data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -182,6 +182,27 @@ Khi spec yêu cầu "move page X từ fe-admin → fe-user" hoặc ngược lạ
|
|||||||
- Token cost ~20k (under budget 25k). Card grid + avatar gradient palette inline helpers (Pattern 14 reuse) — không tách component riêng vì single-use scope.
|
- Token cost ~20k (under budget 25k). Card grid + avatar gradient palette inline helpers (Pattern 14 reuse) — không tách component riêng vì single-use scope.
|
||||||
- LESSON pattern repeat trust: S33 Task 5 spec "Task 5 cookie-cutter mirror EmployeesListPage" used 4-place checklist explicit. S34 G-O1 Task 3 spec follow same template → execute 0 ambiguity. Pattern 16-bis xứng đáng "BLESSED Foundation" cho future cookie-cutter cross-app mirror.
|
- LESSON pattern repeat trust: S33 Task 5 spec "Task 5 cookie-cutter mirror EmployeesListPage" used 4-place checklist explicit. S34 G-O1 Task 3 spec follow same template → execute 0 ambiguity. Pattern 16-bis xứng đáng "BLESSED Foundation" cho future cookie-cutter cross-app mirror.
|
||||||
|
|
||||||
|
### Pattern 12-ter: 5× satellite CRUD scaffold cookie-cutter same parent (S34 G-H1 Phase 1.5 Item 3)
|
||||||
|
|
||||||
|
Khi spec yêu cầu "5 satellite entity CRUD same parent" (vd Employee → WorkHistory/Education/FamilyRelation/Skill/Document):
|
||||||
|
- **1 file Application layer** `<Parent>SatelliteFeatures.cs` chứa 5 region cookie-cutter Create/Update/Delete cho mỗi satellite (~600 LOC)
|
||||||
|
- **1 file Controller** extend với 15 endpoint (3 verb × 5 satellite)
|
||||||
|
- **Pattern per region:**
|
||||||
|
- `Create{X}Command(EmployeeProfileId, ...)` → `IRequest<Guid>` + Validator + Handler (verify parent exists trước → `AnyAsync` parent + throw NotFoundException → save → return Id)
|
||||||
|
- `Update{X}Command(Id, ...)` → `IRequest` + Validator + Handler (FirstOrDefaultAsync `!IsDeleted` + throw NotFoundException + assign + save)
|
||||||
|
- `Delete{X}Command(Id)` → `IRequest` + Handler (soft delete IsDeleted=true + DeletedAt + DeletedBy từ ICurrentUser + save)
|
||||||
|
- **Controller endpoints:**
|
||||||
|
- `POST /{parentId:guid}/{satellite}` — verify `parentId == cmd.EmployeeProfileId` (BadRequest "ID không khớp" mismatch) → return `{ id: newId }`
|
||||||
|
- `PUT /{parentId:guid}/{satellite}/{satId:guid}` — verify `satId == cmd.Id` → NoContent
|
||||||
|
- `DELETE /{parentId:guid}/{satellite}/{satId:guid}` — direct `DeleteCommand(satId)` → NoContent
|
||||||
|
- **Per-action policy override class-level Read** (`Hrm_HoSo.Create/Update/Delete`)
|
||||||
|
- **Verify parent exists pattern**: `AnyAsync(x => x.Id == ... && !x.IsDeleted, ct)` — không cần Include nav
|
||||||
|
- **Soft delete pattern**: AuditableEntity `IsDeleted` + `DeletedAt = DateTime.UtcNow` + `DeletedBy = currentUser.UserId` (inject ICurrentUser)
|
||||||
|
|
||||||
|
Bài học S34 Plan 3 Phase 1.5 Item 3: 2 file modification (1 new ~621 LOC + 1 extend +160 LOC) — build clean 0 error 2 warn (pre-existing DocxRenderer), 130/130 test PASS, endpoint count 5→20.
|
||||||
|
|
||||||
|
Reusable cho future bất kỳ parent entity có N satellite cookie-cutter (Project → milestones/risks/deliverables, Department → roles/budgets/headcounts...). Polymorphic discriminator field (vd EmployeeSkill.Kind) treat as regular required field — không cần special handling.
|
||||||
|
|
||||||
### Pattern 12-bis: Cross-module entity cookie-cutter mirror (S29 Plan B Chunk C — Mig 33)
|
### Pattern 12-bis: Cross-module entity cookie-cutter mirror (S29 Plan B Chunk C — Mig 33)
|
||||||
|
|
||||||
Khi spec yêu cầu "mirror entity X từ PE module sang Contract module" (vd LevelOpinions / DepartmentApproval / ManualBudgetFields):
|
Khi spec yêu cầu "mirror entity X từ PE module sang Contract module" (vd LevelOpinions / DepartmentApproval / ManualBudgetFields):
|
||||||
|
|||||||
@ -67,4 +67,168 @@ public class EmployeesController(IMediator mediator) : ControllerBase
|
|||||||
await mediator.Send(new DeleteEmployeeProfileCommand(id), ct);
|
await mediator.Send(new DeleteEmployeeProfileCommand(id), ct);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 1.5 Item 3 (S34) — 5 satellite CRUD endpoint scaffold.
|
||||||
|
// FE inline form satellite DEFER S35. File upload IFileStorage wire body DEFER.
|
||||||
|
// Per-action policy inherit class-level Read + override Create/Update/Delete.
|
||||||
|
|
||||||
|
#region WorkHistory satellite
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/work-history")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Create")]
|
||||||
|
public async Task<ActionResult<object>> CreateWorkHistory(
|
||||||
|
Guid id, [FromBody] CreateEmployeeWorkHistoryCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
|
||||||
|
var newId = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id = newId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/work-history/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Update")]
|
||||||
|
public async Task<IActionResult> UpdateWorkHistory(
|
||||||
|
Guid id, Guid satId, [FromBody] UpdateEmployeeWorkHistoryCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||||
|
await mediator.Send(cmd, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}/work-history/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Delete")]
|
||||||
|
public async Task<IActionResult> DeleteWorkHistory(Guid id, Guid satId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteEmployeeWorkHistoryCommand(satId), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Education satellite
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/education")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Create")]
|
||||||
|
public async Task<ActionResult<object>> CreateEducation(
|
||||||
|
Guid id, [FromBody] CreateEmployeeEducationCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
|
||||||
|
var newId = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id = newId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/education/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Update")]
|
||||||
|
public async Task<IActionResult> UpdateEducation(
|
||||||
|
Guid id, Guid satId, [FromBody] UpdateEmployeeEducationCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||||
|
await mediator.Send(cmd, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}/education/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Delete")]
|
||||||
|
public async Task<IActionResult> DeleteEducation(Guid id, Guid satId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteEmployeeEducationCommand(satId), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region FamilyRelation satellite
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/family-relations")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Create")]
|
||||||
|
public async Task<ActionResult<object>> CreateFamilyRelation(
|
||||||
|
Guid id, [FromBody] CreateEmployeeFamilyRelationCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
|
||||||
|
var newId = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id = newId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/family-relations/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Update")]
|
||||||
|
public async Task<IActionResult> UpdateFamilyRelation(
|
||||||
|
Guid id, Guid satId, [FromBody] UpdateEmployeeFamilyRelationCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||||
|
await mediator.Send(cmd, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}/family-relations/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Delete")]
|
||||||
|
public async Task<IActionResult> DeleteFamilyRelation(Guid id, Guid satId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteEmployeeFamilyRelationCommand(satId), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Skill satellite
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/skills")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Create")]
|
||||||
|
public async Task<ActionResult<object>> CreateSkill(
|
||||||
|
Guid id, [FromBody] CreateEmployeeSkillCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
|
||||||
|
var newId = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id = newId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/skills/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Update")]
|
||||||
|
public async Task<IActionResult> UpdateSkill(
|
||||||
|
Guid id, Guid satId, [FromBody] UpdateEmployeeSkillCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||||
|
await mediator.Send(cmd, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}/skills/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Delete")]
|
||||||
|
public async Task<IActionResult> DeleteSkill(Guid id, Guid satId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteEmployeeSkillCommand(satId), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Document satellite
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/documents")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Create")]
|
||||||
|
public async Task<ActionResult<object>> CreateDocument(
|
||||||
|
Guid id, [FromBody] CreateEmployeeDocumentCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
|
||||||
|
var newId = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id = newId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/documents/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Update")]
|
||||||
|
public async Task<IActionResult> UpdateDocument(
|
||||||
|
Guid id, Guid satId, [FromBody] UpdateEmployeeDocumentCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||||
|
await mediator.Send(cmd, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}/documents/{satId:guid}")]
|
||||||
|
[Authorize(Policy = "Hrm_HoSo.Delete")]
|
||||||
|
public async Task<IActionResult> DeleteDocument(Guid id, Guid satId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteEmployeeDocumentCommand(satId), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,621 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Application.Common.Exceptions;
|
||||||
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Domain.Hrm;
|
||||||
|
|
||||||
|
namespace SolutionErp.Application.Hrm;
|
||||||
|
|
||||||
|
// Plan G-H1 Phase 1.5 Item 3 (S34) — 5 satellite CRUD endpoint scaffold.
|
||||||
|
// Cookie-cutter 5× pattern: Create/Update/Delete cho WorkHistory/Education/
|
||||||
|
// FamilyRelation/Skill/Document. Mirror parent EmployeeFeatures.cs pattern
|
||||||
|
// (Reviewer minor S33+S34 — bool? safe partial update KHÔNG áp dụng satellite,
|
||||||
|
// satellite create/update full overwrite).
|
||||||
|
//
|
||||||
|
// FE inline form satellite DEFER S35 (em main capacity).
|
||||||
|
// File upload IFileStorage wire cho Document body DEFER S35.
|
||||||
|
|
||||||
|
#region WorkHistory
|
||||||
|
|
||||||
|
public record CreateEmployeeWorkHistoryCommand(
|
||||||
|
Guid EmployeeProfileId,
|
||||||
|
string CompanyName,
|
||||||
|
string? CompanyAddress = null,
|
||||||
|
string? Industry = null,
|
||||||
|
DateOnly? FromDate = null,
|
||||||
|
DateOnly? ToDate = null,
|
||||||
|
string? JobTitle = null,
|
||||||
|
string? JobDescription = null,
|
||||||
|
string? ResignReason = null) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateEmployeeWorkHistoryCommandValidator : AbstractValidator<CreateEmployeeWorkHistoryCommand>
|
||||||
|
{
|
||||||
|
public CreateEmployeeWorkHistoryCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EmployeeProfileId).NotEmpty();
|
||||||
|
RuleFor(x => x.CompanyName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.CompanyAddress).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Industry).MaximumLength(100);
|
||||||
|
RuleFor(x => x.JobTitle).MaximumLength(200);
|
||||||
|
RuleFor(x => x.JobDescription).MaximumLength(2000);
|
||||||
|
RuleFor(x => x.ResignReason).MaximumLength(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateEmployeeWorkHistoryCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<CreateEmployeeWorkHistoryCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateEmployeeWorkHistoryCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parentExists = await db.EmployeeProfiles
|
||||||
|
.AnyAsync(x => x.Id == request.EmployeeProfileId && !x.IsDeleted, ct);
|
||||||
|
if (!parentExists)
|
||||||
|
throw new NotFoundException("EmployeeProfile", request.EmployeeProfileId);
|
||||||
|
|
||||||
|
var entity = new EmployeeWorkHistory
|
||||||
|
{
|
||||||
|
EmployeeProfileId = request.EmployeeProfileId,
|
||||||
|
CompanyName = request.CompanyName,
|
||||||
|
CompanyAddress = request.CompanyAddress,
|
||||||
|
Industry = request.Industry,
|
||||||
|
FromDate = request.FromDate,
|
||||||
|
ToDate = request.ToDate,
|
||||||
|
JobTitle = request.JobTitle,
|
||||||
|
JobDescription = request.JobDescription,
|
||||||
|
ResignReason = request.ResignReason,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EmployeeWorkHistories.Add(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateEmployeeWorkHistoryCommand(
|
||||||
|
Guid Id,
|
||||||
|
string CompanyName,
|
||||||
|
string? CompanyAddress,
|
||||||
|
string? Industry,
|
||||||
|
DateOnly? FromDate,
|
||||||
|
DateOnly? ToDate,
|
||||||
|
string? JobTitle,
|
||||||
|
string? JobDescription,
|
||||||
|
string? ResignReason) : IRequest;
|
||||||
|
|
||||||
|
public class UpdateEmployeeWorkHistoryCommandValidator : AbstractValidator<UpdateEmployeeWorkHistoryCommand>
|
||||||
|
{
|
||||||
|
public UpdateEmployeeWorkHistoryCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).NotEmpty();
|
||||||
|
RuleFor(x => x.CompanyName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.CompanyAddress).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Industry).MaximumLength(100);
|
||||||
|
RuleFor(x => x.JobTitle).MaximumLength(200);
|
||||||
|
RuleFor(x => x.JobDescription).MaximumLength(2000);
|
||||||
|
RuleFor(x => x.ResignReason).MaximumLength(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEmployeeWorkHistoryCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<UpdateEmployeeWorkHistoryCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UpdateEmployeeWorkHistoryCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeWorkHistories
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeWorkHistory", request.Id);
|
||||||
|
|
||||||
|
entity.CompanyName = request.CompanyName;
|
||||||
|
entity.CompanyAddress = request.CompanyAddress;
|
||||||
|
entity.Industry = request.Industry;
|
||||||
|
entity.FromDate = request.FromDate;
|
||||||
|
entity.ToDate = request.ToDate;
|
||||||
|
entity.JobTitle = request.JobTitle;
|
||||||
|
entity.JobDescription = request.JobDescription;
|
||||||
|
entity.ResignReason = request.ResignReason;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeleteEmployeeWorkHistoryCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteEmployeeWorkHistoryCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeWorkHistoryCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteEmployeeWorkHistoryCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeWorkHistories
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeWorkHistory", request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = DateTime.UtcNow;
|
||||||
|
entity.DeletedBy = currentUser.UserId;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Education
|
||||||
|
|
||||||
|
public record CreateEmployeeEducationCommand(
|
||||||
|
Guid EmployeeProfileId,
|
||||||
|
string SchoolName,
|
||||||
|
string? Major = null,
|
||||||
|
DegreeLevel? DegreeLevel = null,
|
||||||
|
EducationMode? EducationMode = null,
|
||||||
|
GradeLevel? GradeLevel = null,
|
||||||
|
DateOnly? FromDate = null,
|
||||||
|
DateOnly? ToDate = null,
|
||||||
|
DateOnly? CertificateIssueDate = null,
|
||||||
|
string? Notes = null) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateEmployeeEducationCommandValidator : AbstractValidator<CreateEmployeeEducationCommand>
|
||||||
|
{
|
||||||
|
public CreateEmployeeEducationCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EmployeeProfileId).NotEmpty();
|
||||||
|
RuleFor(x => x.SchoolName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Major).MaximumLength(200);
|
||||||
|
RuleFor(x => x.DegreeLevel).IsInEnum().When(x => x.DegreeLevel.HasValue);
|
||||||
|
RuleFor(x => x.EducationMode).IsInEnum().When(x => x.EducationMode.HasValue);
|
||||||
|
RuleFor(x => x.GradeLevel).IsInEnum().When(x => x.GradeLevel.HasValue);
|
||||||
|
RuleFor(x => x.Notes).MaximumLength(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateEmployeeEducationCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<CreateEmployeeEducationCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateEmployeeEducationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parentExists = await db.EmployeeProfiles
|
||||||
|
.AnyAsync(x => x.Id == request.EmployeeProfileId && !x.IsDeleted, ct);
|
||||||
|
if (!parentExists)
|
||||||
|
throw new NotFoundException("EmployeeProfile", request.EmployeeProfileId);
|
||||||
|
|
||||||
|
var entity = new EmployeeEducation
|
||||||
|
{
|
||||||
|
EmployeeProfileId = request.EmployeeProfileId,
|
||||||
|
SchoolName = request.SchoolName,
|
||||||
|
Major = request.Major,
|
||||||
|
DegreeLevel = request.DegreeLevel,
|
||||||
|
EducationMode = request.EducationMode,
|
||||||
|
GradeLevel = request.GradeLevel,
|
||||||
|
FromDate = request.FromDate,
|
||||||
|
ToDate = request.ToDate,
|
||||||
|
CertificateIssueDate = request.CertificateIssueDate,
|
||||||
|
Notes = request.Notes,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EmployeeEducations.Add(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateEmployeeEducationCommand(
|
||||||
|
Guid Id,
|
||||||
|
string SchoolName,
|
||||||
|
string? Major,
|
||||||
|
DegreeLevel? DegreeLevel,
|
||||||
|
EducationMode? EducationMode,
|
||||||
|
GradeLevel? GradeLevel,
|
||||||
|
DateOnly? FromDate,
|
||||||
|
DateOnly? ToDate,
|
||||||
|
DateOnly? CertificateIssueDate,
|
||||||
|
string? Notes) : IRequest;
|
||||||
|
|
||||||
|
public class UpdateEmployeeEducationCommandValidator : AbstractValidator<UpdateEmployeeEducationCommand>
|
||||||
|
{
|
||||||
|
public UpdateEmployeeEducationCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).NotEmpty();
|
||||||
|
RuleFor(x => x.SchoolName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Major).MaximumLength(200);
|
||||||
|
RuleFor(x => x.DegreeLevel).IsInEnum().When(x => x.DegreeLevel.HasValue);
|
||||||
|
RuleFor(x => x.EducationMode).IsInEnum().When(x => x.EducationMode.HasValue);
|
||||||
|
RuleFor(x => x.GradeLevel).IsInEnum().When(x => x.GradeLevel.HasValue);
|
||||||
|
RuleFor(x => x.Notes).MaximumLength(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEmployeeEducationCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<UpdateEmployeeEducationCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UpdateEmployeeEducationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeEducations
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeEducation", request.Id);
|
||||||
|
|
||||||
|
entity.SchoolName = request.SchoolName;
|
||||||
|
entity.Major = request.Major;
|
||||||
|
entity.DegreeLevel = request.DegreeLevel;
|
||||||
|
entity.EducationMode = request.EducationMode;
|
||||||
|
entity.GradeLevel = request.GradeLevel;
|
||||||
|
entity.FromDate = request.FromDate;
|
||||||
|
entity.ToDate = request.ToDate;
|
||||||
|
entity.CertificateIssueDate = request.CertificateIssueDate;
|
||||||
|
entity.Notes = request.Notes;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeleteEmployeeEducationCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteEmployeeEducationCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeEducationCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteEmployeeEducationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeEducations
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeEducation", request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = DateTime.UtcNow;
|
||||||
|
entity.DeletedBy = currentUser.UserId;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region FamilyRelation
|
||||||
|
|
||||||
|
public record CreateEmployeeFamilyRelationCommand(
|
||||||
|
Guid EmployeeProfileId,
|
||||||
|
string FullName,
|
||||||
|
FamilyRelationKind Relationship,
|
||||||
|
int? BirthYear = null,
|
||||||
|
string? Occupation = null,
|
||||||
|
string? CurrentAddress = null,
|
||||||
|
string? Phone = null) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateEmployeeFamilyRelationCommandValidator : AbstractValidator<CreateEmployeeFamilyRelationCommand>
|
||||||
|
{
|
||||||
|
public CreateEmployeeFamilyRelationCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EmployeeProfileId).NotEmpty();
|
||||||
|
RuleFor(x => x.FullName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Relationship).IsInEnum();
|
||||||
|
RuleFor(x => x.BirthYear).InclusiveBetween(1900, 2026).When(x => x.BirthYear.HasValue);
|
||||||
|
RuleFor(x => x.Occupation).MaximumLength(200);
|
||||||
|
RuleFor(x => x.CurrentAddress).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Phone).MaximumLength(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateEmployeeFamilyRelationCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<CreateEmployeeFamilyRelationCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateEmployeeFamilyRelationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parentExists = await db.EmployeeProfiles
|
||||||
|
.AnyAsync(x => x.Id == request.EmployeeProfileId && !x.IsDeleted, ct);
|
||||||
|
if (!parentExists)
|
||||||
|
throw new NotFoundException("EmployeeProfile", request.EmployeeProfileId);
|
||||||
|
|
||||||
|
var entity = new EmployeeFamilyRelation
|
||||||
|
{
|
||||||
|
EmployeeProfileId = request.EmployeeProfileId,
|
||||||
|
FullName = request.FullName,
|
||||||
|
Relationship = request.Relationship,
|
||||||
|
BirthYear = request.BirthYear,
|
||||||
|
Occupation = request.Occupation,
|
||||||
|
CurrentAddress = request.CurrentAddress,
|
||||||
|
Phone = request.Phone,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EmployeeFamilyRelations.Add(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateEmployeeFamilyRelationCommand(
|
||||||
|
Guid Id,
|
||||||
|
string FullName,
|
||||||
|
FamilyRelationKind Relationship,
|
||||||
|
int? BirthYear,
|
||||||
|
string? Occupation,
|
||||||
|
string? CurrentAddress,
|
||||||
|
string? Phone) : IRequest;
|
||||||
|
|
||||||
|
public class UpdateEmployeeFamilyRelationCommandValidator : AbstractValidator<UpdateEmployeeFamilyRelationCommand>
|
||||||
|
{
|
||||||
|
public UpdateEmployeeFamilyRelationCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).NotEmpty();
|
||||||
|
RuleFor(x => x.FullName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Relationship).IsInEnum();
|
||||||
|
RuleFor(x => x.BirthYear).InclusiveBetween(1900, 2026).When(x => x.BirthYear.HasValue);
|
||||||
|
RuleFor(x => x.Occupation).MaximumLength(200);
|
||||||
|
RuleFor(x => x.CurrentAddress).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Phone).MaximumLength(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEmployeeFamilyRelationCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<UpdateEmployeeFamilyRelationCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UpdateEmployeeFamilyRelationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeFamilyRelations
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeFamilyRelation", request.Id);
|
||||||
|
|
||||||
|
entity.FullName = request.FullName;
|
||||||
|
entity.Relationship = request.Relationship;
|
||||||
|
entity.BirthYear = request.BirthYear;
|
||||||
|
entity.Occupation = request.Occupation;
|
||||||
|
entity.CurrentAddress = request.CurrentAddress;
|
||||||
|
entity.Phone = request.Phone;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeleteEmployeeFamilyRelationCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteEmployeeFamilyRelationCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeFamilyRelationCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteEmployeeFamilyRelationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeFamilyRelations
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeFamilyRelation", request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = DateTime.UtcNow;
|
||||||
|
entity.DeletedBy = currentUser.UserId;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Skill
|
||||||
|
|
||||||
|
public record CreateEmployeeSkillCommand(
|
||||||
|
Guid EmployeeProfileId,
|
||||||
|
SkillKind Kind,
|
||||||
|
string Name,
|
||||||
|
string? LanguageId = null,
|
||||||
|
string? Level = null) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateEmployeeSkillCommandValidator : AbstractValidator<CreateEmployeeSkillCommand>
|
||||||
|
{
|
||||||
|
public CreateEmployeeSkillCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EmployeeProfileId).NotEmpty();
|
||||||
|
RuleFor(x => x.Kind).IsInEnum();
|
||||||
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.LanguageId).MaximumLength(10);
|
||||||
|
RuleFor(x => x.Level).MaximumLength(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateEmployeeSkillCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<CreateEmployeeSkillCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateEmployeeSkillCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parentExists = await db.EmployeeProfiles
|
||||||
|
.AnyAsync(x => x.Id == request.EmployeeProfileId && !x.IsDeleted, ct);
|
||||||
|
if (!parentExists)
|
||||||
|
throw new NotFoundException("EmployeeProfile", request.EmployeeProfileId);
|
||||||
|
|
||||||
|
var entity = new EmployeeSkill
|
||||||
|
{
|
||||||
|
EmployeeProfileId = request.EmployeeProfileId,
|
||||||
|
Kind = request.Kind,
|
||||||
|
Name = request.Name,
|
||||||
|
LanguageId = request.LanguageId,
|
||||||
|
Level = request.Level,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EmployeeSkills.Add(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateEmployeeSkillCommand(
|
||||||
|
Guid Id,
|
||||||
|
SkillKind Kind,
|
||||||
|
string Name,
|
||||||
|
string? LanguageId,
|
||||||
|
string? Level) : IRequest;
|
||||||
|
|
||||||
|
public class UpdateEmployeeSkillCommandValidator : AbstractValidator<UpdateEmployeeSkillCommand>
|
||||||
|
{
|
||||||
|
public UpdateEmployeeSkillCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).NotEmpty();
|
||||||
|
RuleFor(x => x.Kind).IsInEnum();
|
||||||
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.LanguageId).MaximumLength(10);
|
||||||
|
RuleFor(x => x.Level).MaximumLength(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEmployeeSkillCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<UpdateEmployeeSkillCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UpdateEmployeeSkillCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeSkills
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeSkill", request.Id);
|
||||||
|
|
||||||
|
entity.Kind = request.Kind;
|
||||||
|
entity.Name = request.Name;
|
||||||
|
entity.LanguageId = request.LanguageId;
|
||||||
|
entity.Level = request.Level;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeleteEmployeeSkillCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteEmployeeSkillCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeSkillCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteEmployeeSkillCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeSkills
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeSkill", request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = DateTime.UtcNow;
|
||||||
|
entity.DeletedBy = currentUser.UserId;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Document
|
||||||
|
|
||||||
|
// Phase 1.5 S34 — metadata-only CRUD. File upload body (multipart) DEFER S35
|
||||||
|
// khi wire IFileStorage. Tạm thời admin paste sẵn FilePath/FileSize/ContentType
|
||||||
|
// từ chỗ khác (UI form input chấp nhận thủ công).
|
||||||
|
public record CreateEmployeeDocumentCommand(
|
||||||
|
Guid EmployeeProfileId,
|
||||||
|
EmployeeDocumentType DocumentType,
|
||||||
|
string FileName,
|
||||||
|
string FilePath,
|
||||||
|
long FileSize,
|
||||||
|
string ContentType,
|
||||||
|
DateOnly? IssueDate = null,
|
||||||
|
DateOnly? ExpiryDate = null,
|
||||||
|
string? Notes = null) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateEmployeeDocumentCommandValidator : AbstractValidator<CreateEmployeeDocumentCommand>
|
||||||
|
{
|
||||||
|
public CreateEmployeeDocumentCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EmployeeProfileId).NotEmpty();
|
||||||
|
RuleFor(x => x.DocumentType).IsInEnum();
|
||||||
|
RuleFor(x => x.FileName).NotEmpty().MaximumLength(500);
|
||||||
|
RuleFor(x => x.FilePath).NotEmpty().MaximumLength(1000);
|
||||||
|
RuleFor(x => x.FileSize).GreaterThanOrEqualTo(0);
|
||||||
|
RuleFor(x => x.ContentType).NotEmpty().MaximumLength(100);
|
||||||
|
RuleFor(x => x.Notes).MaximumLength(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateEmployeeDocumentCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<CreateEmployeeDocumentCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateEmployeeDocumentCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parentExists = await db.EmployeeProfiles
|
||||||
|
.AnyAsync(x => x.Id == request.EmployeeProfileId && !x.IsDeleted, ct);
|
||||||
|
if (!parentExists)
|
||||||
|
throw new NotFoundException("EmployeeProfile", request.EmployeeProfileId);
|
||||||
|
|
||||||
|
var entity = new EmployeeDocument
|
||||||
|
{
|
||||||
|
EmployeeProfileId = request.EmployeeProfileId,
|
||||||
|
DocumentType = request.DocumentType,
|
||||||
|
FileName = request.FileName,
|
||||||
|
FilePath = request.FilePath,
|
||||||
|
FileSize = request.FileSize,
|
||||||
|
ContentType = request.ContentType,
|
||||||
|
IssueDate = request.IssueDate,
|
||||||
|
ExpiryDate = request.ExpiryDate,
|
||||||
|
Notes = request.Notes,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EmployeeDocuments.Add(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateEmployeeDocumentCommand(
|
||||||
|
Guid Id,
|
||||||
|
EmployeeDocumentType DocumentType,
|
||||||
|
string FileName,
|
||||||
|
string FilePath,
|
||||||
|
long FileSize,
|
||||||
|
string ContentType,
|
||||||
|
DateOnly? IssueDate,
|
||||||
|
DateOnly? ExpiryDate,
|
||||||
|
string? Notes) : IRequest;
|
||||||
|
|
||||||
|
public class UpdateEmployeeDocumentCommandValidator : AbstractValidator<UpdateEmployeeDocumentCommand>
|
||||||
|
{
|
||||||
|
public UpdateEmployeeDocumentCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).NotEmpty();
|
||||||
|
RuleFor(x => x.DocumentType).IsInEnum();
|
||||||
|
RuleFor(x => x.FileName).NotEmpty().MaximumLength(500);
|
||||||
|
RuleFor(x => x.FilePath).NotEmpty().MaximumLength(1000);
|
||||||
|
RuleFor(x => x.FileSize).GreaterThanOrEqualTo(0);
|
||||||
|
RuleFor(x => x.ContentType).NotEmpty().MaximumLength(100);
|
||||||
|
RuleFor(x => x.Notes).MaximumLength(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEmployeeDocumentCommandHandler(
|
||||||
|
IApplicationDbContext db) : IRequestHandler<UpdateEmployeeDocumentCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UpdateEmployeeDocumentCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeDocuments
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeDocument", request.Id);
|
||||||
|
|
||||||
|
entity.DocumentType = request.DocumentType;
|
||||||
|
entity.FileName = request.FileName;
|
||||||
|
entity.FilePath = request.FilePath;
|
||||||
|
entity.FileSize = request.FileSize;
|
||||||
|
entity.ContentType = request.ContentType;
|
||||||
|
entity.IssueDate = request.IssueDate;
|
||||||
|
entity.ExpiryDate = request.ExpiryDate;
|
||||||
|
entity.Notes = request.Notes;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeleteEmployeeDocumentCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteEmployeeDocumentCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeDocumentCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteEmployeeDocumentCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var entity = await db.EmployeeDocuments
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
|
||||||
|
?? throw new NotFoundException("EmployeeDocument", request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = DateTime.UtcNow;
|
||||||
|
entity.DeletedBy = currentUser.UserId;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
Reference in New Issue
Block a user