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() .Where(status => !LifecycleStatusSets.Terminal.Contains(status)) .ToArray(); private readonly ApplicationDbContext _context; public CompletionDocTemplateDataService(ApplicationDbContext context) { _context = context; } public async Task 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> 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 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 GetTrackedForEditAsync(int id, CancellationToken cancellationToken) => _context.CompletionDocTemplates .Include(t => t.Procedures) .FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true, cancellationToken); public async Task 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? 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 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> 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 ProjectDetail(IQueryable 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> MapRow = t => new CompletionDocTemplateRow(t.Id, t.Name, t.ServiceKey, t.WorkOrderType, t.TemplateUrl, t.IsActive); } }