Skip to content

Commit cd02ec4

Browse files
committed
feat: add dark theme styles for mobile cards and labels
feat: add error for invoice document entry not found in crate tracking feat: implement UploadInvoiceCratePod endpoint in InvoiceController refactor: use ILike for transaction type comparisons in crate transaction queries fix: update crate transaction queries to use ILike for case-insensitive matching feat: add SAP error handling for no matching records found feat: create UploadInvoiceCratePodCommand for handling crate POD uploads feat: implement UploadInvoiceCratePodHandler to process crate POD uploads
1 parent c01bcd0 commit cd02ec4

10 files changed

Lines changed: 1099 additions & 502 deletions

File tree

ShopInventory.Web/Components/Pages/CustomerPortal/CustomerPods.razor

Lines changed: 914 additions & 499 deletions
Large diffs are not rendered by default.

ShopInventory.Web/wwwroot/app.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8370,6 +8370,38 @@ html.dark-theme {
83708370
.dark-theme .cpod-time {
83718371
color: #64748b;
83728372
}
8373+
.dark-theme .cpod-mobile-card {
8374+
background: linear-gradient(180deg, #1e293b 0%, #162033 100%);
8375+
border-color: #334155;
8376+
}
8377+
.dark-theme .cpod-mobile-card-header {
8378+
border-bottom-color: #334155;
8379+
}
8380+
.dark-theme .cpod-mobile-label {
8381+
color: #64748b;
8382+
}
8383+
.dark-theme .cpod-mobile-code {
8384+
color: #94a3b8;
8385+
}
8386+
.dark-theme .cpod-mobile-file-card {
8387+
background: #0f172a;
8388+
border-color: #334155;
8389+
}
8390+
.dark-theme .cpod-mobile-value,
8391+
.dark-theme .cpod-mobile-user {
8392+
color: #e2e8f0;
8393+
}
8394+
.dark-theme .cpod-mobile-location {
8395+
color: #94a3b8;
8396+
}
8397+
.dark-theme .cpod-mobile-action-btn.cpod-action-view {
8398+
background: rgba(45, 212, 191, 0.1);
8399+
border-color: rgba(45, 212, 191, 0.3);
8400+
}
8401+
.dark-theme .cpod-mobile-action-btn.cpod-action-download {
8402+
background: rgba(56, 189, 248, 0.1);
8403+
border-color: rgba(56, 189, 248, 0.3);
8404+
}
83738405
.dark-theme .cpod-action-view {
83748406
color: #5eead4;
83758407
}

ShopInventory/Common/Errors/Errors.CrateTracking.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public static Error TransactionNotFound(int id) =>
1212
public static Error InvoiceNotFound(int docNum) =>
1313
Error.NotFound("CrateTracking.InvoiceNotFound", $"Invoice {docNum} was not found or is not available for crate tracking.");
1414

15+
public static Error InvoiceDocEntryNotFound(int docEntry) =>
16+
Error.NotFound("CrateTracking.InvoiceDocEntryNotFound", $"Invoice doc entry {docEntry} was not found or is not available for crate tracking.");
17+
1518
public static Error SubmissionNotFound(int id) =>
1619
Error.NotFound("CrateTracking.SubmissionNotFound", $"Crate POD submission {id} was not found.");
1720

ShopInventory/Controllers/InvoiceController.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.AspNetCore.Mvc;
44
using ShopInventory.DTOs;
55
using ShopInventory.Models;
6+
using ShopInventory.Features.Crates.Commands.UploadInvoiceCratePod;
67
using ShopInventory.Features.Invoices.Commands.CreateInvoice;
78
using ShopInventory.Features.Invoices.Commands.UploadPod;
89
using ShopInventory.Features.Invoices.Queries.DownloadInvoiceAttachment;
@@ -229,6 +230,48 @@ public async Task<IActionResult> UploadPod(
229230
Problem);
230231
}
231232

233+
[HttpPost("{docEntry:int}/crate-pod")]
234+
[Authorize(Roles = "Admin,Manager,Merchandiser,Driver")]
235+
[ProducesResponseType(typeof(CratePodSubmissionDto), StatusCodes.Status200OK)]
236+
[ProducesResponseType(typeof(ErrorResponseDto), StatusCodes.Status400BadRequest)]
237+
[RequestSizeLimit(20 * 1024 * 1024)]
238+
public async Task<IActionResult> UploadInvoiceCratePod(
239+
int docEntry,
240+
[FromForm] int? invoiceDocNum,
241+
[FromForm] decimal quantity,
242+
[FromForm] string? submissionRole,
243+
[FromForm] string? notes,
244+
IFormFile file,
245+
CancellationToken cancellationToken = default)
246+
{
247+
if (file == null || file.Length == 0)
248+
{
249+
return BadRequest(new ErrorResponseDto { Message = "A crate POD document is required." });
250+
}
251+
252+
var allowedTypes = new[] { "image/jpeg", "image/png", "image/webp", "application/pdf" };
253+
if (!allowedTypes.Contains(file.ContentType, StringComparer.OrdinalIgnoreCase))
254+
{
255+
return BadRequest(new ErrorResponseDto { Message = "Invalid file type. Only JPEG, PNG, WebP images and PDF files are allowed." });
256+
}
257+
258+
using var stream = file.OpenReadStream();
259+
var result = await mediator.Send(
260+
new UploadInvoiceCratePodCommand(
261+
docEntry,
262+
invoiceDocNum,
263+
submissionRole,
264+
quantity,
265+
notes,
266+
stream,
267+
file.FileName,
268+
file.ContentType,
269+
GetUserId()),
270+
cancellationToken);
271+
272+
return result.Match(Ok, Problem);
273+
}
274+
232275
[HttpGet("pods")]
233276
[Authorize(Roles = "Admin,Cashier,PodOperator,Driver,SalesRep")]
234277
[ProducesResponseType(typeof(PodAttachmentListResponseDto), StatusCodes.Status200OK)]

ShopInventory/Features/Crates/Commands/EnsureInvoiceCrateTransaction/EnsureInvoiceCrateTransactionHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public async Task<ErrorOr<EnsureInvoiceCrateTransactionResponseDto>> Handle(
6464

6565
var transaction = await context.CrateTransactions
6666
.FirstOrDefaultAsync(existing =>
67-
string.Equals(existing.TransactionType, CrateTrackingConstants.TransactionTypeInvoice, StringComparison.OrdinalIgnoreCase) &&
67+
EF.Functions.ILike(existing.TransactionType, CrateTrackingConstants.TransactionTypeInvoice) &&
6868
((existing.InvoiceDocEntry.HasValue && existing.InvoiceDocEntry.Value == invoice.DocEntry) ||
6969
(existing.InvoiceDocNum.HasValue && existing.InvoiceDocNum.Value == invoice.DocNum)),
7070
cancellationToken);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using ErrorOr;
2+
using MediatR;
3+
using ShopInventory.DTOs;
4+
5+
namespace ShopInventory.Features.Crates.Commands.UploadInvoiceCratePod;
6+
7+
public sealed record UploadInvoiceCratePodCommand(
8+
int InvoiceDocEntry,
9+
int? InvoiceDocNum,
10+
string? SubmissionRole,
11+
decimal Quantity,
12+
string? Notes,
13+
Stream FileStream,
14+
string FileName,
15+
string ContentType,
16+
Guid? UserId
17+
) : IRequest<ErrorOr<CratePodSubmissionDto>>;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using ErrorOr;
2+
using MediatR;
3+
using ShopInventory.Common.Errors;
4+
using ShopInventory.DTOs;
5+
using ShopInventory.Features.Crates.Commands.EnsureInvoiceCrateTransaction;
6+
using ShopInventory.Services;
7+
8+
namespace ShopInventory.Features.Crates.Commands.UploadInvoiceCratePod;
9+
10+
public sealed class UploadInvoiceCratePodHandler(
11+
ISAPServiceLayerClient sapClient,
12+
ISender mediator,
13+
ILogger<UploadInvoiceCratePodHandler> logger
14+
) : IRequestHandler<UploadInvoiceCratePodCommand, ErrorOr<CratePodSubmissionDto>>
15+
{
16+
public async Task<ErrorOr<CratePodSubmissionDto>> Handle(
17+
UploadInvoiceCratePodCommand command,
18+
CancellationToken cancellationToken)
19+
{
20+
var invoiceDocNum = command.InvoiceDocNum;
21+
if (!invoiceDocNum.HasValue || invoiceDocNum.Value <= 0)
22+
{
23+
var invoice = await sapClient.GetInvoiceByDocEntryAsync(command.InvoiceDocEntry, cancellationToken);
24+
if (invoice is null)
25+
{
26+
return Errors.CrateTracking.InvoiceDocEntryNotFound(command.InvoiceDocEntry);
27+
}
28+
29+
invoiceDocNum = invoice.DocNum;
30+
}
31+
32+
var ensureResult = await mediator.Send(
33+
new EnsureInvoiceCrateTransactionCommand(invoiceDocNum.Value, command.Quantity, command.UserId),
34+
cancellationToken);
35+
36+
if (ensureResult.IsError)
37+
{
38+
return ensureResult.Errors;
39+
}
40+
41+
if (command.FileStream.CanSeek)
42+
{
43+
command.FileStream.Position = 0;
44+
}
45+
46+
logger.LogInformation(
47+
"Routing invoice-based crate POD upload for invoice doc entry {DocEntry} / doc num {DocNum} to crate transaction {CrateTransactionId}",
48+
command.InvoiceDocEntry,
49+
invoiceDocNum.Value,
50+
ensureResult.Value.Id);
51+
52+
return await mediator.Send(
53+
new UploadCratePod.UploadCratePodCommand(
54+
ensureResult.Value.Id,
55+
command.SubmissionRole,
56+
command.Quantity,
57+
command.Notes,
58+
command.FileStream,
59+
command.FileName,
60+
command.ContentType,
61+
command.UserId),
62+
cancellationToken);
63+
}
64+
}

ShopInventory/Features/Crates/Queries/GetCrateTransactions/GetCrateTransactionsHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task<ErrorOr<List<CrateTransactionDto>>> Handle(
3838
if (string.Equals(currentUser.Role, CrateTrackingConstants.SubmissionRoleDriver, StringComparison.OrdinalIgnoreCase))
3939
{
4040
query = query.Where(t =>
41-
string.Equals(t.TransactionType, CrateTrackingConstants.TransactionTypeInvoice, StringComparison.OrdinalIgnoreCase) &&
41+
EF.Functions.ILike(t.TransactionType, CrateTrackingConstants.TransactionTypeInvoice) &&
4242
(!t.PodSubmissions.Any(s => s.SubmissionRole == CrateTrackingConstants.SubmissionRoleDriver) ||
4343
t.PodSubmissions.Any(s => s.SubmissionRole == CrateTrackingConstants.SubmissionRoleDriver && s.SubmittedByUserId == request.UserId)));
4444
}

ShopInventory/Features/Crates/Queries/ValidateBulkCratePods/ValidateBulkCratePodsHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public async Task<ErrorOr<BulkCratePodValidationResponseDto>> Handle(
5050
.ThenInclude(submission => submission.SubmittedByUser)
5151
.Include(transaction => transaction.Grv)
5252
.Where(transaction =>
53-
string.Equals(transaction.TransactionType, CrateTrackingConstants.TransactionTypeInvoice, StringComparison.OrdinalIgnoreCase) &&
53+
EF.Functions.ILike(transaction.TransactionType, CrateTrackingConstants.TransactionTypeInvoice) &&
5454
transaction.InvoiceDocNum.HasValue &&
5555
requestedDocNums.Contains(transaction.InvoiceDocNum.Value))
5656
.OrderByDescending(transaction => transaction.EffectiveDate)

ShopInventory/Services/SAPServiceLayerClient.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10651,6 +10651,15 @@ FROM ORDR T0
1065110651
if (!response.IsSuccessStatusCode)
1065210652
{
1065310653
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
10654+
if (IsSapNoMatchingRecordsError(response.StatusCode, errorContent))
10655+
{
10656+
_logger.LogDebug(
10657+
"Sales order with U_OrderNumber '{OrderNumber}' was not found in SAP during duplicate check",
10658+
orderNumber);
10659+
10660+
return null;
10661+
}
10662+
1065410663
_logger.LogError("Failed to check sales order by T0.\"U_OrderNumber\" '{OrderNumber}': {StatusCode} - {Error}", orderNumber, response.StatusCode, errorContent);
1065510664
throw new Exception($"Failed to check sales order by T0.\"U_OrderNumber\": {response.StatusCode} - {errorContent}");
1065610665
}
@@ -12531,6 +12540,20 @@ private static bool IsBusinessPartnerDataError(string errorContent)
1253112540
errorContent.Contains("Partner", StringComparison.OrdinalIgnoreCase)));
1253212541
}
1253312542

12543+
private static bool IsSapNoMatchingRecordsError(HttpStatusCode statusCode, string errorContent)
12544+
{
12545+
if (statusCode != HttpStatusCode.NotFound)
12546+
{
12547+
return false;
12548+
}
12549+
12550+
var sapError = ExtractSAPErrorMessage(errorContent);
12551+
return errorContent.Contains("-2028", StringComparison.OrdinalIgnoreCase)
12552+
&& (errorContent.Contains("No matching records found", StringComparison.OrdinalIgnoreCase)
12553+
|| (!string.IsNullOrWhiteSpace(sapError)
12554+
&& sapError.Contains("No matching records found", StringComparison.OrdinalIgnoreCase)));
12555+
}
12556+
1253412557
private static string? ExtractSAPErrorMessage(string errorContent)
1253512558
{
1253612559
try

0 commit comments

Comments
 (0)