Files
WorkClub Automation 672dec5f21
Some checks failed
CI Pipeline / Backend Build & Test (pull_request) Successful in 48s
CI Pipeline / Frontend Lint, Test & Build (pull_request) Failing after 28s
CI Pipeline / Infrastructure Validation (pull_request) Successful in 4s
Fix task and shift self-assignment features
2026-03-09 15:47:57 +01:00

331 lines
10 KiB
C#

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<ShiftListDto> GetShiftsAsync(DateTimeOffset? from, DateTimeOffset? to, int page, int pageSize, string? currentExternalUserId = null)
{
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 tenantId = _tenantProvider.GetTenantId();
var memberId = currentExternalUserId != null
? await _context.Members
.Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId)
.Select(m => (Guid?)m.Id)
.FirstOrDefaultAsync()
: null;
var userSignups = memberId.HasValue
? await _context.ShiftSignups
.Where(ss => shiftIds.Contains(ss.ShiftId) && ss.MemberId == memberId.Value)
.Select(ss => ss.ShiftId)
.ToListAsync()
: new List<Guid>();
var userSignedUpShiftIds = userSignups.ToHashSet();
var items = shifts.Select(s => new ShiftListItemDto(
s.Id,
s.Title,
s.StartTime,
s.EndTime,
s.Capacity,
signupCounts.GetValueOrDefault(s.Id, 0),
userSignedUpShiftIds.Contains(s.Id)
)).ToList();
return new ShiftListDto(items, total, page, pageSize);
}
public async Task<ShiftDetailDto?> GetShiftByIdAsync(Guid id, string? currentExternalUserId = null)
{
var shift = await _context.Shifts.FindAsync(id);
if (shift == null)
return null;
var signups = await (from ss in _context.ShiftSignups
where ss.ShiftId == id
join m in _context.Members on ss.MemberId equals m.Id
orderby ss.SignedUpAt
select new { ss.Id, ss.MemberId, m.ExternalUserId, ss.SignedUpAt })
.ToListAsync();
var signupDtos = signups.Select(ss => new ShiftSignupDto(
ss.Id,
ss.MemberId,
ss.ExternalUserId,
ss.SignedUpAt
)).ToList();
var isSignedUp = currentExternalUserId != null && signupDtos.Any(s => s.ExternalUserId == currentExternalUserId);
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,
isSignedUp
);
}
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<ShiftSignupDto>(),
shift.ClubId,
shift.CreatedById,
shift.CreatedAt,
shift.UpdatedAt,
false
);
return (dto, null);
}
public async Task<(ShiftDetailDto? shift, string? error, bool isConflict)> UpdateShiftAsync(Guid id, UpdateShiftRequest request, string? currentExternalUserId = null)
{
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 (from ss in _context.ShiftSignups
where ss.ShiftId == id
join m in _context.Members on ss.MemberId equals m.Id
orderby ss.SignedUpAt
select new { ss.Id, ss.MemberId, m.ExternalUserId, ss.SignedUpAt })
.ToListAsync();
var signupDtos = signups.Select(ss => new ShiftSignupDto(
ss.Id,
ss.MemberId,
ss.ExternalUserId,
ss.SignedUpAt
)).ToList();
var isSignedUp = currentExternalUserId != null && signupDtos.Any(s => s.ExternalUserId == currentExternalUserId);
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,
isSignedUp
);
return (dto, null, false);
}
public async Task<bool> 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);
}
}