mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
Merge remote-tracking branch 'origin/dev' into feat/SH-186-status-auto-derivation
This commit is contained in:
commit
cd34a23e78
7 changed files with 103 additions and 4 deletions
|
|
@ -25,6 +25,8 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
HasAvetaDocument = w.workOrderAttachments!.Any(a =>
|
HasAvetaDocument = w.workOrderAttachments!.Any(a =>
|
||||||
(a.IsDeleted == null || a.IsDeleted == false)
|
(a.IsDeleted == null || a.IsDeleted == false)
|
||||||
&& a.Category == WorkOrderMediaCategory.Aveta),
|
&& a.Category == WorkOrderMediaCategory.Aveta),
|
||||||
|
MediaCount = w.workOrderAttachments!.Count(a =>
|
||||||
|
a.IsDeleted == null || a.IsDeleted == false),
|
||||||
w.OriginalDate,
|
w.OriginalDate,
|
||||||
w.OriginalWeek,
|
w.OriginalWeek,
|
||||||
w.WorkOrderType,
|
w.WorkOrderType,
|
||||||
|
|
@ -157,7 +159,8 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.OriginalDate,
|
w.OriginalDate,
|
||||||
w.OriginalWeek,
|
w.OriginalWeek,
|
||||||
isUnscheduled,
|
isUnscheduled,
|
||||||
w.Severity);
|
w.Severity,
|
||||||
|
w.MediaCount);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
DateOnly? OriginalDate,
|
DateOnly? OriginalDate,
|
||||||
DateOnly? OriginalWeek,
|
DateOnly? OriginalWeek,
|
||||||
bool IsUnscheduled,
|
bool IsUnscheduled,
|
||||||
string? Severity = null);
|
string? Severity = null,
|
||||||
|
int MediaCount = 0);
|
||||||
|
|
||||||
public record WorkOrderBoardQueryResult(
|
public record WorkOrderBoardQueryResult(
|
||||||
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? ServiceNotes { get; set; }
|
public string? ServiceNotes { get; set; }
|
||||||
public List<string>? ExtraServices { get; set; }
|
public List<string>? ExtraServices { get; set; }
|
||||||
public DocStatus? DocStatus { 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; }
|
public DateTime? CompletedDate { get; set; }
|
||||||
/// <summary>Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar).</summary>
|
/// <summary>Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar).</summary>
|
||||||
public string? FlagColor { get; set; }
|
public string? FlagColor { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
"image/png",
|
"image/png",
|
||||||
"video/mp4",
|
"video/mp4",
|
||||||
"video/quicktime"
|
"video/quicktime"
|
||||||
|
|
@ -22,15 +23,24 @@ namespace SeaHaven.Services.Helpers
|
||||||
["video/quicktime"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mov" }
|
["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)
|
public static bool IsAllowed(IFormFile file)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length <= 0)
|
if (file == null || file.Length <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var contentType = (file.ContentType ?? string.Empty).Trim();
|
var declaredType = (file.ContentType ?? string.Empty).Trim();
|
||||||
if (string.IsNullOrWhiteSpace(contentType) || !AllowedContentTypes.Contains(contentType))
|
if (string.IsNullOrWhiteSpace(declaredType) || !AllowedContentTypes.Contains(declaredType))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
var contentType = CanonicalContentType(declaredType);
|
||||||
|
|
||||||
var extension = Path.GetExtension(file.FileName ?? string.Empty);
|
var extension = Path.GetExtension(file.FileName ?? string.Empty);
|
||||||
if (string.IsNullOrWhiteSpace(extension)
|
if (string.IsNullOrWhiteSpace(extension)
|
||||||
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)
|
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
ExtraServices = ParseExtraServices(row.ExtraServices),
|
ExtraServices = ParseExtraServices(row.ExtraServices),
|
||||||
AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts),
|
AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts),
|
||||||
DocStatus = row.DocStatus,
|
DocStatus = row.DocStatus,
|
||||||
|
MediaCount = row.MediaCount,
|
||||||
CompletedDate = row.CompletedDate,
|
CompletedDate = row.CompletedDate,
|
||||||
FlagColor = row.FlagColor,
|
FlagColor = row.FlagColor,
|
||||||
Severity = WorkOrderSeverityRules.ParseLevel(row.Severity),
|
Severity = WorkOrderSeverityRules.ParseLevel(row.Severity),
|
||||||
|
|
|
||||||
|
|
@ -895,6 +895,81 @@ public class WorkOrderBoardServiceTests
|
||||||
Assert.False(response.Scheduled[0].HasAvetaDocument);
|
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]
|
[Fact]
|
||||||
public async Task GetBoardAsync_InvalidWeekWindow_Throws()
|
public async Task GetBoardAsync_InvalidWeekWindow_Throws()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1748,6 +1748,13 @@ public class WorkOrderMediaFileRulesTests
|
||||||
Assert.True(WorkOrderMediaFileRules.IsAllowed(FormFile(jpeg, "photo.jpg", "image/jpeg")));
|
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]
|
[Fact]
|
||||||
public void IsAllowed_SpoofedExtension_ReturnsFalse()
|
public void IsAllowed_SpoofedExtension_ReturnsFalse()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue