using Fitness_core_BE.Interfaces; using Fitness_core_BE.Models; using Microsoft.EntityFrameworkCore; namespace Fitness_core_BE.Repos { public class FacilityRepository : IFacilityRepository { private readonly TenantDbContext _context; public FacilityRepository(TenantDbContext context) { _context = context; } public async Task> GetAllFacilities(Guid clientId, bool includeInactive = false) { var query = _context.Facilities.Where(f => f.FKClientId == clientId); if (!includeInactive) query = query.Where(f => f.IsActive); return await query.OrderBy(f => f.Name).ToListAsync(); } public Task GetFacilityById(Guid facilityId) => _context.Facilities.FirstOrDefaultAsync(f => f.FacilityId == facilityId); public async Task CreateFacility(Facility facility) { facility.FacilityId = Guid.NewGuid(); facility.CreatedAt = DateTime.UtcNow; facility.IsActive = true; _context.Facilities.Add(facility); return facility; } public async Task UpdateFacility(Facility facility) { var existing = await _context.Facilities.FindAsync(facility.FacilityId) ?? throw new KeyNotFoundException("Facility not found"); existing.Name = facility.Name; existing.SupportsClasses = facility.SupportsClasses; existing.SupportsBooking = facility.SupportsBooking; existing.Description = facility.Description; existing.DefaultCapacity = facility.DefaultCapacity; existing.NumberOfUnits = facility.NumberOfUnits; existing.PricePerHour = facility.PricePerHour; existing.IsActive = facility.IsActive; return existing; } public async Task SetFacilityActive(Guid facilityId, Guid clientId, bool isActive) { var facility = await _context.Facilities .FirstOrDefaultAsync(f => f.FacilityId == facilityId && f.FKClientId == clientId); if (facility == null) return null; facility.IsActive = isActive; return facility; } public async Task CreateFacilitySession(FacilitySession session) { session.FacilitySessionId = Guid.NewGuid(); session.CreatedAt = DateTime.UtcNow; session.IsCancelled = false; _context.FacilitySessions.Add(session); return session; } public Task GetFacilitySessionById(Guid facilitySessionId) => _context.FacilitySessions .Include(s => s.Facility) .FirstOrDefaultAsync(s => s.FacilitySessionId == facilitySessionId); public async Task> GetSessions(Guid clientId, Guid? facilityId, DateTime fromUtc, DateTime toUtc, bool includeCancelled = false) { var query = _context.FacilitySessions .Include(s => s.Facility) .Include(s => s.FacilityClass) .Where(s => s.FKClientId == clientId && s.StartTime >= fromUtc && s.StartTime <= toUtc); if (!includeCancelled) query = query.Where(s => !s.IsCancelled); if (facilityId.HasValue) query = query.Where(s => s.FKFacilityId == facilityId.Value); return await query.OrderBy(s => s.StartTime).ToListAsync(); } public Task GetActiveBookingCount(Guid facilitySessionId) => _context.Bookings.CountAsync(b => b.FKFacilitySessionId == facilitySessionId && b.Status != "Cancelled"); /// True interval overlap (StartTime < end && EndTime > start), not a /// "StartTime falls inside the window" filter like GetSessions -- a class that started /// before the window and is still running must still count as occupying it. public Task> GetOverlappingActiveSessionsAsync(Guid facilityId, DateTime startUtc, DateTime endUtc) => _context.FacilitySessions .Where(s => s.FKFacilityId == facilityId && !s.IsCancelled && s.StartTime < endUtc && s.EndTime > startUtc) .ToListAsync(); public async Task MarkFacilitySessionNotHeldAsync(Guid facilitySessionId, Guid clientId, string? reason) { var session = await _context.FacilitySessions .Include(s => s.Bookings) .FirstOrDefaultAsync(s => s.FacilitySessionId == facilitySessionId && s.FKClientId == clientId); if (session == null) return null; session.IsCancelled = true; session.NotHeldReason = reason; session.NotHeldAt = DateTime.UtcNow; return session; } // --- Device binding (absorbed from PlaceRepo) --- public Task GetDeviceByIdForClientAsync(Guid deviceId, Guid clientId) => _context.AccessDevices .FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.FKClientId == clientId); public async Task> GetFacilityNamesByIdsAsync(List facilityIds) { return await _context.Facilities .Where(f => facilityIds.Contains(f.FacilityId)) .ToDictionaryAsync(f => f.FacilityId, f => f.Name); } public async Task> GetDevicesByFacilityIdsAsync(List facilityIds) { var devices = await _context.AccessDevices .Where(d => d.FKFacilityId != null && facilityIds.Contains(d.FKFacilityId.Value)) .ToListAsync(); return devices.ToDictionary(d => d.FKFacilityId!.Value, d => d); } public Task IsDeviceAssignedToAnotherFacilityAsync(Guid deviceId, Guid facilityId) => _context.AccessDevices .AnyAsync(d => d.DeviceId == deviceId && d.FKFacilityId != null && d.FKFacilityId != facilityId); public async Task AssignDeviceAsync(Guid facilityId, Guid deviceId) { // A device can only sit in one facility at a time -- clear anywhere else it's // currently assigned before pointing it at this facility. var previouslyAssigned = await _context.AccessDevices .Where(d => d.FKFacilityId == facilityId && d.DeviceId != deviceId) .ToListAsync(); foreach (var device in previouslyAssigned) device.FKFacilityId = null; var target = await _context.AccessDevices.FindAsync(deviceId) ?? throw new KeyNotFoundException("Device not found"); target.FKFacilityId = facilityId; } public async Task UnassignDeviceAsync(Guid facilityId) { var devices = await _context.AccessDevices .Where(d => d.FKFacilityId == facilityId) .ToListAsync(); foreach (var device in devices) device.FKFacilityId = null; } // --- CustomerPlan access-scoping (absorbed from PlaceRepo) --- public async Task SetPlanFacilitiesAsync(Guid customerPlanId, List facilityIds) { var existing = await _context.CustomerPlanFacilities .Where(pf => pf.FKCustomerPlanId == customerPlanId) .ToListAsync(); _context.CustomerPlanFacilities.RemoveRange(existing); foreach (var facilityId in facilityIds.Distinct()) { _context.CustomerPlanFacilities.Add(new CustomerPlanFacility { CustomerPlanFacilityId = Guid.NewGuid(), FKCustomerPlanId = customerPlanId, FKFacilityId = facilityId }); } } public Task> GetFacilityIdsForPlanAsync(Guid customerPlanId) => _context.CustomerPlanFacilities .Where(pf => pf.FKCustomerPlanId == customerPlanId) .Select(pf => pf.FKFacilityId) .ToListAsync(); public async Task>> GetFacilityIdsByPlanIdsAsync(List customerPlanIds) { var rows = await _context.CustomerPlanFacilities .Where(pf => customerPlanIds.Contains(pf.FKCustomerPlanId)) .ToListAsync(); return rows .GroupBy(pf => pf.FKCustomerPlanId) .ToDictionary(g => g.Key, g => g.Select(pf => pf.FKFacilityId).ToList()); } // --- WalkIn booking support (new) --- public Task GetOverlappingWalkInBookingCountAsync(Guid facilityId, DateTime startTime, DateTime endTime, Guid? excludeBookingId = null) { var query = _context.Bookings.Where(b => b.FKFacilityId == facilityId && b.FKFacilitySessionId == null && // WalkIn bookings only b.Status != "Cancelled" && b.Status != "NoShow" && b.Status != "Completed" && b.StartTime < endTime && b.EndTime > startTime); if (excludeBookingId.HasValue) query = query.Where(b => b.BookingId != excludeBookingId.Value); return query.CountAsync(); } public async Task LockFacilityForUpdateAsync(Guid facilityId) { // FromSqlInterpolated (not ExecuteSqlInterpolatedAsync, which is for non-query DML) // is the correct EF Core idiom for a locking SELECT -- it runs through the same ADO // connection/transaction as the caller's already-open _uow, so the row lock is held // until that transaction commits/rolls back. AsNoTracking avoids clashing with any // already-tracked Facility instance in this same context/request. await _context.Facilities .FromSqlInterpolated($"SELECT * FROM Facilities WHERE FacilityId = {facilityId} FOR UPDATE") .AsNoTracking() .ToListAsync(); } } }