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