using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using System.Text.Json; using WiBLL.Common; using WiBLL.EmployeeLeaveLink; using WiDAL.CustomCode.LineRoster; using WiDAL.DTOs; namespace WiBLL.LineRoster; public class LineRosterBLL : ILineRosterBLL { private readonly ILineRosterDAL _dal; private readonly AutoNumber _autoNumber; private readonly IValidation _validation; private readonly IWiHubNotifier _hubNotifier; private readonly IEmployeeLeaveLinkBLL _leaveLinkBll; public LineRosterBLL(ILineRosterDAL dal, AutoNumber autoNumber, IValidation validation, IWiHubNotifier hubNotifier, IEmployeeLeaveLinkBLL leaveLinkBll) { _dal = dal; _autoNumber = autoNumber; _validation = validation; _hubNotifier = hubNotifier; _leaveLinkBll = leaveLinkBll; } public async Task GetRosterAsync( int productionLineId, DateOnly rosterDate, int shiftId, byte rosterType, LoginDTO login, CancellationToken ct) { var dto = await _dal.GetRosterHeaderAsync(productionLineId, rosterDate, shiftId, rosterType, login, ct) .ConfigureAwait(false); return JsonSerializer.Serialize(dto); } public async Task SaveRosterAsync(LineShiftRosterDTO dto, LoginDTO login, CancellationToken ct) { if (dto.ProductionLineId == 0) throw new InvalidOperationException("ProductionLineId is required."); if (dto.ShiftId == 0) throw new InvalidOperationException("ShiftId is required."); if (dto.RosterDate == default) throw new InvalidOperationException("RosterDate is required."); bool isNew = dto.LineShiftRosterId == 0; if (isNew) { var an = await _autoNumber.GetNumberAsync(1, "WIVERSION", login).ConfigureAwait(false); dto.LineShiftRosterId = an.StartNumber; try { await _dal.InsertRosterAsync(dto, login, ct).ConfigureAwait(false); } catch { await _autoNumber.RollbackAutoNumber("WIVERSION", dto.LineShiftRosterId, login).ConfigureAwait(false); throw; } return $"{SuccessResponse.SaveSuccess}"; } await _dal.UpdateRosterStatusAsync(dto.LineShiftRosterId, (byte)dto.RosterStatus, dto.InChargeEmployeeId, login, ct) .ConfigureAwait(false); return $"{SuccessResponse.UpdateSuccess}"; } // Actual→Confirmed only — a Planned roster is a forecast, never directly confirmable public async Task ConfirmRosterAsync(int rosterId, int inChargeEmployeeId, LoginDTO login, CancellationToken ct) { var current = await _dal.GetRosterByIdAsync(rosterId, login, ct).ConfigureAwait(false); if (current is null) throw new InvalidOperationException($"Roster {rosterId} not found."); if (current.RosterType != RosterType.Actual) throw new InvalidOperationException("Only Actual rosters can be Confirmed."); await _dal.UpdateRosterStatusAsync(rosterId, (byte)RosterStatus.Confirmed, inChargeEmployeeId, login, ct) .ConfigureAwait(false); return $"{SuccessResponse.UpdateSuccessMessage} {rosterId}"; } public async Task CloseRosterAsync(int rosterId, LoginDTO login, CancellationToken ct) { await _dal.UpdateRosterStatusAsync(rosterId, (byte)RosterStatus.Closed, -1, login, ct) .ConfigureAwait(false); return $"{SuccessResponse.UpdateSuccessMessage} {rosterId}"; } // Copy-forward: seed a new planned roster from the most recent one for the same line+shift public async Task CopyForwardAsync(int productionLineId, int shiftId, DateOnly newRosterDate, LoginDTO login, CancellationToken ct) { if (productionLineId == 0) throw new InvalidOperationException("ProductionLineId is required."); if (shiftId == 0) throw new InvalidOperationException("ShiftId is required."); var source = await _dal.GetCopyForwardSourceAsync( productionLineId, shiftId, (byte)RosterType.Planned, newRosterDate, login, ct) .ConfigureAwait(false); if (source is null) return "No previous roster found for this line and shift to copy from."; // Create new Planned roster header var an = await _autoNumber.GetNumberAsync(1, "WIVERSION", login).ConfigureAwait(false); int newRosterId = an.StartNumber; var newRoster = new LineShiftRosterDTO { LineShiftRosterId = newRosterId, ProductionLineId = productionLineId, ShiftId = shiftId, RosterDate = newRosterDate, RosterType = RosterType.Planned, RosterStatus = RosterStatus.Draft, InChargeEmployeeId = source.InChargeEmployeeId, PlannedRosterId = null }; try { await _dal.InsertRosterAsync(newRoster, login, ct).ConfigureAwait(false); // Copy staffing rows — reset IsAbsent to false for new planned roster var staffingJson = await _dal.GetStaffingByRosterAsync(source.LineShiftRosterId, login, ct) .ConfigureAwait(false); var staffingRows = System.Text.Json.JsonSerializer.Deserialize>(staffingJson) ?? []; foreach (var row in staffingRows) { row.LineShiftRosterId = newRosterId; row.IsAbsent = false; row.AbsenceReason = null; row.ReplacedByEmployeeId = null; await _dal.InsertStaffingAsync(row, login, ct).ConfigureAwait(false); } } catch { await _autoNumber.RollbackAutoNumber("WIVERSION", newRosterId, login).ConfigureAwait(false); throw; } return $"{SuccessResponse.SaveSuccess}"; } public async Task SaveStaffingAsync(LineShiftStaffingDTO dto, LoginDTO login, CancellationToken ct) { if (dto.LineShiftRosterId == 0) throw new InvalidOperationException("LineShiftRosterId is required."); if (dto.WorkstationId == 0) throw new InvalidOperationException("WorkstationId is required."); if (dto.EmployeeId == 0) throw new InvalidOperationException("EmployeeId is required."); // Enforce: only one ISPRIMARY=1 per workstation per roster (read existing before inserting) if (dto.IsPrimary) { var existingJson = await _dal.GetStaffingByRosterAsync(dto.LineShiftRosterId, login, ct) .ConfigureAwait(false); var existing = System.Text.Json.JsonSerializer.Deserialize>(existingJson) ?? []; bool alreadyHasPrimary = existing.Any(s => s.WorkstationId == dto.WorkstationId && s.IsPrimary && !s.IsAbsent); if (alreadyHasPrimary) throw new InvalidOperationException( $"Workstation {dto.WorkstationId} already has a primary operator on this roster."); } await _dal.InsertStaffingAsync(dto, login, ct).ConfigureAwait(false); var roster = await _dal.GetRosterByIdAsync(dto.LineShiftRosterId, login, ct).ConfigureAwait(false); if (roster is not null) await _hubNotifier.NotifyStaffingChangedAsync(roster.ProductionLineId, roster.RosterDate, roster.ShiftId, ct) .ConfigureAwait(false); return SuccessResponse.SaveSuccess; } // Never DELETE — mark absent, optionally record replacement public async Task MarkAbsentAsync(int staffingId, string? absenceReason, int? replacedByEmployeeId, LoginDTO login, CancellationToken ct) { if (staffingId == 0) throw new InvalidOperationException("StaffingId is required."); await _dal.MarkAbsentAsync(staffingId, absenceReason, replacedByEmployeeId, login, ct) .ConfigureAwait(false); var rosterId = await _dal.GetRosterIdForStaffing(staffingId, login, ct).ConfigureAwait(false); if (rosterId is not null) { var roster = await _dal.GetRosterByIdAsync(rosterId.Value, login, ct).ConfigureAwait(false); if (roster is not null) await _hubNotifier.NotifyStaffingChangedAsync(roster.ProductionLineId, roster.RosterDate, roster.ShiftId, ct) .ConfigureAwait(false); } return SuccessResponse.UpdateSuccess; } public async Task GetPlannedVsActualAsync(int productionLineId, DateOnly rosterDate, int shiftId, int plannedInChargeEmployeeId, LoginDTO login, CancellationToken ct) => await _dal.GetPlannedVsActualAsync(productionLineId, rosterDate, shiftId, plannedInChargeEmployeeId, login, ct) .ConfigureAwait(false); public async Task GetStaffingByRosterAsync(int rosterId, LoginDTO login, CancellationToken ct) { var json = await _dal.GetStaffingByRosterAsync(rosterId, login, ct).ConfigureAwait(false); var staffing = JsonSerializer.Deserialize>(json) ?? []; if (staffing.Count > 0) { var roster = await _dal.GetRosterByIdAsync(rosterId, login, ct).ConfigureAwait(false); if (roster is not null) { var employeeIds = staffing.Select(s => s.EmployeeId).Distinct(); var onLeave = await _leaveLinkBll.GetApprovedLeaveEmployeeIdsAsync(employeeIds, roster.RosterDate, login, ct).ConfigureAwait(false); foreach (var row in staffing) row.OnApprovedLeave = onLeave.Contains(row.EmployeeId); } } return JsonSerializer.Serialize(staffing); } public async Task SaveRosterWithStaffingAsync(SaveLineRosterWithStaffingDTO request,LoginDTO login,CancellationToken ct) { if (request.Roster is null) throw new InvalidOperationException("Roster information is required."); if (request.Roster.ProductionLineId == 0) throw new InvalidOperationException("ProductionLineId is required."); if (request.Roster.ShiftId == 0) throw new InvalidOperationException("ShiftId is required."); if (request.Roster.RosterDate == default) throw new InvalidOperationException("RosterDate is required."); if (request.Staffing is null) request.Staffing = []; // Generate roster ID var an = await _autoNumber.GetNumberAsync(1,"WIVERSION",login).ConfigureAwait(false); int rosterId = an.StartNumber; request.Roster.LineShiftRosterId = rosterId; try { // Save roster header await _dal.InsertRosterAsync(request.Roster,login,ct).ConfigureAwait(false); // Save staffing rows foreach (var staffing in request.Staffing) { if (staffing.WorkstationId == 0) throw new InvalidOperationException("WorkstationId is required."); if (staffing.EmployeeId == 0) throw new InvalidOperationException("EmployeeId is required."); staffing.LineShiftRosterId = rosterId; // Primary validation if (staffing.IsPrimary) { var existingJson =await _dal.GetStaffingByRosterAsync(rosterId,login,ct).ConfigureAwait(false); var existing =JsonSerializer.Deserialize>(existingJson) ?? []; bool alreadyHasPrimary = existing.Any(x =>x.WorkstationId == staffing.WorkstationId &&x.IsPrimary &&!x.IsAbsent); if (alreadyHasPrimary) { throw new InvalidOperationException($"Workstation {staffing.WorkstationId} already has a primary operator."); } } await _dal.InsertStaffingAsync(staffing,login,ct).ConfigureAwait(false); } await _hubNotifier.NotifyStaffingChangedAsync( request.Roster.ProductionLineId, request.Roster.RosterDate, request.Roster.ShiftId, ct) .ConfigureAwait(false); return $"{SuccessResponse.SaveSuccess}"; } catch { await _autoNumber.RollbackAutoNumber("WIVERSION",rosterId,login).ConfigureAwait(false); throw; } } }