feat(booking-service, web-client-tpos): implement staff schedule creation/deletion and enhance staff name display.

This commit is contained in:
Ho Ngoc Hai
2026-03-05 16:19:46 +07:00
parent 81c5be9e37
commit 3f1ecc8122
5 changed files with 86 additions and 7 deletions

View File

@@ -108,7 +108,10 @@ else if (SubSection == "shifts")
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Nhân viên</label>
<select @bind="_newSchedStaffIdStr" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);font-size:13px;">
<option value="">-- Chọn NV --</option>
@foreach (var s in _staff) { <option value="@s.Id">@(s.EmployeeCode ?? s.Id.ToString()[..8])</option> }
@foreach (var s in _staff) {
var sName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : (s.EmployeeCode ?? s.Id.ToString()[..8]);
<option value="@s.Id">@sName</option>
}
</select>
</div>
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Ngày</label>
@@ -148,7 +151,8 @@ else if (SubSection == "shifts")
@foreach (var emp in _staff)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;white-space:nowrap;">@(emp.EmployeeCode ?? emp.Id.ToString()[..8])</td>
@{ var empName = !string.IsNullOrWhiteSpace(emp.LastName) || !string.IsNullOrWhiteSpace(emp.FirstName) ? $"{emp.LastName} {emp.FirstName}".Trim() : (emp.EmployeeCode ?? emp.Id.ToString()[..8]); }
<td style="padding:12px 16px;font-weight:600;white-space:nowrap;">@empName</td>
@foreach (var dow in new[] { 1, 2, 3, 4, 5, 6, 0 })
{
var sched = _staffSchedules.FirstOrDefault(s => s.StaffId == emp.Id && s.DayOfWeek == dow);

View File

@@ -154,7 +154,7 @@ public class StaffController : ControllerBase
public Task<IActionResult> GetStaffSchedules([FromQuery] Guid? shopId = null)
{
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
return _booking.GetAsync($"/api/v1/schedules{qs}").ProxyAsync();
return _booking.GetAsync($"/api/v1.0/schedules{qs}").ProxyAsync();
}
/// <summary>
@@ -163,7 +163,7 @@ public class StaffController : ControllerBase
/// </summary>
[HttpPost("staff/schedules")]
public Task<IActionResult> CreateSchedule([FromBody] JsonElement body) =>
_booking.PostAsJsonAsync("/api/v1/schedules", body).ProxyAsync();
_booking.PostAsJsonAsync("/api/v1.0/schedules", body).ProxyAsync();
/// <summary>
/// EN: Update a staff schedule — proxies to BookingService.
@@ -171,7 +171,7 @@ public class StaffController : ControllerBase
/// </summary>
[HttpPut("staff/schedules/{scheduleId:guid}")]
public Task<IActionResult> UpdateSchedule(Guid scheduleId, [FromBody] JsonElement body) =>
_booking.PutAsJsonAsync($"/api/v1/schedules/{scheduleId}", body).ProxyAsync();
_booking.PutAsJsonAsync($"/api/v1.0/schedules/{scheduleId}", body).ProxyAsync();
/// <summary>
/// EN: Delete a staff schedule — proxies to BookingService.
@@ -179,5 +179,5 @@ public class StaffController : ControllerBase
/// </summary>
[HttpDelete("staff/schedules/{scheduleId:guid}")]
public Task<IActionResult> DeleteSchedule(Guid scheduleId) =>
_booking.DeleteAsync($"/api/v1/schedules/{scheduleId}").ProxyAsync();
_booking.DeleteAsync($"/api/v1.0/schedules/{scheduleId}").ProxyAsync();
}

View File

@@ -5,6 +5,8 @@ using Asp.Versioning;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Responses;
using BookingService.Domain.AggregatesModel.StaffAggregate;
using BookingService.Infrastructure.Repositories;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -17,11 +19,13 @@ namespace BookingService.API.Controllers;
public class SchedulesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly IStaffScheduleRepository _scheduleRepository;
private readonly ILogger<SchedulesController> _logger;
public SchedulesController(IMediator mediator, ILogger<SchedulesController> logger)
public SchedulesController(IMediator mediator, IStaffScheduleRepository scheduleRepository, ILogger<SchedulesController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_scheduleRepository = scheduleRepository ?? throw new ArgumentNullException(nameof(scheduleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -44,4 +48,69 @@ public class SchedulesController : ControllerBase
return Ok(ApiResponse<List<StaffScheduleDto>>.Ok(result));
}
/// <summary>
/// EN: Create a single schedule entry.
/// VI: Tạo một entry lịch làm việc.
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<StaffScheduleDto>), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ApiResponse<StaffScheduleDto>>> CreateSchedule(
[FromBody] CreateScheduleRequest request,
CancellationToken cancellationToken = default)
{
if (request.ShopId == Guid.Empty || request.StaffId == Guid.Empty)
return BadRequest(ApiResponse<StaffScheduleDto>.Fail("ShopId and StaffId are required"));
if (!TimeOnly.TryParse(request.StartTime, out var startTime) ||
!TimeOnly.TryParse(request.EndTime, out var endTime))
return BadRequest(ApiResponse<StaffScheduleDto>.Fail("Invalid time format. Use HH:mm"));
var schedule = new StaffSchedule(request.StaffId, request.ShopId, request.DayOfWeek, startTime, endTime);
_scheduleRepository.Add(schedule);
await _scheduleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation("Schedule created: {Id} for staff {StaffId}", schedule.Id, request.StaffId);
var dto = new StaffScheduleDto
{
Id = schedule.Id,
StaffId = schedule.StaffId,
ShopId = schedule.ShopId,
DayOfWeek = schedule.DayOfWeek,
StartTime = schedule.StartTime,
EndTime = schedule.EndTime
};
return Created($"/api/v1/schedules/{schedule.Id}", ApiResponse<StaffScheduleDto>.Ok(dto));
}
/// <summary>
/// EN: Delete a schedule entry by ID.
/// VI: Xóa một entry lịch làm việc theo ID.
/// </summary>
[HttpDelete("{id:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteSchedule(Guid id, CancellationToken cancellationToken = default)
{
var schedule = await _scheduleRepository.GetByIdAsync(id, cancellationToken);
if (schedule == null)
return NotFound(ApiResponse<object>.Fail("Schedule not found"));
_scheduleRepository.Remove(schedule);
await _scheduleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation("Schedule deleted: {Id}", id);
return Ok(new { success = true, message = "Schedule deleted" });
}
}
public record CreateScheduleRequest
{
public Guid ShopId { get; init; }
public Guid StaffId { get; init; }
public int DayOfWeek { get; init; }
public string StartTime { get; init; } = "08:00";
public string EndTime { get; init; } = "17:00";
}

View File

@@ -15,4 +15,5 @@ public interface IStaffScheduleRepository
Task<List<StaffSchedule>> GetByStaffIdAsync(Guid staffId, Guid shopId, CancellationToken cancellationToken = default);
Task<List<StaffSchedule>> GetByShopIdAndDayAsync(Guid shopId, int dayOfWeek, CancellationToken cancellationToken = default);
Task<List<StaffSchedule>> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default);
Task<StaffSchedule?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}

View File

@@ -55,4 +55,9 @@ public class StaffScheduleRepository : IStaffScheduleRepository
.ThenBy(s => s.DayOfWeek)
.ToListAsync(cancellationToken);
}
public async Task<StaffSchedule?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.StaffSchedules.FirstOrDefaultAsync(s => s.Id == id, cancellationToken);
}
}