Render invoice-editor images (mfr portal only) into the PDF

The invoice line editor already shows <img> pictures pulled from the mfr
portal, but the PDF pipeline's HTML-to-MigraDoc converter had no <img>
handler, so they never appeared on the actual invoice.

InvoiceService now fetches only https://portal.mobilefieldreport.com/mfr/*
image sources before rendering (size/type/count capped, failures logged and
skipped) and hands the resolved images down through ApplyInvoice into the
HTML converter, which renders them inline at their original HTML position
and size (px converted to cm, capped against layout overflow). See ADR 0018.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 19:36:31 +02:00
co-authored by Claude Sonnet 5
parent 1a8a14791d
commit 071982caec
15 changed files with 516 additions and 28 deletions
@@ -75,7 +75,7 @@ public partial class IntranetController
private async Task<IActionResult> HandleDraftPreview(string fn, string id, string code)
{
if (!HasForm("token")) return BadRequest400();
var doc = _invoiceDrafts.RenderPreview(Form("token"));
var doc = await _invoiceDrafts.RenderPreviewAsync(Form("token"));
if (doc == null) return DraftGone();
var imgcol = await _pdf.DocToImageCollectionAsync(doc);
return await JSONAsync(new { img = imgcol.ImgB64Array, total = imgcol.TotalPages });
@@ -75,7 +75,7 @@ public partial class IntranetController
{
_logger.LogInformation("Requests sprep: invoice draft {InvId} created, user={User}", fdInv.Id, UserAccountID);
await _events.InvoiceDraftRegisteredAsync(fdInv, changed: false, userAccountId: UserAccountID);
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
var imgcol = await _pdf.DocToImageCollectionAsync(await _invoices.GenerateInvoicePdfAsync(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht erstellt werden.");
@@ -92,7 +92,7 @@ public partial class IntranetController
{
_logger.LogInformation("Requests sedit: invoice draft {InvId} updated, user={User}", fdInv.Id, UserAccountID);
await _events.InvoiceDraftRegisteredAsync(fdInv, changed: true, userAccountId: UserAccountID);
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
var imgcol = await _pdf.DocToImageCollectionAsync(await _invoices.GenerateInvoicePdfAsync(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht aktualisiert werden.");
@@ -396,7 +396,7 @@ public partial class IntranetController
{
byte[]? ct = Form("create", "0") != "1"
? await _invoices.GetInvoiceFileAsync(fdInv, fdInv.IsDraft, _mfr) is { Length: > 0 } f1 ? f1 : await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec)
: _pdf.DocToPdfBytes(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
: _pdf.DocToPdfBytes(await _invoices.GenerateInvoicePdfAsync(fdInv, fdInv.IsDraft));
if (ct == null)
return await InvoiceIssueResult("Die Rechnungs-PDF konnte aufgrund eines Fehlers nicht erstellt werden.", fdInv.Id);
// Serve inline for the in-browser viewer, but carry the real DocumentName so the browser's
@@ -405,7 +405,7 @@ public partial class IntranetController
SetInlinePdfFilename(filename);
return File(ct, "application/pdf");
}
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
var imgcol = await _pdf.DocToImageCollectionAsync(await _invoices.GenerateInvoicePdfAsync(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}