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

@@ -10,6 +10,7 @@ import { ContractPricingScheduleBuilder, PricingSchedule } from './contract-pric
import { ContractRateScheduleBuilder, RateSchedule } from './contract-rate-schedule.builder';
import { ContractTemplateResolver } from './contract-template.resolver';
import { StampSettingsService } from '../modules/stamp-settings/stamp-settings.service';
import { LogoSettingsService } from '../modules/logo-settings/logo-settings.service';
import { ContractTemplateMeta, getTemplateMeta } from './contract-template.registry';
export interface ContractSignatureView {
@@ -111,6 +112,8 @@ export interface ContractViewModel {
hasCustomerSignature: boolean;
hasStaffSignature: boolean;
dynamicTemplate?: ContractDynamicTemplateView;
/** Company logo for the cover-page header (LogoSettingsService); null renders the "EDR" mark. */
logoImageUrl?: string | null;
}
@Injectable()
@@ -121,6 +124,7 @@ export class ContractViewModelBuilder {
private readonly pricingBuilder: ContractPricingScheduleBuilder,
private readonly rateScheduleBuilder: ContractRateScheduleBuilder,
private readonly stampSettings: StampSettingsService,
private readonly logoSettings: LogoSettingsService,
) {}
async build(bookingId: string): Promise<{ booking: Booking; view: ContractViewModel }> {
@@ -138,6 +142,7 @@ export class ContractViewModelBuilder {
template.freight,
);
const signatures = await this.loadSignatures(bookingId);
const logoImageUrl = await this.logoSettings.getLogoImageUrl();
const hasCustomer = signatures.some((s) => s.role === 'CUSTOMER');
const hasStaff = signatures.some((s) => s.role === 'STAFF');
@@ -194,6 +199,7 @@ export class ContractViewModelBuilder {
hasContractDocument: hasContractFile,
hasCustomerSignature: hasCustomer,
hasStaffSignature: hasStaff,
logoImageUrl,
};
return { booking, view };

View File

@@ -77,6 +77,12 @@
letter-spacing: 0.08em;
width: 72px;
}
.logo-mark img {
display: block;
max-height: 100%;
max-width: 100%;
object-fit: contain;
}
.kicker {
color: #0e5b45;
font-family: Arial, sans-serif;

View File

@@ -11,7 +11,7 @@
{{!-- ─────────────────────────── Cover page ─────────────────────────── --}}
<section class="cover page-section">
<div class="brand-row">
<div class="logo-mark">EDR</div>
<div class="logo-mark">{{#if logoImageUrl}}<img src="{{logoImageUrl}}" alt="Company logo" />{{else}}EDR{{/if}}</div>
<div>
<p class="kicker">Ethio-Djibouti Standard Gauge Railway Share Company</p>
<p class="muted">Freight Transport Services</p>

View File

@@ -9,7 +9,7 @@
<main class="contract">
<section class="cover page-section">
<div class="brand-row">
<div class="logo-mark">EDR</div>
<div class="logo-mark">{{#if logoImageUrl}}<img src="{{logoImageUrl}}" alt="Company logo" />{{else}}EDR{{/if}}</div>
<div>
<p class="kicker">Ethio-Djibouti Standard Gauge Railway Share Company</p>
<p class="muted">Freight Transport Contract</p>

View File

@@ -9,6 +9,7 @@
main { padding: 32px 40px; }
.brand-row { display: flex; align-items: center; gap: 14px; border-bottom: 3px solid #1a5632; padding-bottom: 14px; }
.logo-mark { background: #1a5632; color: #fff; font-weight: 700; font-size: 18px; padding: 10px 14px; border-radius: 6px; }
.logo-mark img { display: block; max-height: 32px; max-width: 100px; object-fit: contain; }
.kicker { margin: 0; font-weight: 700; }
.muted { margin: 0; color: #666; }
h1 { font-size: 20px; margin: 24px 0 4px; }
@@ -32,7 +33,7 @@
<body>
<main>
<div class="brand-row">
<div class="logo-mark">EDR</div>
<div class="logo-mark">{{#if logoImageUrl}}<img src="{{logoImageUrl}}" alt="Company logo" />{{else}}EDR{{/if}}</div>
<div>
<p class="kicker">Ethio-Djibouti Standard Gauge Railway Share Company</p>
<p class="muted">Last-Mile Delivery Contract</p>