shoc-backend/SeaHaven.DataServices/Implementation/LocationDataService.cs
Alexandre Brandizzi 6bfb56f349 fix(data): stamp audit times in UTC and keep a caller-set work order creation time
WorkOrderDataService.AddAsync overwrote the UTC CreatedDate set by WorkOrderService with local server time, offsetting the SLA response clock on any host not running in UTC. Data services now stamp CreatedDate, LastModificationTime and DeletionTime with DateTime.UtcNow, and a work order keeps the creation time its caller set.
2026-09-25 12:04:40 -03:00

303 lines
11 KiB
C#

using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class LocationDataService : ILocationDataService
{
private readonly ApplicationDbContext _context;
public LocationDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Locations?> GetByIdAsync(int id)
{
return await _context.Locations.FindAsync(id);
}
public async Task<Locations?> GetByIdWithDetailsAsync(int id)
{
return await _context.Locations
.FirstOrDefaultAsync(l => l.Id == id);
}
public async Task<IEnumerable<Locations>> GetAllAsync()
{
return await _context.Locations.ToListAsync();
}
public async Task<IEnumerable<Locations>> GetByAccountIdAsync(int accountId)
{
return await _context.Locations
.AsNoTracking()
.Where(l => l.AccountId == accountId)
.ToListAsync();
}
public async Task<(bool Exists, int? AccountId)> GetAccountScopeAsync(
int locationId,
CancellationToken cancellationToken = default)
{
var row = await _context.Locations
.AsNoTracking()
.Where(l => l.Id == locationId)
.Select(l => new { l.AccountId })
.FirstOrDefaultAsync(cancellationToken);
return row == null ? (false, null) : (true, row.AccountId);
}
public async Task<(IEnumerable<Locations> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
var query = _context.Locations.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(l =>
l.Name!.Contains(search) ||
l.City!.Contains(search));
}
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(l => l.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<IReadOnlyList<SiteOptionRow>> GetSiteOptionsAsync(
string? search = null,
CancellationToken cancellationToken = default)
{
var query = _context.Locations
.AsNoTracking()
.Where(l => l.AccountId != null);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(l =>
(l.Name ?? "").ToLower().Contains(s) ||
(l.City ?? "").ToLower().Contains(s));
}
return await query
.OrderBy(l => l.Name)
.Select(l => new SiteOptionRow(l.Id, l.Name, l.City, l.State))
.ToListAsync(cancellationToken);
}
public async Task<Locations> AddAsync(Locations location)
{
location.CreatedDate = DateTime.UtcNow;
await _context.Locations.AddAsync(location);
await _context.SaveChangesAsync();
return location;
}
public async Task UpdateAsync(Locations location)
{
location.LastModificationTime = DateTime.UtcNow;
_context.Locations.Update(location);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Locations.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Locations.AnyAsync(l => l.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Locations.CountAsync();
}
public async Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(
int page,
int pageSize,
string? search,
IReadOnlyCollection<string>? states,
CancellationToken cancellationToken,
string? sortBy = null,
string? sortDirection = null)
{
IQueryable<Locations> query = _context.Locations
.AsNoTracking()
.Include(l => l.Account);
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(l =>
l.Name!.Contains(search) ||
l.Address1!.Contains(search) ||
l.City!.Contains(search) ||
l.Account != null && l.Account.Name!.Contains(search));
}
if (states is { Count: > 0 })
{
query = query.Where(l => l.State != null && states.Contains(l.State.Trim().ToUpper()));
}
query = ApplyListSort(query, sortBy, sortDirection);
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (items, totalCount);
}
private static IQueryable<Locations> ApplyListSort(IQueryable<Locations> query, string? sortBy, string? sortDirection)
{
var descending = string.Equals(sortDirection?.Trim(), "desc", StringComparison.OrdinalIgnoreCase);
IOrderedQueryable<Locations> ordered = (sortBy?.Trim().ToLowerInvariant()) switch
{
"code" => descending ? query.OrderByDescending(l => l.Name) : query.OrderBy(l => l.Name),
"client" => descending ? query.OrderByDescending(l => l.Account!.Name) : query.OrderBy(l => l.Account!.Name),
"address" => descending ? query.OrderByDescending(l => l.Address1) : query.OrderBy(l => l.Address1),
"city" => descending ? query.OrderByDescending(l => l.City) : query.OrderBy(l => l.City),
"state" => descending ? query.OrderByDescending(l => l.State) : query.OrderBy(l => l.State),
"poc" => descending
? query.OrderByDescending(FirstActiveContactName)
: query.OrderBy(FirstActiveContactName),
_ => query.OrderBy(l => l.Name)
};
return ordered.ThenBy(l => l.Id);
}
private static readonly System.Linq.Expressions.Expression<Func<Locations, string?>> FirstActiveContactName =
l => l.Contacts
.Where(c => c.IsDeleted != true)
.OrderBy(c => c.SiteContactOrder)
.ThenBy(c => c.Id)
.Select(c => c.FirstName)
.FirstOrDefault();
public async Task<Locations?> GetDetailByIdAsync(int id, CancellationToken cancellationToken)
{
return await _context.Locations
.AsNoTracking()
.Include(l => l.Contacts.Where(c => c.IsDeleted != true))
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
}
public async Task<Locations?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken)
{
return await _context.Locations
.Include(l => l.Contacts)
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
}
public async Task<IReadOnlyList<Contacts>> GetSiteContactsByLocationIdsAsync(
IReadOnlyCollection<int> locationIds,
CancellationToken cancellationToken)
{
if (locationIds.Count == 0)
return Array.Empty<Contacts>();
return await _context.Contacts
.AsNoTracking()
.Where(c => c.LocationId != null && locationIds.Contains(c.LocationId.Value) && c.IsDeleted != true)
.OrderBy(c => c.SiteContactOrder)
.ThenBy(c => c.Id)
.ToListAsync(cancellationToken);
}
public async Task<Locations> AddAsync(Locations location, CancellationToken cancellationToken)
{
location.CreatedDate = DateTime.UtcNow;
await _context.Locations.AddAsync(location, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return location;
}
public async Task UpdateAsync(Locations location, CancellationToken cancellationToken)
{
location.LastModificationTime = DateTime.UtcNow;
_context.Locations.Update(location);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task<bool> DeleteByIdAsync(int id, CancellationToken cancellationToken)
{
var entity = await _context.Locations
.Include(l => l.Contacts)
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
if (entity == null)
return false;
var now = DateTime.UtcNow;
foreach (var contact in entity.Contacts ?? Enumerable.Empty<Contacts>())
{
if (contact.IsDeleted != true)
{
contact.IsDeleted = true;
contact.DeletionTime = now;
}
contact.LocationId = null;
}
_context.Locations.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Locations.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(w =>
(w.Name ?? "").ToLower().Contains(s) ||
(w.Title ?? "").ToLower().Contains(s));
}
var totalCount = await query.CountAsync();
var data = await query
.Select(n => new
{
n.Id,
n.Name,
n.Title
})
.OrderByDescending(d => d.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (data, totalCount);
}
}
}