mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
UpdateAsync only applied WorkOrderType when it had a value, so once a template was restricted to one work order type no request could make it trade-generic again. The DTO now records whether workOrderType was present in the body: omitting it keeps the stored value, an explicit null clears it, and a concrete value sets it. The templates page echoes the stored legacy fields on PUT, so its behaviour is unchanged.
259 lines
11 KiB
C#
259 lines
11 KiB
C#
using System.Security.Claims;
|
|
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Constants;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public sealed class CompletionDocTemplateService : ICompletionDocTemplateService
|
|
{
|
|
private readonly ICompletionDocTemplateDataService _templateData;
|
|
private readonly ITeamPermissionOverrideDataService _permissionUsers;
|
|
private readonly ITeamPermissionPolicy _permissionPolicy;
|
|
private readonly IWorkOrderAccountResolver _accountResolver;
|
|
|
|
public CompletionDocTemplateService(
|
|
ICompletionDocTemplateDataService templateData,
|
|
ITeamPermissionOverrideDataService permissionUsers,
|
|
ITeamPermissionPolicy permissionPolicy,
|
|
IWorkOrderAccountResolver accountResolver)
|
|
{
|
|
_templateData = templateData;
|
|
_permissionUsers = permissionUsers;
|
|
_permissionPolicy = permissionPolicy;
|
|
_accountResolver = accountResolver;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<CompletionDocTemplateDetailDto>> ListAsync(
|
|
string? search,
|
|
string? serviceKey,
|
|
WorkOrderType? workOrderType,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var rows = await _templateData.ListAsync(search, serviceKey, workOrderType, cancellationToken);
|
|
return rows.Select(Map).ToList();
|
|
}
|
|
|
|
public async Task<CompletionDocTemplateDetailDto?> GetAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var row = await _templateData.GetDetailAsync(id, cancellationToken);
|
|
return row == null ? null : Map(row);
|
|
}
|
|
|
|
public async Task<CompletionDocTemplateLinkedWorkOrdersDto> GetLinkedWorkOrdersAsync(
|
|
ClaimsPrincipal user,
|
|
int id,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
|
|
|
if (await _templateData.GetDetailAsync(id, cancellationToken) == null)
|
|
throw NotFound();
|
|
|
|
var ids = await _templateData.GetOpenLinkedWorkOrderIdsAsync(id, accountId, cancellationToken);
|
|
return new CompletionDocTemplateLinkedWorkOrdersDto { Count = ids.Count, WorkOrderIds = ids.ToList() };
|
|
}
|
|
|
|
public async Task<CompletionDocTemplateDetailDto> CreateAsync(
|
|
ClaimsPrincipal user,
|
|
CompletionDocTemplateCreateDto request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var actorId = await EnsureAllowedAsync(
|
|
user,
|
|
TeamPermissionKeys.CreateCompletionDocTemplates,
|
|
"You do not have permission to create completion document templates.",
|
|
cancellationToken);
|
|
|
|
var input = Validate(request);
|
|
var entity = new CompletionDocTemplate
|
|
{
|
|
Name = input.Name,
|
|
ExtraSafetyNote = input.ExtraSafetyNote,
|
|
ServiceKey = input.ServiceKey ?? "",
|
|
WorkOrderType = request.WorkOrderType,
|
|
TemplateUrl = input.TemplateUrl ?? "",
|
|
IsActive = request.IsActive ?? true,
|
|
createdby = actorId,
|
|
Procedures = (input.Procedures ?? new List<CompletionDocTemplateProcedure>()).ToList()
|
|
};
|
|
|
|
var id = await _templateData.CreateAsync(entity, cancellationToken);
|
|
return await GetSavedAsync(id, cancellationToken);
|
|
}
|
|
|
|
public async Task<CompletionDocTemplateDetailDto> UpdateAsync(
|
|
ClaimsPrincipal user,
|
|
int id,
|
|
CompletionDocTemplateCreateDto request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await EnsureAllowedAsync(
|
|
user,
|
|
TeamPermissionKeys.EditCompletionDocTemplates,
|
|
"You do not have permission to edit completion document templates.",
|
|
cancellationToken);
|
|
|
|
var input = Validate(request);
|
|
var entity = await _templateData.GetTrackedForEditAsync(id, cancellationToken) ?? throw NotFound();
|
|
|
|
entity.Name = input.Name;
|
|
entity.ExtraSafetyNote = input.ExtraSafetyNote;
|
|
if (input.ServiceKey != null)
|
|
entity.ServiceKey = input.ServiceKey;
|
|
if (request.WorkOrderTypeSpecified)
|
|
entity.WorkOrderType = request.WorkOrderType;
|
|
if (input.TemplateUrl != null)
|
|
entity.TemplateUrl = input.TemplateUrl;
|
|
if (request.IsActive.HasValue)
|
|
entity.IsActive = request.IsActive.Value;
|
|
|
|
await _templateData.SaveEditAsync(entity, input.Procedures, cancellationToken);
|
|
return await GetSavedAsync(id, cancellationToken);
|
|
}
|
|
|
|
public async Task DeleteAsync(ClaimsPrincipal user, int id, CancellationToken cancellationToken)
|
|
{
|
|
var actorId = await EnsureAllowedAsync(
|
|
user,
|
|
TeamPermissionKeys.DeleteCompletionDocTemplates,
|
|
"You do not have permission to delete completion document templates.",
|
|
cancellationToken);
|
|
|
|
if (!await _templateData.DeleteAndUnlinkServicesAsync(id, actorId, cancellationToken))
|
|
throw NotFound();
|
|
}
|
|
|
|
private async Task<string> EnsureAllowedAsync(
|
|
ClaimsPrincipal user,
|
|
string permissionKey,
|
|
string deniedMessage,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = user?.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
var permissionUser = string.IsNullOrWhiteSpace(userId)
|
|
? null
|
|
: await _permissionUsers.GetUserAsync(userId, cancellationToken);
|
|
|
|
if (permissionUser is null
|
|
|| !_permissionPolicy.IsAllowed(permissionUser.RoleName, permissionKey, permissionUser.Overrides))
|
|
{
|
|
throw new WorkOrderBoardValidationException("Forbidden", deniedMessage);
|
|
}
|
|
|
|
return userId!;
|
|
}
|
|
|
|
private async Task<CompletionDocTemplateDetailDto> GetSavedAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var row = await _templateData.GetDetailAsync(id, cancellationToken) ?? throw NotFound();
|
|
return Map(row);
|
|
}
|
|
|
|
private sealed record ValidatedInput(
|
|
string Name,
|
|
string? ExtraSafetyNote,
|
|
string? ServiceKey,
|
|
string? TemplateUrl,
|
|
IReadOnlyList<CompletionDocTemplateProcedure>? Procedures);
|
|
|
|
private static ValidatedInput Validate(CompletionDocTemplateCreateDto request)
|
|
{
|
|
if (request == null || string.IsNullOrWhiteSpace(request.Name))
|
|
throw new WorkOrderBoardValidationException("TitleRequired", "Document Title is required.");
|
|
|
|
var extraSafetyNote = string.IsNullOrWhiteSpace(request.ExtraSafetyNote)
|
|
? null
|
|
: request.ExtraSafetyNote.Trim();
|
|
if (extraSafetyNote?.Length > CompletionDocTemplate.ExtraSafetyNoteMaxLength)
|
|
{
|
|
throw new WorkOrderBoardValidationException(
|
|
"InvalidValue",
|
|
$"Extra safety note must be {CompletionDocTemplate.ExtraSafetyNoteMaxLength} characters or fewer.");
|
|
}
|
|
|
|
var templateUrl = request.TemplateUrl?.Trim();
|
|
if (!string.IsNullOrEmpty(templateUrl)
|
|
&& (!Uri.TryCreate(templateUrl, UriKind.Absolute, out var uri)
|
|
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)))
|
|
{
|
|
throw new WorkOrderBoardValidationException("InvalidValue", "TemplateUrl must be an absolute http(s) URL.");
|
|
}
|
|
|
|
// Blank procedure names and descriptions are allowed: authors may keep a
|
|
// placeholder step and fill it in later.
|
|
List<CompletionDocTemplateProcedure>? procedures = null;
|
|
if (request.Procedures != null)
|
|
{
|
|
procedures = new List<CompletionDocTemplateProcedure>(request.Procedures.Count);
|
|
foreach (var procedure in request.Procedures)
|
|
{
|
|
var name = procedure?.Name?.Trim() ?? "";
|
|
if (name.Length > CompletionDocTemplateProcedure.NameMaxLength)
|
|
{
|
|
throw new WorkOrderBoardValidationException(
|
|
"InvalidValue",
|
|
$"Procedure name must be {CompletionDocTemplateProcedure.NameMaxLength} characters or fewer.");
|
|
}
|
|
|
|
procedures.Add(new CompletionDocTemplateProcedure
|
|
{
|
|
SortOrder = procedures.Count,
|
|
Name = name,
|
|
Description = procedure?.Description ?? ""
|
|
});
|
|
}
|
|
}
|
|
|
|
return new ValidatedInput(
|
|
request.Name.Trim(),
|
|
extraSafetyNote,
|
|
request.ServiceKey?.Trim(),
|
|
templateUrl,
|
|
procedures);
|
|
}
|
|
|
|
private static WorkOrderBoardValidationException NotFound()
|
|
=> new("NotFound", "Template not found.");
|
|
|
|
private static CompletionDocTemplateDetailDto Map(CompletionDocTemplateDetailRow row)
|
|
{
|
|
var createdByName = string.Join(
|
|
" ",
|
|
new[] { row.CreatedByFirstName, row.CreatedByLastName }
|
|
.Where(part => !string.IsNullOrWhiteSpace(part))
|
|
.Select(part => part!.Trim()));
|
|
|
|
return new CompletionDocTemplateDetailDto
|
|
{
|
|
Id = row.Id,
|
|
Name = row.Name,
|
|
ServiceKey = row.ServiceKey,
|
|
WorkOrderType = row.WorkOrderType,
|
|
TemplateUrl = row.TemplateUrl,
|
|
IsActive = row.IsActive,
|
|
ExtraSafetyNote = row.ExtraSafetyNote,
|
|
Procedures = row.Procedures
|
|
.Select(p => new CompletionDocTemplateProcedureDto
|
|
{
|
|
Id = p.Id,
|
|
Name = p.Name,
|
|
Description = p.Description,
|
|
SortOrder = p.SortOrder
|
|
})
|
|
.ToList(),
|
|
CreatedAt = AsUtc(row.CreatedAt),
|
|
UpdatedAt = AsUtc(row.UpdatedAt),
|
|
CreatedByName = createdByName.Length == 0 ? null : createdByName
|
|
};
|
|
}
|
|
|
|
// Audit timestamps are written as DateTime.UtcNow; the store drops the kind.
|
|
private static DateTime? AsUtc(DateTime? value)
|
|
=> value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
|
|
}
|
|
}
|