shoc-backend/SeaHaven.DataServices/Implementation/WorkOrderPocDataService.cs
Alexandre Brandizzi cbecc7b4ea fix(workorders): persist manual POC override with audit and site-follow (SH-379)
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.
2026-09-18 14:30:54 -03:00

47 lines
1.7 KiB
C#

using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class WorkOrderPocDataService : IWorkOrderPocDataService
{
private readonly ApplicationDbContext _context;
public WorkOrderPocDataService(ApplicationDbContext context)
{
_context = context;
}
public Task<WorkOrder?> GetTrackedWorkOrderAsync(
int workOrderId,
int? accountId,
CancellationToken cancellationToken)
=> _context.workOrders
.Include(workOrder => workOrder.Locations!)
.ThenInclude(location => location.Contacts)
.Include(workOrder => workOrder.WorkOrderContacts!)
.ThenInclude(contact => contact.POC)
.FirstOrDefaultAsync(
w => w.Id == workOrderId
&& w.istemplate != true
&& (w.IsDeleted != true || w.IsDeleted == null)
&& (accountId == null || w.AccountId == accountId),
cancellationToken);
public async Task<IReadOnlyList<Contacts>> GetSiteContactsAsync(
int? locationId,
CancellationToken cancellationToken)
{
if (locationId == null)
return Array.Empty<Contacts>();
return await _context.Contacts
.AsNoTracking()
.Where(c => c.LocationId == locationId && c.IsDeleted != true)
.OrderBy(c => c.SiteContactOrder)
.ThenBy(c => c.Id)
.ToListAsync(cancellationToken);
}
}
}