192 lines
7.2 KiB
C#
192 lines
7.2 KiB
C#
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<Attendance?> 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<HashSet<string>> 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<string>();
|
|
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<HashSet<string>> GetSourceEventKeysAsync(
|
|
IReadOnlyCollection<string> candidateKeys,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (candidateKeys.Count == 0)
|
|
return new HashSet<string>();
|
|
|
|
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<Attendance> attendances)
|
|
{
|
|
await _dbContext.Attendances.AddRangeAsync(attendances);
|
|
}
|
|
|
|
public async Task<List<Attendance>> 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<List<AttendanceCountDTO>> 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<List<TodayAttendanceDTO>> 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<Attendance> 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}";
|
|
}
|
|
}
|
|
}
|