[CLAUDE] Domain+App+Infra+Api+FE-Admin+FE-User: S38 G-O4+G-O5+G-O6+G-P1+G-H3 SKELETON full-stack
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m53s

Phase 10.3-10.4 SKELETON 5 plan combo finish — Mig 39+40 + BE skeleton 7 module +
FE 2 app SHA256 IDENTICAL + 11 menu key. UAT visible end-to-end.

⚠️ SKELETON Phase 1 trade-off rõ:
  - Status flat 5-state WorkflowAppStatus enum share Leave/OT/Travel/Vehicle
  - ApproveV2 workflow advance DEFER Phase 11 (Drafter Create OK, Approve flow chưa wire)
  - LevelOpinions per-module DEFER Phase 11
  - LeaveBalance calc + Auto-assign + SLA timer DEFER Phase 11
  - CodeGen atomic + MaDonTu/MaTicket gen DEFER Phase 11
  - Vehicle catalog + Driver catalog DEFER Phase 11 (free text VehicleLicense)
  - ItTicketComments thread DEFER Phase 11 (free text Resolution field)

Mig 39 (em main solo): 5 entity Workflow Apps schema
  - LeaveRequest (G-O4, FK LeaveType Hrm Mig 35, ApplicableType=5)
  - OtRequest (G-O4, FK OtPolicy optional, ApplicableType=6)
  - TravelRequest (G-O4, reuse ApplicableType=4 Proposal)
  - VehicleBooking (G-O5, free text vehicle, ApplicableType=7)
  - ItTicket (G-O6, NO workflow V2 — kanban status flow)

Mig 40 (em main solo): Attendance entity (G-P1)
  - GPS lat/long check-in/out + Source enum Web/Mobile/Device
  - UNIQUE composite (UserId, AttendanceDate)
  - WorkHours computed simple diff (NO OtPolicy multiplier yet)

BE CQRS (em main solo, single mega ~1100 LOC):
  - WorkflowAppsFeatures.cs 7 region (5 module Create+List + Attendance CheckIn/Out/GetMonth + HrDashboard)
  - 7 Controller: /api/leave-requests + /ot-requests + /travel-requests + /vehicle-bookings + /it-tickets + /attendances + /hr/dashboard
  - Class-level [Authorize] any authenticated
  - 13 endpoint total

FE 2 app (em main solo fallback gotcha #53 risk):
  - types/workflowApps.ts × 2 SHA256 IDENTICAL 77470e182a15de88 (all DTOs + Status badge)
  - WorkflowAppsListPage.tsx × 2 IDENTICAL 58139d0301a60ddf — generic declarative KIND_CONFIG handles 4 module (Leave/OT/Travel/Vehicle)
  - ItTicketsPage.tsx × 2 IDENTICAL d3062de2f54c794c — kanban 5 status column
  - MyAttendancePage.tsx × 2 IDENTICAL 86da48ae147db012 — GPS check-in/out + tháng calendar
  - HrmDashboardPage.tsx × 2 IDENTICAL d9c6c12a5a8694f8 — 4 KPI card + gender ratio + status breakdown
  - Pattern 16-bis 9× cumulative (App.tsx +4 routes + menuKeys +8 const + Layout staticMap +7 entry)
  - 7 amber banner "Skeleton Phase 1 — full feature Phase 11" rõ ràng UAT

Menu seed: +11 const + SeedMenuTreeAsync 8 row (Off_DonTu sub-group + 3 leaf + Off_DatXe + Off_ItTicket + Off_ChamCong + Hrm_Dashboard).
DbInitializer Sample workflow seed DEFER (workflows V2 already seeded từ S29+S37 reuse — admin clone tạo riêng per ApplicableType=5/6/7).

Verify:
- dotnet build PASS 0 error 2 pre-existing warning
- dotnet test 130/130 PASS baseline preserve
- npm build × 2 PASS clean
- SHA256 verify 5 file × 2 app all IDENTICAL

Plan G-* progress 11/11  (100% COMPLETE):
   G-H1 (S33) + G-O1 (S34) + G-H2 (S35) + G-O2 (S36) + G-O3 (S37) +
   G-O4 + G-O5 + G-O6 + G-P1 + G-H3 (S38 skeleton)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-28 16:19:42 +07:00
parent 17aaba9df0
commit e54a22de0c
41 changed files with 14980 additions and 0 deletions

View File

@ -0,0 +1,33 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/attendances")]
[Authorize]
public class AttendancesController(IMediator mediator) : ControllerBase
{
[HttpPost("check-in")]
public async Task<IActionResult> CheckIn([FromBody] CheckInCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
[HttpPost("check-out")]
public async Task<IActionResult> CheckOut([FromBody] CheckOutCommand cmd)
{
await mediator.Send(cmd);
return NoContent();
}
[HttpGet("me")]
public async Task<IActionResult> GetMyMonth([FromQuery] int? year, [FromQuery] int? month)
{
var now = DateTime.Now;
return Ok(await mediator.Send(new GetMyAttendanceQuery(year ?? now.Year, month ?? now.Month)));
}
}

View File

@ -0,0 +1,16 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/hr/dashboard")]
[Authorize]
public class HrDashboardController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get()
=> Ok(await mediator.Send(new GetHrDashboardQuery()));
}

View File

@ -0,0 +1,33 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/it-tickets")]
[Authorize]
public class ItTicketsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] int? category, [FromQuery] int? priority,
[FromQuery] Guid? assignedToUserId, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
=> Ok(await mediator.Send(new GetItTicketsQuery(status, category, priority, assignedToUserId, requesterUserId, page, pageSize)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateItTicketCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
[HttpPut("{id:guid}/status")]
public async Task<IActionResult> UpdateStatus(Guid id, [FromBody] UpdateItTicketStatusBody body)
{
await mediator.Send(new UpdateItTicketStatusCommand(id, body.Status, body.Resolution));
return NoContent();
}
public record UpdateItTicketStatusBody(int Status, string? Resolution);
}

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/leave-requests")]
[Authorize]
public class LeaveRequestsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
=> Ok(await mediator.Send(new GetLeaveRequestsQuery(status, requesterUserId, page, pageSize)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateLeaveRequestCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
}

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/ot-requests")]
[Authorize]
public class OtRequestsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
=> Ok(await mediator.Send(new GetOtRequestsQuery(status, requesterUserId, page, pageSize)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOtRequestCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
}

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/travel-requests")]
[Authorize]
public class TravelRequestsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
=> Ok(await mediator.Send(new GetTravelRequestsQuery(status, requesterUserId, page, pageSize)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateTravelRequestCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
}

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Office;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/vehicle-bookings")]
[Authorize]
public class VehicleBookingsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
=> Ok(await mediator.Send(new GetVehicleBookingsQuery(status, requesterUserId, page, pageSize)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateVehicleBookingCommand cmd)
{
var id = await mediator.Send(cmd);
return Created(string.Empty, new { id });
}
}