From 8998f68c0f3d61b9fcd078a708b69b13d3b6183d Mon Sep 17 00:00:00 2001
From: Matt
Date: Mon, 25 May 2026 17:18:00 +0200
Subject: [PATCH] feat(reports-p7): cover-page brand picker (admin-only)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- DashboardReportBuilder grows an optional Cover-page brand picker
surfaced only when can('admin', 'manage_settings') AND the user has
access to >1 port. Pulls ports from PortContext; default option is
"Use active port brand", remaining options are the other ports the
user can reach. Choice persists in config.coverBrandPortId; threaded
through preview, download (/reports/generate), and queue
(/reports/runs) payloads.
- render-report.service.ts: when run.config.coverBrandPortId resolves
to an accessible port, the cover-page logo + portName come from THAT
port's brand kit. Falls back to the source port silently when the
override port is missing or stale. Source-port DATA stays — only the
cover branding swaps. Useful for cross-port leadership decks.
Verified: tsc clean, 1493/1493 vitest.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../builders/dashboard-report-builder.tsx | 53 ++++++++++++++++++-
src/lib/services/report-render.service.ts | 21 ++++++--
2 files changed, 70 insertions(+), 4 deletions(-)
diff --git a/src/components/reports/builders/dashboard-report-builder.tsx b/src/components/reports/builders/dashboard-report-builder.tsx
index e344b5b0..751b1265 100644
--- a/src/components/reports/builders/dashboard-report-builder.tsx
+++ b/src/components/reports/builders/dashboard-report-builder.tsx
@@ -11,6 +11,15 @@ import { DatePicker } from '@/components/ui/date-picker';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { usePermissions } from '@/hooks/use-permissions';
+import { usePortContext } from '@/providers/port-provider';
import {
PDF_DASHBOARD_WIDGETS,
PDF_DASHBOARD_CATEGORY_LABELS,
@@ -60,10 +69,17 @@ export function DashboardReportBuilder({ portSlug, initialFrom, initialTo }: Pro
const [dateFrom, setDateFrom] = useState(initialFrom ?? toIsoLocal(last30));
const [dateTo, setDateTo] = useState(initialTo ?? toIsoLocal(today));
const [outputFormat, setOutputFormat] = useState<'pdf' | 'csv'>('pdf');
+ const [coverBrandPortId, setCoverBrandPortId] = useState('');
const [loading, setLoading] = useState(false);
const [enqueuing, setEnqueuing] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
+ // P7: cover-brand swap — admin-only. The renderer falls back to the
+ // active port's brand kit when this is empty or invalid.
+ const { can } = usePermissions();
+ const { ports, currentPortId } = usePortContext();
+ const canPickBrand = can('admin', 'manage_settings') && ports.length > 1;
+
const previewPayload = useMemo(
() => ({
title: title.trim() || 'Report',
@@ -71,11 +87,12 @@ export function DashboardReportBuilder({ portSlug, initialFrom, initialTo }: Pro
kind: 'dashboard' as const,
widgetIds: selected,
...(subtitle.trim() ? { subtitle: subtitle.trim() } : {}),
+ ...(coverBrandPortId ? { coverBrandPortId } : {}),
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {}),
},
}),
- [title, subtitle, selected, dateFrom, dateTo],
+ [title, subtitle, selected, coverBrandPortId, dateFrom, dateTo],
);
function toggle(id: PdfDashboardWidgetId) {
@@ -105,6 +122,7 @@ export function DashboardReportBuilder({ portSlug, initialFrom, initialTo }: Pro
kind: 'dashboard',
widgetIds: selected,
...(subtitle.trim() ? { subtitle: subtitle.trim() } : {}),
+ ...(coverBrandPortId ? { coverBrandPortId } : {}),
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {}),
},
@@ -147,6 +165,7 @@ export function DashboardReportBuilder({ portSlug, initialFrom, initialTo }: Pro
widgetIds: selected,
title: title.trim() || 'Report',
...(subtitle.trim() ? { subtitle: subtitle.trim() } : {}),
+ ...(coverBrandPortId ? { coverBrandPortId } : {}),
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {}),
},
@@ -266,6 +285,38 @@ export function DashboardReportBuilder({ portSlug, initialFrom, initialTo }: Pro
Sections marked “needs date range” only render when both dates are set.
+ {canPickBrand ? (
+
+
+
+
+ Swaps the cover-page logo and port name to the picked brand. The data inside stays
+ from the active port.
+
+
+ ) : null}
diff --git a/src/lib/services/report-render.service.ts b/src/lib/services/report-render.service.ts
index ac204126..f7d458c6 100644
--- a/src/lib/services/report-render.service.ts
+++ b/src/lib/services/report-render.service.ts
@@ -202,11 +202,26 @@ export async function renderReportRun(reportRunId: string): Promise {
throw new Error(`Cannot render report ${run.id}: port ${run.portId} not found`);
}
- const logo = await resolvePortLogo(run.portId).catch(() => ({
+ // P7: optional cover-brand swap. When config.coverBrandPortId points
+ // at a port the rep has access to, the cover-page logo + port name
+ // come from THAT port's brand kit instead of the report's source
+ // port. Useful for cross-port leadership decks; falls back to the
+ // source port when the override port is missing / inaccessible.
+ const params = (run.config as Record) ?? {};
+ const overrideBrandPortId =
+ typeof params.coverBrandPortId === 'string' && params.coverBrandPortId.length > 0
+ ? params.coverBrandPortId
+ : null;
+ const brandPortId = overrideBrandPortId ?? run.portId;
+ const brandPort =
+ overrideBrandPortId === null
+ ? port
+ : ((await db.query.ports.findFirst({ where: eq(ports.id, brandPortId) })) ?? port);
+
+ const logo = await resolvePortLogo(brandPort.id).catch(() => ({
buffer: null as Buffer | null,
}));
- const ctx: RenderCtx = { portName: port.name, logoBuffer: logo.buffer ?? null };
- const params = (run.config as Record) ?? {};
+ const ctx: RenderCtx = { portName: brandPort.name, logoBuffer: logo.buffer ?? null };
const data = await renderer.fetchData(run.portId, params);