shoc-backend/SeaHaven.DataServices/Implementation/CompletionDocTemplateDataService.cs

198 lines
8.1 KiB
C#
Raw Permalink Normal View History

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class CompletionDocTemplateDataService : ICompletionDocTemplateDataService
{
private static readonly LifecycleStatus[] OpenStatuses = Enum.GetValues<LifecycleStatus>()
.Where(status => !LifecycleStatusSets.Terminal.Contains(status))
.ToArray();
private readonly ApplicationDbContext _context;
public CompletionDocTemplateDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<CompletionDocTemplateRow?> ResolveForWorkOrderAsync(string? trade, WorkOrderType? workOrderType)
{
if (!string.IsNullOrWhiteSpace(trade))
{
var byTrade = await _context.CompletionDocTemplates
.AsNoTracking()
.Where(t => t.IsActive && t.IsDeleted != true && t.ServiceKey == trade)
.OrderBy(t => t.WorkOrderType == workOrderType ? 0 : 1)
.ThenBy(t => t.Name)
.ThenBy(t => t.Id)
.Select(MapRow)
.FirstOrDefaultAsync();
if (byTrade != null)
return byTrade;
}
if (!workOrderType.HasValue)
return null;
return await _context.CompletionDocTemplates
.AsNoTracking()
.Where(t => t.IsActive && t.IsDeleted != true && t.WorkOrderType == workOrderType)
.OrderBy(t => t.Name)
.ThenBy(t => t.Id)
.Select(MapRow)
.FirstOrDefaultAsync();
}
public async Task<IReadOnlyList<CompletionDocTemplateDetailRow>> ListAsync(
string? search,
string? serviceKey,
WorkOrderType? workOrderType,
CancellationToken cancellationToken)
{
var query = _context.CompletionDocTemplates
.AsNoTracking()
.Where(t => t.IsActive && t.IsDeleted != true);
if (!string.IsNullOrWhiteSpace(serviceKey))
query = query.Where(t => t.ServiceKey == serviceKey);
if (workOrderType.HasValue)
query = query.Where(t => t.WorkOrderType == null || t.WorkOrderType == workOrderType);
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
query = query.Where(t => t.Name.ToLower().Contains(term));
}
return await ProjectDetail(query).ToListAsync(cancellationToken);
}
public async Task<CompletionDocTemplateDetailRow?> GetDetailAsync(int id, CancellationToken cancellationToken)
{
var query = _context.CompletionDocTemplates
.AsNoTracking()
.Where(t => t.Id == id && t.IsDeleted != true);
return await ProjectDetail(query).FirstOrDefaultAsync(cancellationToken);
}
public Task<CompletionDocTemplate?> GetTrackedForEditAsync(int id, CancellationToken cancellationToken)
=> _context.CompletionDocTemplates
.Include(t => t.Procedures)
.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true, cancellationToken);
public async Task<int> CreateAsync(CompletionDocTemplate template, CancellationToken cancellationToken)
{
template.CreatedDate = DateTime.UtcNow;
await _context.CompletionDocTemplates.AddAsync(template, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return template.Id;
}
public async Task SaveEditAsync(
CompletionDocTemplate template,
IReadOnlyList<CompletionDocTemplateProcedure>? replacementProcedures,
CancellationToken cancellationToken)
{
if (replacementProcedures != null)
{
// Explicit removal: the context forces Restrict on every FK before the
// procedure cascade is configured, so orphan deletion is not relied on.
_context.CompletionDocTemplateProcedures.RemoveRange(template.Procedures.ToList());
foreach (var procedure in replacementProcedures)
{
procedure.CompletionDocTemplateId = template.Id;
_context.CompletionDocTemplateProcedures.Add(procedure);
}
}
template.LastModificationTime = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
}
public async Task<bool> DeleteAndUnlinkServicesAsync(
int id,
string? deleterUserId,
CancellationToken cancellationToken)
{
var template = await _context.CompletionDocTemplates
.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true, cancellationToken);
if (template == null)
return false;
var now = DateTime.UtcNow;
template.IsDeleted = true;
template.DeletionTime = now;
template.DeleterUserId = deleterUserId;
var linkedServices = await _context.Services
.Where(s => s.CompletionDocTemplateId == id)
.ToListAsync(cancellationToken);
foreach (var service in linkedServices)
{
service.CompletionDocTemplateId = null;
service.LastModificationTime = now;
}
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<IReadOnlyList<int>> GetOpenLinkedWorkOrderIdsAsync(
int templateId,
int? accountId,
CancellationToken cancellationToken)
{
var workOrders = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
if (accountId.HasValue)
workOrders = WorkOrderBoardQueryFilters.ApplyAccountScope(workOrders, accountId.Value);
var linkedServiceIds = _context.Services
.Where(s => s.CompletionDocTemplateId == templateId)
.Select(s => (int?)s.Id);
// Shared status filter so legacy rows without a LifecycleStatus are read
// through their legacy status text (a legacy "completed" row is not open).
workOrders = WorkOrderBoardQueryFilters.ApplyStatusFilter(workOrders, OpenStatuses);
return await workOrders
.Where(w => w.ServiceId != null && linkedServiceIds.Contains(w.ServiceId))
.OrderBy(w => w.Id)
.Select(w => w.Id)
.ToListAsync(cancellationToken);
}
private IQueryable<CompletionDocTemplateDetailRow> ProjectDetail(IQueryable<CompletionDocTemplate> templates)
=> from t in templates
join u in _context.Users on t.createdby equals u.Id into creators
from creator in creators.DefaultIfEmpty()
orderby t.Name, t.Id
select new CompletionDocTemplateDetailRow(
t.Id,
t.Name,
t.ServiceKey,
t.WorkOrderType,
t.TemplateUrl,
t.IsActive,
t.ExtraSafetyNote,
t.CreatedDate,
t.LastModificationTime,
creator != null ? creator.FirstName : null,
creator != null ? creator.LastName : null,
t.Procedures
.OrderBy(p => p.SortOrder)
.ThenBy(p => p.Id)
.Select(p => new CompletionDocTemplateProcedureRow(p.Id, p.SortOrder, p.Name, p.Description))
.ToList());
private static readonly System.Linq.Expressions.Expression<Func<CompletionDocTemplate, CompletionDocTemplateRow>> MapRow =
t => new CompletionDocTemplateRow(t.Id, t.Name, t.ServiceKey, t.WorkOrderType, t.TemplateUrl, t.IsActive);
}
}