feat(freight): add company logo setting applied to every generated document

New logo-settings module (mirrors stamp-settings): single uploaded logo,
stored via FilesService/MinIO, injected as a data URL into invoice/receipt,
contract, warehouse, train-scheduling, and payment-receipt PDFs. Adds a
matching backoffice settings page and settings:logo:view/manage permissions.

>
This commit is contained in:
Hagernesh
2026-08-13 10:53:42 +00:00
parent 63b40ea4f8
commit c4bf3c9479
31 changed files with 800 additions and 8 deletions

View File

@@ -1,8 +1,10 @@
import { Injectable } from "@nestjs/common";
import { StampSettingsService } from "../../stamp-settings/stamp-settings.service";
import { LogoSettingsService } from "../../logo-settings/logo-settings.service";
import { PdfRenderService } from "./pdf-render.service";
import { sealClass, sealImageCss, sealMarkup } from "./seal-markup.util";
import { logoImageCss, logoMarkup } from "./logo-markup.util";
import {
PdfColor,
assembleSinglePagePdf,
@@ -62,6 +64,7 @@ export interface InvoiceDocumentModel {
* explicitly only to override that default for one document.
*/
stampImageUrl?: string | null;
logoImageUrl?: string | null;
/**
* MoR EIMS verification QR (data URL, pre-rendered by the caller from `Invoice.eimsSignedQr` —
* see that column's comment). Set only once an invoice is actually registered; the IRN text
@@ -81,6 +84,7 @@ export class InvoiceDocumentService {
constructor(
private readonly pdf: PdfRenderService,
private readonly stampSettings: StampSettingsService,
private readonly logoSettings: LogoSettingsService,
) {}
async render(
@@ -90,7 +94,11 @@ export class InvoiceDocumentService {
model.stampImageUrl !== undefined
? model.stampImageUrl
: await this.stampSettings.getStampImageUrl();
const resolvedModel: InvoiceDocumentModel = { ...model, stampImageUrl };
const logoImageUrl =
model.logoImageUrl !== undefined
? model.logoImageUrl
: await this.logoSettings.getLogoImageUrl();
const resolvedModel: InvoiceDocumentModel = { ...model, stampImageUrl, logoImageUrl };
const html = this.buildHtml(resolvedModel);
const kindLabel = model.kind === "RECEIPT" ? "receipt" : "invoice";
@@ -250,6 +258,7 @@ export class InvoiceDocumentService {
model.sealText ?? (model.kind === "RECEIPT" || model.status === "PAID" ? "EDR PAID" : "EDR");
const sealInner = sealMarkup(model.stampImageUrl, sealText);
const sealCssClass = sealClass(model.stampImageUrl);
const logoInner = logoMarkup(model.logoImageUrl);
const qrMarkup = model.qrImageUrl
? `<div class="qr"><img src="${esc(model.qrImageUrl)}" alt="EIMS verification QR" /><span>Scan to verify (MoR EIMS)</span></div>`
@@ -293,6 +302,7 @@ export class InvoiceDocumentService {
.meta strong { display: block; color: #0f172a; font-size: 17px; margin-top: 5px; }
.seal { position: absolute; right: 28px; top: 118px; width: 116px; height: 116px; border: 4px double #0f766e; border-radius: 999px; color: #0f766e; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 18px; transform: rotate(-14deg); opacity: .82; }
${sealImageCss()}
${logoImageCss()}
.qr { position: absolute; right: 160px; top: 118px; width: 90px; text-align: center; }
.qr img { width: 90px; height: 90px; }
.qr span { display: block; font-size: 7px; color: #64748b; margin-top: 3px; }
@@ -322,6 +332,7 @@ export class InvoiceDocumentService {
<div class="doc">
<div class="top">
<div>
${logoInner}
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>${esc(model.title)} ${model.kind === "RECEIPT" ? "Receipt" : "Invoice"}</h1>
</div>