mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
Editing a work order's POC never reached the backend: no update path wrote
PocName/PocPhone/PocNotes, so the optimistic UI edit was lost on refetch and
the completion freeze captured the Site contact instead of the manual value,
and nothing was audited.
- Add tenant-scoped PATCH api/workorders/{id}/poc via new WorkOrderPocService
+ WorkOrderPocDataService: persists the override, stages FieldChanged audit
entries (which also write field locks so sync never overwrites a manual POC),
and enforces row-version concurrency and terminal-status read-only rules.
- Lock semantics (SH-190): a manual POC away from the Site's live contacts is
stored WO-level; an edit equal to a live Site contact (or blanking name+phone)
stores nothing so the WO follows the Site. PocCustomized exposes the state.
- Board projection, completion freeze and create now share one Site-contact
fallback (first non-deleted contact by SiteContactOrder) so a never-overridden
WO keeps following the Site, including at create when the wizard prefills it.
- Route contract baseline gains PATCH {id:int}/poc.
144 lines
5.8 KiB
C#
144 lines
5.8 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
|
|
namespace SeaHaven.DataServices.Implementation
|
|
{
|
|
public class WorkOrderBoardMutationDataService : IWorkOrderBoardMutationDataService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public WorkOrderBoardMutationDataService(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public Task<bool> VendorExistsAsync(int vendorId, CancellationToken cancellationToken)
|
|
=> _context.Vendors.AnyAsync(v => v.Id == vendorId && v.IsDeleted != true, cancellationToken);
|
|
|
|
public async Task<int> GetMaxWorkOrderIdAsync(CancellationToken cancellationToken)
|
|
=> await _context.workOrders.MaxAsync(w => (int?)w.Id, cancellationToken) ?? 0;
|
|
|
|
public Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken)
|
|
=> _context.workOrders
|
|
.Include(workOrder => workOrder.Locations!)
|
|
.ThenInclude(location => location.Contacts)
|
|
.Include(workOrder => workOrder.WorkOrderContacts!)
|
|
.ThenInclude(contact => contact.POC)
|
|
.Include(workOrder => workOrder.PrimaryDispatch!)
|
|
.ThenInclude(dispatch => dispatch.Vendor)
|
|
.FirstOrDefaultAsync(
|
|
w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null),
|
|
cancellationToken);
|
|
|
|
public Task<Dispatch?> GetTrackedDispatchAsync(int dispatchId, int workOrderId, CancellationToken cancellationToken)
|
|
=> _context.Dispatches
|
|
.Include(d => d.DispatchWorkOrders)
|
|
.FirstOrDefaultAsync(
|
|
d => d.Id == dispatchId
|
|
&& (d.WorkOrderId == workOrderId
|
|
|| d.DispatchWorkOrders!.Any(link => link.WorkOrderId == workOrderId)),
|
|
cancellationToken);
|
|
|
|
public Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken)
|
|
=> _context.Dispatches
|
|
.Include(d => d.DispatchWorkOrders)
|
|
.FirstOrDefaultAsync(d => d.Id == dispatchId, cancellationToken);
|
|
|
|
public void DetachWorkOrderFromDispatch(Dispatch dispatch, int workOrderId)
|
|
{
|
|
var links = dispatch.DispatchWorkOrders?
|
|
.Where(link => link.WorkOrderId == workOrderId)
|
|
.ToList();
|
|
if (links == null || links.Count == 0)
|
|
{
|
|
links = _context.DispatchWorkOrders
|
|
.Where(link => link.DispatchId == dispatch.Id && link.WorkOrderId == workOrderId)
|
|
.ToList();
|
|
}
|
|
|
|
foreach (var link in links)
|
|
_context.DispatchWorkOrders.Remove(link);
|
|
|
|
if (dispatch.WorkOrderId != workOrderId)
|
|
return;
|
|
|
|
var remainingIds = (dispatch.DispatchWorkOrders ?? Enumerable.Empty<DispatchWorkOrder>())
|
|
.Where(link => link.WorkOrderId != workOrderId)
|
|
.Select(link => link.WorkOrderId)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
if (remainingIds.Count == 0)
|
|
{
|
|
dispatch.WorkOrderId = null;
|
|
return;
|
|
}
|
|
|
|
var preferredOwnerId = _context.workOrders
|
|
.Where(workOrder => remainingIds.Contains(workOrder.Id)
|
|
&& workOrder.PrimaryDispatchId == dispatch.Id)
|
|
.Select(workOrder => (int?)workOrder.Id)
|
|
.FirstOrDefault();
|
|
|
|
dispatch.WorkOrderId = preferredOwnerId ?? remainingIds[0];
|
|
}
|
|
|
|
public void TrackNewWorkOrder(WorkOrder workOrder)
|
|
=> _context.workOrders.Add(workOrder);
|
|
|
|
public void TrackNewDispatch(Dispatch dispatch)
|
|
=> _context.Dispatches.Add(dispatch);
|
|
|
|
public void TrackWorkOrderContact(int workOrderId, int contactId, string? notes)
|
|
=> _context.WorkOrderContacts.Add(new WorkOrderContacts
|
|
{
|
|
WorkorderId = workOrderId,
|
|
ContactId = contactId,
|
|
Notes = notes
|
|
});
|
|
|
|
public void SetExpectedWorkOrderVersion(WorkOrder workOrder, byte[] version)
|
|
=> _context.Entry(workOrder).Property(w => w.RowVersion).OriginalValue = version;
|
|
|
|
public void SetExpectedDispatchVersion(Dispatch dispatch, byte[] version)
|
|
=> _context.Entry(dispatch).Property(d => d.RowVersion).OriginalValue = version;
|
|
|
|
public bool HasPendingChanges()
|
|
=> _context.ChangeTracker.HasChanges();
|
|
|
|
public async Task ExecuteTransactionalAsync(Func<CancellationToken, Task> work, CancellationToken cancellationToken)
|
|
{
|
|
await using var transaction = _context.Database.IsRelational()
|
|
? await _context.Database.BeginTransactionAsync(cancellationToken)
|
|
: null;
|
|
|
|
try
|
|
{
|
|
await work(cancellationToken);
|
|
if (transaction is not null)
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
if (transaction is not null)
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<BoardSaveOutcome> SaveAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return BoardSaveOutcome.Saved;
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
_context.ChangeTracker.Clear();
|
|
return BoardSaveOutcome.ConcurrencyConflict;
|
|
}
|
|
}
|
|
}
|
|
}
|