first commit
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user