shoc-backend/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.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

187 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Data.SeaHavenIndustries.Enums;
using SeaHaven.Services.Helpers;
namespace SeaHaven.Services.DTOs
{
public class WorkOrderBoardQueryDto
{
public DateOnly WeekStart { get; set; }
public DateOnly? WeekEnd { get; set; }
public List<string>? Dispatchers { get; set; }
public bool MyWorkOrders { get; set; }
public List<WorkOrderType>? Types { get; set; }
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
public bool Overdue { get; set; }
public string? Search { get; set; }
}
/// <summary>
/// Scheduled-in-week counts for GET /board. <see cref="Total"/> is the week
/// match before the bar search filter; <see cref="Returned"/> is after it.
/// Undated work orders are not included.
/// </summary>
public class WorkOrderBoardCountsDto
{
public int Returned { get; set; }
public int Total { get; set; }
}
public class WorkOrderUpliftSummaryDto
{
public bool HasUplift { get; set; }
public int PendingCount { get; set; }
public string? PrimaryStatus { get; set; }
public decimal? Amount { get; set; }
}
public class WorkOrderFrozenSiteDto
{
public string? Code { get; set; }
public string? Label { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Maps { get; set; }
}
public class WorkOrderFrozenCompanyDto
{
public string? Name { get; set; }
public string? ContactName { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Maps { get; set; }
}
public class WorkOrderFrozenPocDto
{
public List<WorkOrderFrozenPocContactDto> Contacts { get; set; } = new();
public string? Notes { get; set; }
}
public class WorkOrderFrozenPocContactDto
{
public string? Name { get; set; }
public string? Phone { get; set; }
}
public class WorkOrderBoardRowDto
{
public int Id { get; set; }
public string? WoNumber { get; set; }
public int RescheduleCount { get; set; }
public int CarriedOver { get; set; }
public bool IsAddOn { get; set; }
public bool AvetaRequired { get; set; }
public bool HasAvetaDocument { get; set; }
public DateOnly? OriginalDate { get; set; }
public DateOnly? OriginalWeek { get; set; }
public WorkOrderType? WorkOrderType { get; set; }
public int? ServiceId { get; set; }
public string? ServiceNameSnapshot { get; set; }
public bool IsPastDue { get; set; }
public string? SiteCode { get; set; }
public string? LocationName { get; set; }
public string? PocName { get; set; }
public string? PocPhone { get; set; }
public string? PocNotes { get; set; }
/// <summary>SH-190: a manual WO-level POC override is present (locked away from the Site).</summary>
public bool PocCustomized { get; set; }
public List<WorkOrderAdditionalContactDto>? AdditionalContacts { get; set; }
public WorkOrderFrozenSiteDto? FrozenSite { get; set; }
public WorkOrderFrozenCompanyDto? FrozenCompany { get; set; }
public WorkOrderFrozenPocDto? FrozenPoc { get; set; }
public LifecycleStatus? LifecycleStatus { get; set; }
public string? LifecycleStatusLabel { get; set; }
public string? LegacyStatus { get; set; }
public string? DispatcherId { get; set; }
public string? DispatcherName { get; set; }
public string? Initials { get; set; }
public string? Color { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateOnly? TargetWeek { get; set; }
public bool? ScheduleWeekOnly { get; set; }
public string? DayGroup { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
/// <summary>
/// Primary dispatch status. Inactive statuses are not a live company assignment.
/// </summary>
public string? PrimaryDispatchStatus { get; set; }
public string? TechName { get; set; }
public string? TechPhone { get; set; }
public DateTime? ApptDate { get; set; }
public string? ApptTime { get; set; }
public string? Pm { get; set; }
public string? ServiceNotes { get; set; }
public List<string>? ExtraServices { get; set; }
public DocStatus? DocStatus { get; set; }
/// <summary>Live photo/video/document count for the board CompDoc cell (excludes soft-deleted).</summary>
public int MediaCount { get; set; }
public DateTime? CompletedDate { get; set; }
/// <summary>Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar).</summary>
public string? FlagColor { get; set; }
/// <summary>Severity 1–5 for Emergency/Reactive. Null when unset or not applicable.</summary>
public int? Severity { get; set; }
public int? PrimaryDispatchId { get; set; }
public byte[]? RowVersion { get; set; }
public byte[]? DispatchRowVersion { get; set; }
/// <summary>
/// Count of uplift requests with status Pending across all dispatches tied to this work order (SH-188).
/// Authoritative completion-gate signal for the board/search/detail row contract.
/// </summary>
public int PendingUpliftCount { get; set; }
/// <summary>SH-196: board/search uplift column contract.</summary>
public WorkOrderUpliftSummaryDto? UpliftSummary { get; set; }
}
public class WorkOrderBoardPatchRequestDto
{
public string Field { get; set; } = "";
public string? Value { get; set; }
public string? WorkOrderVersion { get; set; }
public string? DispatchVersion { get; set; }
public int? PrimaryDispatchId { get; set; }
}
public class WorkOrderBoardConflictDto
{
public string Message { get; set; } = "The record was modified by another user. Refresh and retry.";
public WorkOrderBoardRowDto? CurrentState { get; set; }
}
public class WorkOrderBoardValidationErrorDto
{
public string Code { get; set; } = "";
public string Message { get; set; } = "";
}
public class WorkOrderBoardResponseDto
{
public DateOnly WeekStart { get; set; }
public DateOnly WeekEnd { get; set; }
public WorkOrderBoardCountsDto Counts { get; set; } = new();
/// <summary>
/// Always empty on the weekly board. Undated rows are served by GET /board/search.
/// Kept so existing clients can keep reading the field.
/// </summary>
public List<WorkOrderBoardRowDto> Unscheduled { get; set; } = new();
public List<WorkOrderBoardRowDto> Scheduled { get; set; } = new();
}
public class DispatcherLookupDto
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string? Initials { get; set; }
public string? Color { get; set; }
}
}