195 lines
7.5 KiB
C#
195 lines
7.5 KiB
C#
using Fitness_core_BE.Exceptions;
|
|
using Fitness_core_BE.Infra.UoW;
|
|
using Fitness_core_BE.Interfaces;
|
|
using Fitness_core_BE.Interfaces.Services;
|
|
using Fitness_core_BE.Models;
|
|
using Fitness_core_BE.Models.IOs;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Fitness_core_BE.Services
|
|
{
|
|
public class AdminClientDirectoryService : IAdminClientDirectoryService
|
|
{
|
|
private readonly IAdminClientDirectoryRepository _repository;
|
|
private readonly IAgentRepo _agentRepo;
|
|
private readonly IAccessDeviceRepo _accessDeviceRepo;
|
|
private readonly IPairingCodeRepo _pairingCodeRepo;
|
|
private readonly IUnitOfWork _UoW;
|
|
private readonly IAuthService _authService;
|
|
|
|
// Three missed 30-second health heartbeats marks an agent/device offline.
|
|
// Legacy agents without explicit health reports retain the old grace window.
|
|
private static readonly TimeSpan HealthOnlineWindow = TimeSpan.FromSeconds(90);
|
|
private static readonly TimeSpan LegacyOnlineWindow = TimeSpan.FromMinutes(5);
|
|
|
|
public AdminClientDirectoryService(
|
|
IAdminClientDirectoryRepository repository,
|
|
IAgentRepo agentRepo,
|
|
IAccessDeviceRepo accessDeviceRepo,
|
|
IPairingCodeRepo pairingCodeRepo,
|
|
IUnitOfWork unitOfWork,
|
|
IAuthService authService)
|
|
{
|
|
_repository = repository;
|
|
_agentRepo = agentRepo;
|
|
_accessDeviceRepo = accessDeviceRepo;
|
|
_pairingCodeRepo = pairingCodeRepo;
|
|
_UoW = unitOfWork;
|
|
_authService = authService;
|
|
}
|
|
|
|
public async Task<PagedResult<ClientSummary>> ListAsync(ClientDirectoryQuery query)
|
|
{
|
|
var (items, total) = await _repository.ListAsync(query.Search, query.Page, query.PageSize);
|
|
|
|
var summaries = items.Select(c => new ClientSummary(
|
|
c.ClientId,
|
|
c.ClientName,
|
|
c.ClientBusinesses.FirstOrDefault()?.City,
|
|
c.PaymentTrack?.ToString(),
|
|
c.ClientBusinesses.Any(b => b.IsActive),
|
|
c.ClientBusinesses.OrderBy(b => b.createdAt).FirstOrDefault()?.createdAt ?? DateTime.MinValue
|
|
)).ToList();
|
|
|
|
return new PagedResult<ClientSummary>(summaries, total, query.Page, query.PageSize);
|
|
}
|
|
|
|
public async Task<object> GetByIdAsync(Guid id)
|
|
{
|
|
var client = await _repository.GetByIdAsync(id)
|
|
?? throw new NotFoundException("Client not found.");
|
|
|
|
return new
|
|
{
|
|
client.ClientId,
|
|
client.ClientName,
|
|
client.ClientPhone,
|
|
client.ClientEmail,
|
|
PaymentTrack = client.PaymentTrack?.ToString(),
|
|
Businesses = client.ClientBusinesses.Select(b => new
|
|
{
|
|
b.Bid,
|
|
b.BusinessName,
|
|
b.Address,
|
|
b.City,
|
|
b.IsActive
|
|
})
|
|
};
|
|
}
|
|
|
|
public async Task ResendWelcomeEmailAsync(Guid clientId, HttpContext httpContext)
|
|
{
|
|
var client = await _repository.GetByIdAsync(clientId)
|
|
?? throw new NotFoundException("Client not found.");
|
|
|
|
if (string.IsNullOrWhiteSpace(client.ClientEmail))
|
|
throw new BadRequestException("This client has no email on file to send the link to.");
|
|
|
|
var authResponse = await _authService.ForgotPassword(
|
|
new { identifier = client.ClientEmail, useResetLink = true, welcome = true },
|
|
httpContext);
|
|
|
|
if (authResponse.StatusCode != 200 || !authResponse.Success)
|
|
throw new BadRequestException(authResponse.Message ?? "Couldn't resend the welcome email.");
|
|
}
|
|
|
|
public async Task<PairingCodeResult> GeneratePairingCodeAsync(Guid clientId)
|
|
{
|
|
if (!await _repository.ClientExistsAsync(clientId))
|
|
throw new NotFoundException("Client not found.");
|
|
|
|
var pairingCode = new PairingCode
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Code = GenerateShortCode(),
|
|
FKClientId = clientId,
|
|
ExpiresAt = DateTime.UtcNow.AddMinutes(15),
|
|
IsUsed = false,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
await _UoW.BeginAsync();
|
|
try
|
|
{
|
|
await _pairingCodeRepo.AddAsync(pairingCode);
|
|
await _UoW.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
await _UoW.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
return new PairingCodeResult(pairingCode.Code, pairingCode.ExpiresAt);
|
|
}
|
|
|
|
public async Task<AdminClientDevicesResult> GetDevicesAsync(Guid clientId)
|
|
{
|
|
if (!await _repository.ClientExistsAsync(clientId))
|
|
throw new NotFoundException("Client not found.");
|
|
|
|
var agents = await _agentRepo.GetByClientIdAsync(clientId);
|
|
var devices = await _accessDeviceRepo.GetDevicesByClientAsync(clientId);
|
|
|
|
var now = DateTime.UtcNow;
|
|
var activeAgentIds = agents
|
|
.Where(a => a.IsActive)
|
|
.Select(a => a.AgentId)
|
|
.ToHashSet();
|
|
|
|
var agentDtos = agents
|
|
.Select(agent => new AdminAgentDto(
|
|
agent.AgentId,
|
|
agent.Name,
|
|
agent.IsActive,
|
|
agent.LastSeenAt,
|
|
agent.IsActive && agent.LastSeenAt.HasValue &&
|
|
now - agent.LastSeenAt.Value <= HealthOnlineWindow,
|
|
agent.CreatedAt))
|
|
.ToList();
|
|
|
|
var deviceDtos = devices
|
|
.Select(device =>
|
|
{
|
|
var hasHealthReport = device.LastHealthReportedAt.HasValue;
|
|
var presenceTimestamp = hasHealthReport
|
|
? device.LastHealthReportedAt
|
|
: device.LastSeenAt;
|
|
var window = hasHealthReport
|
|
? HealthOnlineWindow
|
|
: LegacyOnlineWindow;
|
|
var isOnline =
|
|
device.IsActive &&
|
|
device.FKAgentId.HasValue &&
|
|
activeAgentIds.Contains(device.FKAgentId.Value) &&
|
|
presenceTimestamp.HasValue &&
|
|
now - presenceTimestamp.Value <= window &&
|
|
(!hasHealthReport || device.IsReachable);
|
|
|
|
return new AdminDeviceDto(
|
|
device.DeviceId,
|
|
device.DeviceName,
|
|
device.IsActive,
|
|
device.LastSeenAt,
|
|
isOnline,
|
|
isOnline ? presenceTimestamp!.Value.Add(window) : null,
|
|
device.FKAgentId.HasValue,
|
|
device.CreatedAt);
|
|
})
|
|
.ToList();
|
|
|
|
return new AdminClientDevicesResult(agentDtos, deviceDtos);
|
|
}
|
|
|
|
private static string GenerateShortCode(int length = 8)
|
|
{
|
|
const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
var bytes = RandomNumberGenerator.GetBytes(length);
|
|
var chars = new char[length];
|
|
for (int i = 0; i < length; i++)
|
|
chars[i] = alphabet[bytes[i] % alphabet.Length];
|
|
return new string(chars);
|
|
}
|
|
}
|
|
}
|