diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 1efe750fc..c106ffff2 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -49,6 +49,7 @@ import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-up import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module"; import { ExchangeSettingsModule } from "./modules/exchange-settings/exchange-settings.module"; import { StampSettingsModule } from "./modules/stamp-settings/stamp-settings.module"; +import { LogoSettingsModule } from "./modules/logo-settings/logo-settings.module"; import { ContractTemplatesModule } from "./modules/contract-templates/contract-templates.module"; import { SupportContentModule } from "./modules/support-content/support-content.module"; import { OtpModule } from "./modules/otp/otp.module"; @@ -209,6 +210,7 @@ if (!process.env.APPLICATION_NAME) { DropdownSettingsModule, ExchangeSettingsModule, StampSettingsModule, + LogoSettingsModule, ContractTemplatesModule, SupportContentModule, OtpModule, diff --git a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts index 94a6809e6..517934bb9 100644 --- a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts +++ b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts @@ -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 }; diff --git a/apps/edr-freight-api/src/contracts/templates/_partials/styles.hbs b/apps/edr-freight-api/src/contracts/templates/_partials/styles.hbs index d9bc9927f..09bf3031e 100644 --- a/apps/edr-freight-api/src/contracts/templates/_partials/styles.hbs +++ b/apps/edr-freight-api/src/contracts/templates/_partials/styles.hbs @@ -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; diff --git a/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs b/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs index 6ba7c1610..81631e5d2 100644 --- a/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs +++ b/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs @@ -11,7 +11,7 @@ {{!-- ─────────────────────────── Cover page ─────────────────────────── --}}
-
EDR
+
{{#if logoImageUrl}}Company logo{{else}}EDR{{/if}}

Ethio-Djibouti Standard Gauge Railway Share Company

Freight Transport Services

diff --git a/apps/edr-freight-api/src/contracts/templates/generic.hbs b/apps/edr-freight-api/src/contracts/templates/generic.hbs index 75f795bce..f576c99ce 100644 --- a/apps/edr-freight-api/src/contracts/templates/generic.hbs +++ b/apps/edr-freight-api/src/contracts/templates/generic.hbs @@ -9,7 +9,7 @@
-
EDR
+
{{#if logoImageUrl}}Company logo{{else}}EDR{{/if}}

Ethio-Djibouti Standard Gauge Railway Share Company

Freight Transport Contract

diff --git a/apps/edr-freight-api/src/contracts/templates/last-mile.hbs b/apps/edr-freight-api/src/contracts/templates/last-mile.hbs index 9437bb18b..0cfef51d5 100644 --- a/apps/edr-freight-api/src/contracts/templates/last-mile.hbs +++ b/apps/edr-freight-api/src/contracts/templates/last-mile.hbs @@ -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 @@
-
EDR
+
{{#if logoImageUrl}}Company logo{{else}}EDR{{/if}}

Ethio-Djibouti Standard Gauge Railway Share Company

Last-Mile Delivery Contract

diff --git a/apps/edr-freight-api/src/migrations/3500000000000-LogoSettings.ts b/apps/edr-freight-api/src/migrations/3500000000000-LogoSettings.ts new file mode 100644 index 000000000..36d70906f --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3500000000000-LogoSettings.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Single-row table holding the one company logo image stamped onto every + * generated document (see LogoSettingsService). Same single-row shape as + * stamp_settings; the app never inserts more than one row. + */ +export class LogoSettings3500000000000 implements MigrationInterface { + name = "LogoSettings3500000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS freight.logo_settings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + logo_file_id uuid REFERENCES freight.files(id), + updated_by_id uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS freight.logo_settings;`); + } +} diff --git a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts index 00e590e17..bc5250888 100644 --- a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts +++ b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts @@ -14,7 +14,7 @@ const model = (over: Partial = {}): InvoiceDocumentModel = }); describe("InvoiceDocumentService.buildHtml — EIMS QR", () => { - const service = new InvoiceDocumentService({} as never, {} as never); + const service = new InvoiceDocumentService({} as never, {} as never, {} as never); it("renders no QR block when qrImageUrl is unset", () => { const html = service.buildHtml(model()); diff --git a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts index d83b7e2f0..f6b264636 100644 --- a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts +++ b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts @@ -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 ? `
EIMS verification QRScan to verify (MoR EIMS)
` @@ -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 {
+ ${logoInner}
Ethio-Djibouti Railway S.C.

${esc(model.title)} ${model.kind === "RECEIPT" ? "Receipt" : "Invoice"}

diff --git a/apps/edr-freight-api/src/modules/billing/documents/logo-markup.util.ts b/apps/edr-freight-api/src/modules/billing/documents/logo-markup.util.ts new file mode 100644 index 000000000..f78109855 --- /dev/null +++ b/apps/edr-freight-api/src/modules/billing/documents/logo-markup.util.ts @@ -0,0 +1,35 @@ +/** + * The single decision every EDR document makes about its header logo: draw + * the one uploaded company logo when configured (LogoSettingsService), or + * render nothing — the existing "Ethio-Djibouti Railway S.C." text brand next + * to it already covers the no-logo case, so there is no text fallback here + * (contrast seal-markup.util.ts, whose seal has no text of its own). + */ + +function escapeHtml(value: unknown): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * `` markup for the header logo, or "" when unset. `logoImageUrl` is + * expected to be a data URL from LogoSettingsService.getLogoImageUrl(). + * `className` defaults to "doc-logo" — each document supplies that class's + * sizing in its own
+ ${logoMarkup(data.logoImageUrl)}
Ethio-Djibouti Railway S.C.

Warehouse Release / Exit Paper

Official gate clearance and warehouse exit authorization
@@ -5775,6 +5789,8 @@ export class WarehouseInventoryService { } | null; /** The one global company stamp; null falls back to the drawn text seal. */ stampImageUrl?: string | null; + /** The one global company logo; null renders the plain text brand. */ + logoImageUrl?: string | null; }): string { const esc = (value: unknown) => String(value ?? '-') @@ -5853,11 +5869,13 @@ export class WarehouseInventoryService { .seal::before { content: ""; position: absolute; width: 78px; height: 78px; border: 1px solid #17633a; border-radius: 999px; } .seal span { position: relative; } ${sealImageCss()} + ${logoImageCss()}
+ ${logoMarkup(data.logoImageUrl)}
Ethio-Djibouti Railway S.C.

Import Goods Handover Document

EDR to customer warehouse handover
diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index cdbffd961..f0b71a056 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -1211,6 +1211,16 @@ export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [ "edr_freight_app:settings:stamp:manage", "Manage the company stamp", ), + perm( + "b4b00004-0001-4000-8000-000000000001", + "edr_freight_app:settings:logo:view", + "View the company logo", + ), + perm( + "b4b00004-0001-4000-8000-000000000002", + "edr_freight_app:settings:logo:manage", + "Manage the company logo", + ), // The per-officer approval teeter (ማህተም) — an individual's own stamp + // signature, not the company seal. It used to ride on settings:stamp:*, which // now gates the ONE company stamp; this key was split out when the two were @@ -1935,6 +1945,12 @@ export const FREIGHT_PERMS = { view: "edr_freight_app:settings:stamp:view", manage: "edr_freight_app:settings:stamp:manage", }, + // The ONE company logo, applied to every generated document (invoices, + // receipts, contracts, warehouse papers, train-scheduling manifests). + logo: { + view: "edr_freight_app:settings:logo:view", + manage: "edr_freight_app:settings:logo:manage", + }, // The per-officer approval teeter (ማህተም) + signature — genuinely per-person, // and NOT the company seal above. Retired: `invoiceStamp`, which used to // gate the company stamp before the two were untangled. diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 64f91da72..37795b2f2 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -52,6 +52,7 @@ import NoAccessPage from "./pages/NoAccessPage"; import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage"; import CompanyStampSettingsPage from "./pages/settings/CompanyStampSettingsPage"; +import LogoSettingsPage from "./pages/settings/LogoSettingsPage"; import ContractTemplatesPage from "./pages/contract_templates/ContractTemplatesPage"; import PortalContentPage from "./pages/portal_content/PortalContentPage"; import ContractTemplateEditorPage from "./pages/contract_templates/ContractTemplateEditorPage"; @@ -807,6 +808,15 @@ const App = () => { path="invoice-stamp-settings" element={} /> + {/* The ONE company logo, shown in the header of every generated document. */} + + + + } + /> void; + label?: string; + description?: string; +} + +/** + * Company logo picker — reads the picked image straight into a data URL, + * same transport as {@link StampUpload}. Kept as its own component (not a + * generalized image-upload) matching how stamp/teeter are already separate + * files here despite the near-identical shape. + */ +export function LogoUpload({ + value, + onChange, + label = "Company logo", + description = "Attach the official company logo.", +}: LogoUploadProps) { + const inputRef = useRef(null); + const [dragging, setDragging] = useState(false); + const [error, setError] = useState(null); + const [fileName, setFileName] = useState(null); + + const readFile = (file: File | undefined | null) => { + if (!file) return; + if (!file.type.startsWith("image/")) { + setError("The logo must be an image file (PNG or JPG)."); + return; + } + if (file.size > MAX_LOGO_MB * 1024 * 1024) { + setError(`The logo image must be under ${MAX_LOGO_MB} MB.`); + return; + } + const reader = new FileReader(); + reader.onload = () => { + setError(null); + setFileName(file.name); + onChange(typeof reader.result === "string" ? reader.result : null); + }; + reader.onerror = () => setError("Could not read that file. Try another."); + reader.readAsDataURL(file); + }; + + const openPicker = () => inputRef.current?.click(); + + const clear = () => { + setFileName(null); + setError(null); + onChange(null); + if (inputRef.current) inputRef.current.value = ""; + }; + + return ( + + + {label} + + + readFile(e.currentTarget.files?.[0])} + /> + + {value ? ( + + + + Company logo + + + + {fileName ?? "Logo attached"} + + + Shown in the header of every generated document. + + + + + + + + + ) : ( + { + e.preventDefault(); + setDragging(true); + }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setDragging(false); + readFile(e.dataTransfer.files?.[0]); + }} + style={{ + borderColor: dragging + ? "var(--mantine-color-edr-green-6)" + : undefined, + borderStyle: "dashed", + backgroundColor: dragging + ? "var(--mantine-color-edr-green-0)" + : undefined, + cursor: "pointer", + }} + > + + + + Upload company logo + + + {description} Drop an image here or click to browse — PNG or JPG, + up to {MAX_LOGO_MB} MB. + + + + )} + + {error && ( + + {error} + + )} + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx index 95608b6db..08d81da5a 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx @@ -9,6 +9,7 @@ import { FileText, Hammer, History, + Image as ImageIcon, Landmark, LayoutDashboard, LayoutGrid, @@ -495,6 +496,12 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] icon: , permission: FREIGHT_PERMS.settings.stamp.view, }, + { + label: "Company logo", + href: "/dashboard/logo-settings", + icon: , + permission: FREIGHT_PERMS.settings.logo.view, + }, { label: "Contract templates", href: "/dashboard/contract-templates", diff --git a/apps/edr-freight-web/backoffice/src/hooks/useLogoSettings.ts b/apps/edr-freight-web/backoffice/src/hooks/useLogoSettings.ts new file mode 100644 index 000000000..662906f42 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useLogoSettings.ts @@ -0,0 +1,45 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { logoSettingsService } from "@/services/logoSettings.service"; +import { useErrorHandler } from "@/shared/hooks/useErrorHandler"; + +const QUERY_KEY = ["logoSettings"]; + +export const useLogoSettingsQuery = () => + useQuery({ + queryKey: QUERY_KEY, + queryFn: () => logoSettingsService.get(), + }); + +export const useSetLogo = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + const { handleError } = useErrorHandler(t); + + return useMutation({ + mutationFn: (logoImageBase64: string) => + logoSettingsService.set(logoImageBase64), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEY }); + toast.success(t("logoSettings.updated", "Company logo updated")); + }, + onError: handleError, + }); +}; + +export const useClearLogo = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + const { handleError } = useErrorHandler(t); + + return useMutation({ + mutationFn: () => logoSettingsService.clear(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEY }); + toast.success(t("logoSettings.cleared", "Company logo removed")); + }, + onError: handleError, + }); +}; diff --git a/apps/edr-freight-web/backoffice/src/lib/permissions.ts b/apps/edr-freight-web/backoffice/src/lib/permissions.ts index c69aa5d1b..06f1f45da 100644 --- a/apps/edr-freight-web/backoffice/src/lib/permissions.ts +++ b/apps/edr-freight-web/backoffice/src/lib/permissions.ts @@ -334,6 +334,12 @@ export const FREIGHT_PERMS = { view: "edr_freight_app:settings:stamp:view", manage: "edr_freight_app:settings:stamp:manage", }, + // The ONE company logo, applied to every generated document (invoices, + // receipts, contracts, warehouse papers, train-scheduling manifests). + logo: { + view: "edr_freight_app:settings:logo:view", + manage: "edr_freight_app:settings:logo:manage", + }, // The per-officer approval teeter (ማህተም) + signature — genuinely per-person, // and NOT the company seal above. Retired: `invoiceStamp`, which used to // gate the company stamp before the two were untangled. diff --git a/apps/edr-freight-web/backoffice/src/pages/settings/LogoSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/settings/LogoSettingsPage.tsx new file mode 100644 index 000000000..d2f09cd48 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/settings/LogoSettingsPage.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/common/ui/card"; +import { Button } from "@/shared/common/ui/button"; +import { Save, Trash2 } from "lucide-react"; + +import { LogoUpload } from "@/components/contracts/LogoUpload"; +import { + useClearLogo, + useSetLogo, + useLogoSettingsQuery, +} from "@/hooks/useLogoSettings"; + +/** + * The ONE company logo, read by every document path server-side via + * LogoSettingsService: invoices and receipts, contract cover pages, warehouse + * GRN/release/handover papers, train-scheduling manifests, and the payment + * receipt. Single global image — no per-document choice. + */ +export default function LogoSettingsPage() { + const { data, isLoading } = useLogoSettingsQuery(); + const setLogo = useSetLogo(); + const clearLogo = useClearLogo(); + const [draft, setDraft] = useState(null); + + useEffect(() => { + setDraft(null); + }, [data?.logoImageUrl]); + + const value = draft !== null ? draft : (data?.logoImageUrl ?? null); + const dirty = draft !== null && draft !== data?.logoImageUrl; + + const handleSave = async () => { + if (!draft) return; + await setLogo.mutateAsync(draft); + }; + + const handleClear = async () => { + if (!data?.logoImageUrl) return; + await clearLogo.mutateAsync(); + }; + + return ( +
+ + + Company logo + + The single EDR logo, applied to every generated document — + invoices and receipts, contracts, warehouse papers, train-scheduling + manifests, and payment receipts. Replacing it here changes it + everywhere at once. + + + + + +
+ + {data?.logoImageUrl && !dirty && ( + + )} +
+
+
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/services/logoSettings.service.ts b/apps/edr-freight-web/backoffice/src/services/logoSettings.service.ts new file mode 100644 index 000000000..fd89f2ed3 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/logoSettings.service.ts @@ -0,0 +1,31 @@ +import { api as client } from "../auth/http"; +import { unwrap } from "@/utils/endpoint"; +import type { ApiResponse } from "@/types/apiResponse"; + +const BASE = "/logo-settings"; + +/** Company logo used on every generated document. */ +export interface LogoSettings { + logoImageUrl: string | null; + updatedById: string | null; + updatedAt: string | null; +} + +export const logoSettingsService = { + get: async (): Promise => { + const response = await client.get>(BASE); + return unwrap(response.data); + }, + + set: async (logoImageBase64: string): Promise => { + const response = await client.put>(BASE, { + logoImageBase64, + }); + return unwrap(response.data); + }, + + clear: async (): Promise => { + const response = await client.delete>(BASE); + return unwrap(response.data); + }, +};