Merge remote-tracking branch 'origin/dev' into feat/SH-186-status-auto-derivation

This commit is contained in:
Arthur Bassi 2026-09-09 10:24:15 -03:00
commit cd34a23e78
7 changed files with 103 additions and 4 deletions

View file

@ -25,6 +25,8 @@ namespace SeaHaven.DataServices.Helpers
HasAvetaDocument = w.workOrderAttachments!.Any(a =>
(a.IsDeleted == null || a.IsDeleted == false)
&& a.Category == WorkOrderMediaCategory.Aveta),
MediaCount = w.workOrderAttachments!.Count(a =>
a.IsDeleted == null || a.IsDeleted == false),
w.OriginalDate,
w.OriginalWeek,
w.WorkOrderType,
@ -157,7 +159,8 @@ namespace SeaHaven.DataServices.Helpers
w.OriginalDate,
w.OriginalWeek,
isUnscheduled,
w.Severity);
w.Severity,
w.MediaCount);
}).ToList();
}

View file

@ -64,7 +64,8 @@ namespace SeaHaven.DataServices.Interfaces
DateOnly? OriginalDate,
DateOnly? OriginalWeek,
bool IsUnscheduled,
string? Severity = null);
string? Severity = null,
int MediaCount = 0);
public record WorkOrderBoardQueryResult(
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,

View file

@ -79,6 +79,8 @@ namespace SeaHaven.Services.DTOs
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; }

View file

@ -8,6 +8,7 @@ namespace SeaHaven.Services.Helpers
private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
{
"image/jpeg",
"image/jpg",
"image/png",
"video/mp4",
"video/quicktime"
@ -22,15 +23,24 @@ namespace SeaHaven.Services.Helpers
["video/quicktime"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mov" }
};
private static string CanonicalContentType(string contentType)
{
if (contentType.Equals("image/jpg", StringComparison.OrdinalIgnoreCase))
return "image/jpeg";
return contentType;
}
public static bool IsAllowed(IFormFile file)
{
if (file == null || file.Length <= 0)
return false;
var contentType = (file.ContentType ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(contentType) || !AllowedContentTypes.Contains(contentType))
var declaredType = (file.ContentType ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(declaredType) || !AllowedContentTypes.Contains(declaredType))
return false;
var contentType = CanonicalContentType(declaredType);
var extension = Path.GetExtension(file.FileName ?? string.Empty);
if (string.IsNullOrWhiteSpace(extension)
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)

View file

@ -137,6 +137,7 @@ namespace SeaHaven.Services.Implementation
ExtraServices = ParseExtraServices(row.ExtraServices),
AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts),
DocStatus = row.DocStatus,
MediaCount = row.MediaCount,
CompletedDate = row.CompletedDate,
FlagColor = row.FlagColor,
Severity = WorkOrderSeverityRules.ParseLevel(row.Severity),

View file

@ -895,6 +895,81 @@ public class WorkOrderBoardServiceTests
Assert.False(response.Scheduled[0].HasAvetaDocument);
}
[Fact]
public async Task GetBoardAsync_MediaCount_CountsLiveAttachmentsOnly()
{
await using var context = CreateContext();
var weekStart = new DateOnly(2026, 6, 22);
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
WorkOrderType = WorkOrderType.Emergency,
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
LifecycleStatus = LifecycleStatus.Scheduled
});
context.workOrderAttachments.Add(new WorkOrderAttachments
{
WorkorderId = 1,
Attachments = "https://example.com/before.jpg",
Category = WorkOrderMediaCategory.Before
});
context.workOrderAttachments.Add(new WorkOrderAttachments
{
WorkorderId = 1,
Attachments = "https://example.com/extra.jpg",
Category = WorkOrderMediaCategory.Extra
});
context.workOrderAttachments.Add(new WorkOrderAttachments
{
WorkorderId = 1,
Attachments = "https://example.com/removed.jpg",
Category = WorkOrderMediaCategory.Extra,
IsDeleted = true
});
await context.SaveChangesAsync();
var boardService = new WorkOrderBoardService(
new WorkOrderBoardDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto
{
WeekStart = weekStart
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Single(response.Scheduled);
Assert.Equal(2, response.Scheduled[0].MediaCount);
}
[Fact]
public async Task GetBoardAsync_MediaCount_ZeroWithoutAttachments()
{
await using var context = CreateContext();
var weekStart = new DateOnly(2026, 6, 22);
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
WorkOrderType = WorkOrderType.Emergency,
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
LifecycleStatus = LifecycleStatus.Scheduled
});
await context.SaveChangesAsync();
var boardService = new WorkOrderBoardService(
new WorkOrderBoardDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto
{
WeekStart = weekStart
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Single(response.Scheduled);
Assert.Equal(0, response.Scheduled[0].MediaCount);
}
[Fact]
public async Task GetBoardAsync_InvalidWeekWindow_Throws()
{

View file

@ -1748,6 +1748,13 @@ public class WorkOrderMediaFileRulesTests
Assert.True(WorkOrderMediaFileRules.IsAllowed(FormFile(jpeg, "photo.jpg", "image/jpeg")));
}
[Fact]
public void IsAllowed_ImageJpgAlias_ReturnsTrue()
{
var jpeg = new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10 };
Assert.True(WorkOrderMediaFileRules.IsAllowed(FormFile(jpeg, "photo.jpg", "image/jpg")));
}
[Fact]
public void IsAllowed_SpoofedExtension_ReturnsFalse()
{