using Fitness_core_BE.DTOs.Attendance; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Models; using Microsoft.EntityFrameworkCore; namespace Fitness_core_BE.Repos { public class AttendanceRepository : IAttendanceRepository { private readonly TenantDbContext _dbContext; public AttendanceRepository(TenantDbContext dbContext) { _dbContext = dbContext; } public async Task GetLatestAttendanceAsync(Guid clientId) { return await _dbContext.Attendances .AsNoTracking() .Where(a => a.FKClientId == clientId) .OrderByDescending(a => a.EventTime) .ThenByDescending(a => a.AttendanceId) .FirstOrDefaultAsync(); } public async Task> GetAttendanceKeysAsync(Guid clientId, DateTime fromTime) { var keys = await _dbContext.Attendances .AsNoTracking() .Where(a => a.FKClientId == clientId && a.EventTime >= fromTime) .Select(a => new { a.FKDeviceId, a.FKUserId, a.FKCustomerId, a.DeviceUserId, a.DeviceEmployeeNo, a.EventTime, a.EventType }) .ToListAsync(); var set = new HashSet(); foreach (var k in keys) { var userId = k.FKUserId ?? k.FKCustomerId ?? Guid.Empty; var employeeNo = string.IsNullOrWhiteSpace(k.DeviceEmployeeNo) ? k.DeviceUserId.ToString() : k.DeviceEmployeeNo; set.Add(BuildKey( k.FKDeviceId ?? Guid.Empty, userId, employeeNo, k.EventTime, k.EventType)); } return set; } public async Task> GetSourceEventKeysAsync( IReadOnlyCollection candidateKeys, CancellationToken ct = default) { if (candidateKeys.Count == 0) return new HashSet(); var keys = await _dbContext.Attendances .AsNoTracking() .Where(attendance => attendance.SourceEventKey != null && candidateKeys.Contains(attendance.SourceEventKey)) .Select(attendance => attendance.SourceEventKey!) .ToListAsync(ct); return keys.ToHashSet(); } public async Task AddAttendancesAsync(IEnumerable attendances) { await _dbContext.Attendances.AddRangeAsync(attendances); } public async Task> GetLatestPerPersonAsync(Guid clientId) { return await LatestPerPersonQuery(clientId) .AsNoTracking() .Include(a => a.TenantCustomerLink) .Include(a => a.AccessProfile) .OrderByDescending(a => a.EventTime) .ThenByDescending(a => a.CreatedAt) .ToListAsync(); } public async Task> GetAttendanceCountsAsync(Guid clientId, DateTime fromDate, DateTime toDate) { var records = await _dbContext.Attendances .AsNoTracking() .Include(a => a.TenantCustomerLink) .Where(a => a.FKClientId == clientId && a.FKCustomerId != null && a.EventTime >= fromDate && a.EventTime <= toDate) .ToListAsync(); return records .GroupBy(a => a.FKCustomerId) .Select(g => new AttendanceCountDTO { CustomerId = g.Key!.Value, CustomerName = g.First().TenantCustomerLink?.DisplayName ?? "Unknown", AttendanceCount = g.Count() }) .ToList(); } public async Task> GetTodayAttendanceAsync(Guid clientId) { var todayStart = DateTime.UtcNow.Date; var todayEnd = todayStart.AddDays(1); var records = await LatestPerPersonQuery(clientId, todayStart, todayEnd) .AsNoTracking() .Include(a => a.TenantCustomerLink) .Include(a => a.AccessProfile) .OrderByDescending(a => a.EventTime) .ThenByDescending(a => a.CreatedAt) .ToListAsync(); return records.Select(a => new TodayAttendanceDTO { CustomerName = a.TenantCustomerLink?.DisplayName ?? a.AccessProfile?.DisplayName ?? "Unknown", EventTime = a.EventTime, EventType = a.EventType }).ToList(); } private IQueryable LatestPerPersonQuery( Guid clientId, DateTime? from = null, DateTime? to = null) { var scoped = _dbContext.Attendances .Where(a => a.FKClientId == clientId); if (from.HasValue) scoped = scoped.Where(a => a.EventTime >= from.Value); if (to.HasValue) scoped = scoped.Where(a => a.EventTime < to.Value); // Raw device events remain available for reports and counts. This // read query exposes only the newest state for each linked person. // User/customer matching also bridges legacy rows created before // AccessProfileId was populated. return scoped.Where(record => !scoped.Any(candidate => ( (record.FKUserId.HasValue && candidate.FKUserId == record.FKUserId) || (record.FKCustomerId.HasValue && candidate.FKCustomerId == record.FKCustomerId) || (record.FKAccessProfileId.HasValue && candidate.FKAccessProfileId == record.FKAccessProfileId) || (!record.FKUserId.HasValue && !record.FKCustomerId.HasValue && !record.FKAccessProfileId.HasValue && !candidate.FKUserId.HasValue && !candidate.FKCustomerId.HasValue && !candidate.FKAccessProfileId.HasValue && candidate.FKDeviceId == record.FKDeviceId && candidate.DeviceEmployeeNo == record.DeviceEmployeeNo) ) && ( candidate.EventTime > record.EventTime || (candidate.EventTime == record.EventTime && candidate.CreatedAt > record.CreatedAt) ))); } private static string BuildKey( Guid deviceId, Guid userId, string deviceEmployeeNo, DateTime eventTime, string eventType) { return $"{deviceId}|{userId}|{deviceEmployeeNo}|{eventTime:O}|{eventType}"; } } }