mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 14:13:12 +00:00
Work orders without a LifecycleStatus are open or closed according to their legacy status text. The linked work-order count now goes through the shared board status filter, so a legacy completed or cancelled row is no longer reported as depending on the template.
197 lines
8.1 KiB
C#
197 lines
8.1 KiB
C#
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);
|
|
}
|
|
}
|