151 lines
5.2 KiB
C#
151 lines
5.2 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.DTOs.MealAttendance;
|
|
using ROLAC.API.Entities;
|
|
|
|
namespace ROLAC.API.Services;
|
|
|
|
public class MealAttendanceService : IMealAttendanceService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
|
|
public MealAttendanceService(AppDbContext db) => _db = db;
|
|
|
|
// Server local time is assumed to match the church's local day.
|
|
public DateOnly ServiceDay
|
|
{
|
|
get
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Now);
|
|
return today.AddDays(-(int)today.DayOfWeek);
|
|
}
|
|
}
|
|
|
|
public async Task<AttendanceCountsDto> GetOrCreateAsync(DateOnly date)
|
|
{
|
|
await EnsureRowAsync(date);
|
|
return await ReadAsync(date);
|
|
}
|
|
|
|
public async Task<AttendanceCountsDto> IncrementAsync(DateOnly date, string category, int delta)
|
|
{
|
|
await EnsureRowAsync(date);
|
|
|
|
var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date);
|
|
|
|
// Atomic, race-free server-side increment; never let a column go below zero.
|
|
switch (category.Trim().ToLowerInvariant())
|
|
{
|
|
case "adult":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(
|
|
a => a.AdultCount, a => a.AdultCount + delta < 0 ? 0 : a.AdultCount + delta));
|
|
break;
|
|
case "youth":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(
|
|
a => a.YouthCount, a => a.YouthCount + delta < 0 ? 0 : a.YouthCount + delta));
|
|
break;
|
|
case "kid":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(
|
|
a => a.KidCount, a => a.KidCount + delta < 0 ? 0 : a.KidCount + delta));
|
|
break;
|
|
default:
|
|
throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category));
|
|
}
|
|
|
|
return await ReadAsync(date);
|
|
}
|
|
|
|
public async Task<AttendanceCountsDto> SetAsync(DateOnly date, string category, int value)
|
|
{
|
|
await EnsureRowAsync(date);
|
|
|
|
// Counts can never be negative; clamp before writing.
|
|
var clamped = value < 0 ? 0 : value;
|
|
|
|
var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date);
|
|
|
|
switch (category.Trim().ToLowerInvariant())
|
|
{
|
|
case "adult":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.AdultCount, clamped));
|
|
break;
|
|
case "youth":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.YouthCount, clamped));
|
|
break;
|
|
case "kid":
|
|
await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.KidCount, clamped));
|
|
break;
|
|
default:
|
|
throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category));
|
|
}
|
|
|
|
return await ReadAsync(date);
|
|
}
|
|
|
|
public async Task<AttendanceCountsDto> SetCountsAsync(DateOnly date, int adult, int youth, int kid)
|
|
{
|
|
var row = await _db.MealAttendances.FirstOrDefaultAsync(a => a.AttendanceDate == date);
|
|
if (row is null)
|
|
{
|
|
row = new MealAttendance { AttendanceDate = date };
|
|
_db.MealAttendances.Add(row);
|
|
}
|
|
|
|
// Counts can never be negative; clamp before writing.
|
|
row.AdultCount = adult < 0 ? 0 : adult;
|
|
row.YouthCount = youth < 0 ? 0 : youth;
|
|
row.KidCount = kid < 0 ? 0 : kid;
|
|
|
|
await _db.SaveChangesAsync();
|
|
return ToDto(row);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<AttendanceCountsDto>> GetRangeAsync(DateOnly from, DateOnly to)
|
|
{
|
|
var rows = await _db.MealAttendances.AsNoTracking()
|
|
.Where(a => a.AttendanceDate >= from && a.AttendanceDate <= to)
|
|
.OrderBy(a => a.AttendanceDate)
|
|
.ToListAsync();
|
|
|
|
return rows.Select(ToDto).ToList();
|
|
}
|
|
|
|
// ── helpers ──────────────────────────────────────────────────────────────
|
|
|
|
// Insert a zeroed row if today's row doesn't exist yet. A concurrent insert
|
|
// that wins the unique index throws DbUpdateException — harmless, the row exists.
|
|
private async Task EnsureRowAsync(DateOnly date)
|
|
{
|
|
if (await _db.MealAttendances.AsNoTracking().AnyAsync(a => a.AttendanceDate == date))
|
|
return;
|
|
|
|
var row = new MealAttendance { AttendanceDate = date };
|
|
_db.MealAttendances.Add(row);
|
|
try
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
_db.Entry(row).State = EntityState.Detached;
|
|
}
|
|
}
|
|
|
|
private async Task<AttendanceCountsDto> ReadAsync(DateOnly date)
|
|
{
|
|
var row = await _db.MealAttendances.AsNoTracking()
|
|
.FirstOrDefaultAsync(a => a.AttendanceDate == date);
|
|
return row is null
|
|
? new AttendanceCountsDto { Date = date.ToString("yyyy-MM-dd") }
|
|
: ToDto(row);
|
|
}
|
|
|
|
private static AttendanceCountsDto ToDto(MealAttendance row) => new()
|
|
{
|
|
Date = row.AttendanceDate.ToString("yyyy-MM-dd"),
|
|
Adult = row.AdultCount,
|
|
Youth = row.YouthCount,
|
|
Kid = row.KidCount,
|
|
};
|
|
}
|