using Fitness_core_BE.DTOs.Auth; using Fitness_core_BE.Exceptions; using Fitness_core_BE.Helpers; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.DTOs.Staff; using System.Text.Json; namespace Fitness_core_BE.Services.Staff { public class StaffService : IStaffService { private readonly IStaffRepository _staffRepository; private readonly IUnitOfWork _uow; private readonly UserRoleHelper _userRoleHelper; private readonly IAuthService _authService; private readonly IAccessDeviceService _accessDeviceService; private readonly IGymRoleRepository _gymRoleRepository; public StaffService(IStaffRepository staffRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper, IAuthService authService, IAccessDeviceService accessDeviceService, IGymRoleRepository gymRoleRepository) { _staffRepository = staffRepository; _uow = uow; _userRoleHelper = userRoleHelper; _authService = authService; _accessDeviceService = accessDeviceService; _gymRoleRepository = gymRoleRepository; } public async Task GetAllStaff(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); bool includeInactive = data != null && data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean(); var staffList = await _staffRepository.GetAllStaff(includeInactive); return await MapStaffListAsync(staffList); } public async Task GetStaffById(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("staffId")) { throw new KeyNotFoundException("staffId is required"); } var staffId = Guid.Parse(data["staffId"].GetString()!); var staff = await _staffRepository.GetStaffById(staffId); if (staff == null) { throw new KeyNotFoundException("Staff not found"); } var callerClientId = await _userRoleHelper.GetClientId( DataExtractor.GetUserIdFromContext(httpContext)); if (staff.FKClientId != callerClientId) throw new ForbiddenException("This staff member does not belong to your gym."); return await MapStaffAsync(staff); } public async Task GetStaffByClientId(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("clientId")) { throw new KeyNotFoundException("clientId is required"); } var clientId = Guid.Parse(data["clientId"].GetString()!); var callerClientId = await _userRoleHelper.GetClientId( DataExtractor.GetUserIdFromContext(httpContext)); if (clientId != callerClientId) throw new ForbiddenException("You cannot view another gym's staff."); bool includeInactive = data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean(); var staffList = await _staffRepository.GetStaffByClientId(clientId, includeInactive); return await MapStaffListAsync(staffList); } public async Task CreateStaff(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid userId = DataExtractor.GetUserIdFromContext(httpContext); Guid clientId = await _userRoleHelper.GetClientId(userId); if (!data!.ContainsKey("userName")) { throw new KeyNotFoundException("userName is required"); } if (!data.ContainsKey("email")) { throw new KeyNotFoundException("email is required"); } if (!data.ContainsKey("phoneNumber")) { throw new KeyNotFoundException("phoneNumber is required"); } var user = new User { UserName = data["userName"].GetString()!, Email = data["email"].GetString()!, PhoneNumber = data["phoneNumber"].GetString()! }; // Optional -- must belong to this same client, same check AssignStaffRole // already does for the post-creation assignment path. Left unset, the new // staff member has no dashboard access until a role is assigned later. Guid? gymRoleId = null; if (data.ContainsKey("gymRoleId") && data["gymRoleId"].ValueKind != JsonValueKind.Null && Guid.TryParse(data["gymRoleId"].GetString(), out var parsedRoleId)) { var role = await _gymRoleRepository.GetRoleById(parsedRoleId); if (role == null || role.FKClientId != clientId) throw new BadRequestException("That role doesn't belong to your gym."); gymRoleId = parsedRoleId; } var staff = new Models.Staff { FKUserId = userId, FKClientId = clientId, StaffPosition = data.ContainsKey("staffPosition") ? data["staffPosition"].GetString() : null, Department = data.ContainsKey("department") ? data["department"].GetString() : null, FKGymRoleId = gymRoleId, }; var result = await _staffRepository.CreateStaff(staff, user); await _uow.SaveChangesAsync(); await _accessDeviceService.EnsureAccessProfileAsync( clientId, user.UserId, "Staff", user.UserName ?? string.Empty); AuthResponse authResponse; try { var authPayload = DeserializePayload.Deserialize(payload); Guid roleId = Guid.Parse("08de6a11-9e9f-4401-8a10-6854567b41ed"); Guid userTypeId = Guid.Parse("00000000-0000-0000-0000-000000000003"); authPayload["userId"] = JsonSerializer.SerializeToElement(user.UserId); authPayload["roleId"] = JsonSerializer.SerializeToElement(roleId); authPayload["userTypeId"] = JsonSerializer.SerializeToElement(userTypeId); // The client portal's request uses "phoneNumber", but Core-Hex-Auth's // RegisterUser only reads "mobileNumber" — without this the auth User row // was silently created with a null phone number. authPayload["mobileNumber"] = JsonSerializer.SerializeToElement(user.PhoneNumber); authResponse = await _authService.RegisterUser(authPayload, httpContext); } catch (Exception ex) { throw new InvalidOperationException($"Auth registration failed: {ex.Message}"); } if (authResponse.StatusCode != 200 || !authResponse.Success) { throw new InvalidOperationException($"Auth registration failed: {authResponse.Message}"); } await _uow.CommitAsync(); // Newly created staff have no password of their own yet — email them a // "set your password" link (reuses the auth service's reset-link flow). // Non-critical: a failure here must not roll back the created staff member. try { await _authService.ForgotPassword( new { identifier = user.Email, useResetLink = true, welcome = true }, httpContext); } catch (Exception ex) { Console.WriteLine($"Staff set-password email failed (non-critical): {ex.Message}"); } return new { staff = await MapStaffAsync(result, user), auth = authResponse }; } catch { await _uow.RollbackAsync(); throw; } } public async Task UpdateStaff(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("staffId")) { throw new KeyNotFoundException("staffId is required"); } var staffId = Guid.Parse(data["staffId"].GetString()!); // Get existing staff first var existingStaff = await _staffRepository.GetStaffById(staffId); if (existingStaff == null) { throw new KeyNotFoundException("Staff not found"); } var callerClientId = await _userRoleHelper.GetClientId( DataExtractor.GetUserIdFromContext(httpContext)); if (existingStaff.FKClientId != callerClientId) throw new ForbiddenException("This staff member does not belong to your gym."); // Create update object with only provided fields var updateStaff = new Models.Staff { StaffId = staffId, StaffPosition = data.ContainsKey("staffPosition") ? data["staffPosition"].GetString() : null, Department = data.ContainsKey("department") ? data["department"].GetString() : null, IsActive = data.ContainsKey("isActive") ? data["isActive"].GetBoolean() : existingStaff.IsActive, User = new User { UserName = data.ContainsKey("userName") ? data["userName"].GetString() : null, Email = data.ContainsKey("email") ? data["email"].GetString() : null, PhoneNumber = data.ContainsKey("phoneNumber") ? data["phoneNumber"].GetString() : null } }; var result = await _staffRepository.UpdateStaff(updateStaff); await _uow.CommitAsync(); return await MapStaffAsync(result); } catch { await _uow.RollbackAsync(); throw; } } public async Task DeleteStaff(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("staffId")) { throw new KeyNotFoundException("staffId is required"); } var staffId = Guid.Parse(data["staffId"].GetString()!); await EnsureStaffBelongsToCallerAsync(staffId, httpContext); // Check if hard delete is requested (default is soft delete) bool softDelete = !data.ContainsKey("hardDelete") || !data["hardDelete"].GetBoolean(); var result = await _staffRepository.DeleteStaff(staffId, softDelete); if (!result) { throw new KeyNotFoundException("Staff not found"); } await _uow.CommitAsync(); return new { success = true, message = softDelete ? "Staff deactivated successfully" : "Staff deleted successfully", softDelete = softDelete }; } catch { await _uow.RollbackAsync(); throw; } } public async Task GetPayRate(Guid staffId, HttpContext httpContext) { await EnsureStaffBelongsToCallerAsync(staffId, httpContext); var rate = await _staffRepository.GetActivePayRateAsync(staffId); return rate == null ? null! : new StaffPayRateDto { Id = rate.Id, RateType = rate.RateType, Amount = rate.Amount, EffectiveFrom = rate.EffectiveFrom }; } public async Task UpsertPayRate(Guid staffId, string rateType, decimal amount, HttpContext httpContext) { await EnsureStaffBelongsToCallerAsync(staffId, httpContext); if (rateType != "Monthly" && rateType != "Hourly") throw new BadRequestException("rateType must be 'Monthly' or 'Hourly'."); if (amount <= 0) throw new BadRequestException("amount must be greater than zero."); var currentRate = await _staffRepository.GetActivePayRateAsync(staffId); if (currentRate != null) currentRate.IsActive = false; var rate = await _staffRepository.AddPayRateAsync(new Models.StaffPayRate { Id = Guid.NewGuid(), FKStaffId = staffId, RateType = rateType, Amount = amount, EffectiveFrom = DateTime.UtcNow, IsActive = true }); await _uow.SaveChangesAsync(); return new StaffPayRateDto { Id = rate.Id, RateType = rate.RateType, Amount = rate.Amount, EffectiveFrom = rate.EffectiveFrom }; } public async Task GetPayments(Guid staffId, HttpContext httpContext) { await EnsureStaffBelongsToCallerAsync(staffId, httpContext); var payments = await _staffRepository.GetPaymentsAsync(staffId); return payments.Select(p => new StaffPaymentDto { Id = p.Id, Amount = p.Amount, Note = p.Note, PaidAt = p.PaidAt }).ToList(); } public async Task RecordPayment(Guid staffId, decimal amount, string? note, HttpContext httpContext) { await EnsureStaffBelongsToCallerAsync(staffId, httpContext); if (amount <= 0) throw new BadRequestException("amount must be greater than zero."); var payment = new Models.StaffPayment { Id = Guid.NewGuid(), FKStaffId = staffId, Amount = amount, Note = note, PaidAt = DateTime.UtcNow, FKPaidByUserId = DataExtractor.GetUserIdFromContext(httpContext) }; await _staffRepository.AddPaymentAsync(payment); await _uow.SaveChangesAsync(); return new StaffPaymentDto { Id = payment.Id, Amount = payment.Amount, Note = payment.Note, PaidAt = payment.PaidAt }; } private async Task EnsureStaffBelongsToCallerAsync(Guid staffId, HttpContext httpContext) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); var staff = await _staffRepository.GetStaffById(staffId) ?? throw new KeyNotFoundException("Staff not found"); if (staff.FKClientId != clientId) throw new ForbiddenException("This staff member does not belong to your gym."); } private async Task MapStaffAsync(Models.Staff staff, User? userOverride = null) { var user = userOverride ?? staff.User; var access = await _accessDeviceService.GetAccessStatusAsync( staff.FKClientId, staff.FKUserId); return new StaffDto { StaffId = staff.StaffId, UserId = staff.FKUserId, UserName = user?.UserName, Email = user?.Email, PhoneNumber = user?.PhoneNumber, ClientId = staff.FKClientId, ClientName = staff.Client?.ClientName, GymRoleId = staff.FKGymRoleId, GymRoleName = staff.GymRole?.Name, StaffPosition = staff.StaffPosition, Department = staff.Department, HireDate = staff.HireDate, IsActive = staff.IsActive, CreatedAt = staff.CreatedAt, AccessProfileId = access?.AccessProfileId, AccessNumber = access?.AccessNumber, AccessProvisionStatus = access?.ProvisionStatus ?? "NotConfigured", ActualAccessState = access?.ActualAccessState ?? "Unknown", DesiredAccessState = access?.DesiredAccessState ?? "Enabled", FingerprintCount = access?.FingerprintCount ?? 0, AccessLastError = access?.LastError }; } private async Task> MapStaffListAsync(IEnumerable staff) { var result = new List(); foreach (var member in staff) result.Add(await MapStaffAsync(member)); return result; } } }