using Microsoft.EntityFrameworkCore; using WorkClub.Application.Interfaces; using WorkClub.Application.Shifts.DTOs; using WorkClub.Domain.Entities; using WorkClub.Infrastructure.Data; namespace WorkClub.Api.Services; public class ShiftService { private readonly AppDbContext _context; private readonly ITenantProvider _tenantProvider; public ShiftService(AppDbContext context, ITenantProvider tenantProvider) { _context = context; _tenantProvider = tenantProvider; } public async Task GetShiftsAsync(DateTimeOffset? from, DateTimeOffset? to, int page, int pageSize) { var query = _context.Shifts.AsQueryable(); if (from.HasValue) query = query.Where(s => s.StartTime >= from.Value); if (to.HasValue) query = query.Where(s => s.StartTime <= to.Value); var total = await query.CountAsync(); var shifts = await query .OrderBy(s => s.StartTime) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); var shiftIds = shifts.Select(s => s.Id).ToList(); var signupCounts = await _context.ShiftSignups .Where(ss => shiftIds.Contains(ss.ShiftId)) .GroupBy(ss => ss.ShiftId) .Select(g => new { ShiftId = g.Key, Count = g.Count() }) .ToDictionaryAsync(x => x.ShiftId, x => x.Count); var items = shifts.Select(s => new ShiftListItemDto( s.Id, s.Title, s.StartTime, s.EndTime, s.Capacity, signupCounts.GetValueOrDefault(s.Id, 0) )).ToList(); return new ShiftListDto(items, total, page, pageSize); } public async Task GetShiftByIdAsync(Guid id) { var shift = await _context.Shifts.FindAsync(id); if (shift == null) return null; var signups = await _context.ShiftSignups .Where(ss => ss.ShiftId == id) .OrderBy(ss => ss.SignedUpAt) .ToListAsync(); var signupDtos = signups.Select(ss => new ShiftSignupDto( ss.Id, ss.MemberId, ss.SignedUpAt )).ToList(); return new ShiftDetailDto( shift.Id, shift.Title, shift.Description, shift.Location, shift.StartTime, shift.EndTime, shift.Capacity, signupDtos, shift.ClubId, shift.CreatedById, shift.CreatedAt, shift.UpdatedAt ); } public async Task<(ShiftDetailDto? shift, string? error)> CreateShiftAsync(CreateShiftRequest request, Guid createdById) { var tenantId = _tenantProvider.GetTenantId(); var shift = new Shift { Id = Guid.NewGuid(), TenantId = tenantId, Title = request.Title, Description = request.Description, Location = request.Location, StartTime = request.StartTime, EndTime = request.EndTime, Capacity = request.Capacity, ClubId = request.ClubId, CreatedById = createdById, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }; _context.Shifts.Add(shift); await _context.SaveChangesAsync(); var dto = new ShiftDetailDto( shift.Id, shift.Title, shift.Description, shift.Location, shift.StartTime, shift.EndTime, shift.Capacity, new List(), shift.ClubId, shift.CreatedById, shift.CreatedAt, shift.UpdatedAt ); return (dto, null); } public async Task<(ShiftDetailDto? shift, string? error, bool isConflict)> UpdateShiftAsync(Guid id, UpdateShiftRequest request) { var shift = await _context.Shifts.FindAsync(id); if (shift == null) return (null, "Shift not found", false); if (request.Title != null) shift.Title = request.Title; if (request.Description != null) shift.Description = request.Description; if (request.Location != null) shift.Location = request.Location; if (request.StartTime.HasValue) shift.StartTime = request.StartTime.Value; if (request.EndTime.HasValue) shift.EndTime = request.EndTime.Value; if (request.Capacity.HasValue) shift.Capacity = request.Capacity.Value; shift.UpdatedAt = DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { return (null, "Shift was modified by another user. Please refresh and try again.", true); } var signups = await _context.ShiftSignups .Where(ss => ss.ShiftId == id) .OrderBy(ss => ss.SignedUpAt) .ToListAsync(); var signupDtos = signups.Select(ss => new ShiftSignupDto( ss.Id, ss.MemberId, ss.SignedUpAt )).ToList(); var dto = new ShiftDetailDto( shift.Id, shift.Title, shift.Description, shift.Location, shift.StartTime, shift.EndTime, shift.Capacity, signupDtos, shift.ClubId, shift.CreatedById, shift.CreatedAt, shift.UpdatedAt ); return (dto, null, false); } public async Task DeleteShiftAsync(Guid id) { var shift = await _context.Shifts.FindAsync(id); if (shift == null) return false; _context.Shifts.Remove(shift); await _context.SaveChangesAsync(); return true; } public async Task<(bool success, string? error, bool isConflict)> SignUpForShiftAsync(Guid shiftId, string externalUserId) { var tenantId = _tenantProvider.GetTenantId(); var member = await _context.Members .FirstOrDefaultAsync(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId); if (member == null) return (false, "Member not found", false); var memberId = member.Id; var shift = await _context.Shifts.FindAsync(shiftId); if (shift == null) return (false, "Shift not found", false); if (shift.StartTime <= DateTimeOffset.UtcNow) { return (false, "Cannot sign up for past shifts", false); } var existingSignup = await _context.ShiftSignups .FirstOrDefaultAsync(ss => ss.ShiftId == shiftId && ss.MemberId == memberId); if (existingSignup != null) { return (false, "Already signed up for this shift", true); } for (int attempt = 0; attempt < 2; attempt++) { try { var currentSignups = await _context.ShiftSignups .Where(ss => ss.ShiftId == shiftId) .CountAsync(); if (currentSignups >= shift.Capacity) { return (false, "Shift is at full capacity", true); } var signup = new ShiftSignup { Id = Guid.NewGuid(), TenantId = tenantId, ShiftId = shiftId, MemberId = memberId, SignedUpAt = DateTimeOffset.UtcNow }; _context.ShiftSignups.Add(signup); await _context.SaveChangesAsync(); return (true, null, false); } catch (DbUpdateConcurrencyException) when (attempt == 0) { _context.Entry(shift).Reload(); } } return (false, "Shift capacity changed during sign-up", true); } public async Task<(bool success, string? error)> CancelSignupAsync(Guid shiftId, string externalUserId) { var tenantId = _tenantProvider.GetTenantId(); var member = await _context.Members .FirstOrDefaultAsync(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId); if (member == null) return (false, "Member not found"); var signup = await _context.ShiftSignups .FirstOrDefaultAsync(ss => ss.ShiftId == shiftId && ss.MemberId == member.Id); if (signup == null) { return (false, "Sign-up not found"); } _context.ShiftSignups.Remove(signup); await _context.SaveChangesAsync(); return (true, null); } }