mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fix(work-orders): accept image/jpg MIME and expose board MediaCount (#107)
Some checks are pending
Validate and deploy / Validate deployable source bundle (push) Waiting to run
Validate and deploy / Deploy shoc-backend-dev through Terraform (push) Blocked by required conditions
Validate and deploy / Deploy shoc-backend-staging to Elastic Beanstalk (push) Blocked by required conditions
Some checks are pending
Validate and deploy / Validate deployable source bundle (push) Waiting to run
Validate and deploy / Deploy shoc-backend-dev through Terraform (push) Blocked by required conditions
Validate and deploy / Deploy shoc-backend-staging to Elastic Beanstalk (push) Blocked by required conditions
Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
This commit is contained in:
parent
1bdbc12292
commit
a0fdd19934
7 changed files with 103 additions and 4 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue