420 lines
18 KiB
C#
420 lines
18 KiB
C#
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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<StaffDto> 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<List<StaffDto>> MapStaffListAsync(IEnumerable<Models.Staff> staff)
|
|
{
|
|
var result = new List<StaffDto>();
|
|
foreach (var member in staff)
|
|
result.Add(await MapStaffAsync(member));
|
|
return result;
|
|
}
|
|
}
|
|
}
|