From 648365348f9d891028b31939cecc9bb3a91d1113 Mon Sep 17 00:00:00 2001 From: fahed Date: Tue, 28 Apr 2026 14:59:24 +0300 Subject: [PATCH] feat(report): visitors by museum, avg ticket price, chart label fix, VAT indicator --- src/components/Report/ReportDocument.tsx | 33 ++++++++++++++++++++---- src/components/Report/reportCharts.tsx | 8 +++--- src/components/Report/reportHelpers.ts | 9 ++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/Report/ReportDocument.tsx b/src/components/Report/ReportDocument.tsx index e7a50f0..554ccab 100644 --- a/src/components/Report/ReportDocument.tsx +++ b/src/components/Report/ReportDocument.tsx @@ -88,7 +88,7 @@ interface Props { data: ReportData; } export function ReportDocument({ data }: Props) { const { config: cfg, metrics, prevMetrics, trendLabels, trendCurrent, trendPrevious, - museumBreakdown, channelBreakdown, pilgrimCapture, generatedAt } = data; + museumBreakdown, museumVisitorBreakdown, channelBreakdown, pilgrimCapture, generatedAt } = data; const lang = cfg.language; const color = cfg.accentColor; @@ -96,6 +96,9 @@ export function ReportDocument({ data }: Props) { const orientation = cfg.orientation === 'landscape' ? 'landscape' : 'portrait'; const T = lang === 'en' ? LABELS_EN : LABELS_AR; + const avgTicketPrice = metrics.tickets > 0 ? metrics.revenue / metrics.tickets : 0; + const prevAvgTicketPrice = prevMetrics && prevMetrics.tickets > 0 ? prevMetrics.revenue / prevMetrics.tickets : null; + const metricsRows = [ { label: T.revenue, curr: formatCurrency(metrics.revenue, cfg.includeVAT), prev: prevMetrics ? formatCurrency(prevMetrics.revenue, cfg.includeVAT) : null, @@ -109,6 +112,9 @@ export function ReportDocument({ data }: Props) { { label: T.avgRev, curr: formatCurrency(metrics.avgRevPerVisitor, false), prev: prevMetrics ? formatCurrency(prevMetrics.avgRevPerVisitor, false) : null, chg: prevMetrics ? pctChange(metrics.avgRevPerVisitor, prevMetrics.avgRevPerVisitor) : null }, + { label: T.avgTicketPrice, curr: formatCurrency(avgTicketPrice, false), + prev: prevAvgTicketPrice !== null ? formatCurrency(prevAvgTicketPrice, false) : null, + chg: prevAvgTicketPrice !== null ? pctChange(avgTicketPrice, prevAvgTicketPrice) : null }, ...(cfg.showPilgrimCapture && pilgrimCapture ? [{ label: T.capture, curr: `${pilgrimCapture.current}%`, prev: pilgrimCapture.previous !== null ? `${pilgrimCapture.previous}%` : null, @@ -152,7 +158,7 @@ export function ReportDocument({ data }: Props) { {cfg.showMetricsTable && ( - + @@ -207,13 +213,22 @@ export function ReportDocument({ data }: Props) { {cfg.showMuseumBreakdown && museumBreakdown.length > 0 && ( - + )} + {cfg.showMuseumBreakdown && museumVisitorBreakdown.length > 0 && ( + + + + + + + )} + {cfg.showChannelBreakdown && channelBreakdown.length > 0 && ( @@ -238,14 +253,18 @@ const LABELS_EN = { generated: 'Generated', execSummary: 'Executive Summary', keyMetrics: 'Key Metrics', + inclVAT: 'Incl. VAT', + exclVAT: 'Excl. VAT', change: 'vs Prior Year', trend: 'Revenue Trend', - byMuseum: 'Revenue by Museum', + byMuseumRevenue: 'Revenue by Museum', + byMuseumVisitors: 'Visitors by Museum', byChannel: 'Visitors by Channel', revenue: 'Revenue', visitors: 'Visitors', tickets: 'Tickets', avgRev: 'Avg Rev / Visitor', + avgTicketPrice: 'Avg Ticket Price', capture: 'Pilgrim Capture Rate', }; @@ -256,13 +275,17 @@ const LABELS_AR = { generated: 'تاريخ الإصدار', execSummary: 'الملخص التنفيذي', keyMetrics: 'المؤشرات الرئيسية', + inclVAT: 'شامل ضريبة القيمة المضافة', + exclVAT: 'غير شامل ضريبة القيمة المضافة', change: 'مقابل العام السابق', trend: 'اتجاه الإيرادات', - byMuseum: 'الإيرادات حسب المتحف', + byMuseumRevenue: 'الإيرادات حسب المتحف', + byMuseumVisitors: 'الزوار حسب المتحف', byChannel: 'الزوار حسب القناة', revenue: 'الإيرادات', visitors: 'الزوار', tickets: 'التذاكر', avgRev: 'متوسط الإيراد / زائر', + avgTicketPrice: 'متوسط سعر التذكرة', capture: 'معدل استيعاب الحجاج', }; diff --git a/src/components/Report/reportCharts.tsx b/src/components/Report/reportCharts.tsx index c71dbd2..c97e3e0 100644 --- a/src/components/Report/reportCharts.tsx +++ b/src/components/Report/reportCharts.tsx @@ -13,7 +13,7 @@ interface TrendChartProps { export function PdfTrendChart({ labels, current, previous, color, width = 460, height = 140 }: TrendChartProps) { const allValues = [...current, ...(previous ?? [])].filter(v => v > 0); const max = allValues.length > 0 ? Math.max(...allValues) : 1; - const padL = 8, padR = 8, padT = 8, padB = 8; + const padL = 8, padR = 8, padT = 8, padB = 18; // padB=18 leaves room for x-axis labels const w = width - padL - padR; const h = height - padT - padB; @@ -47,10 +47,8 @@ export function PdfTrendChart({ labels, current, previous, color, width = 460, h const origIdx = labels.indexOf(label); return ( + x={sx(origIdx).toFixed(1)} y={height - 5} + style={{ fontSize: 7, fill: '#94a3b8', textAnchor: 'middle' }}> {label} ); diff --git a/src/components/Report/reportHelpers.ts b/src/components/Report/reportHelpers.ts index da7edbc..055cdc4 100644 --- a/src/components/Report/reportHelpers.ts +++ b/src/components/Report/reportHelpers.ts @@ -59,7 +59,8 @@ export interface ReportData { trendLabels: string[]; trendCurrent: number[]; trendPrevious: number[] | null; - museumBreakdown: BreakdownItem[]; + museumBreakdown: BreakdownItem[]; // revenue by museum + museumVisitorBreakdown: BreakdownItem[]; // visitors by museum channelBreakdown: BreakdownItem[]; pilgrimCapture: { current: number; previous: number | null } | null; generatedAt: string; @@ -137,6 +138,11 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R .sort((a, b) => b.value - a.value) .slice(0, 10); + const museumVisitorBreakdown: BreakdownItem[] = Object.entries(musG) + .map(([name, g]) => ({ name, value: g.visitors })) + .sort((a, b) => b.value - a.value) + .slice(0, 10); + const chanG = groupByChannel(currRows, cfg.includeVAT); const channelBreakdown: BreakdownItem[] = Object.entries(chanG) .map(([name, g]) => ({ name, value: g.visitors })) @@ -161,6 +167,7 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R trendCurrent, trendPrevious, museumBreakdown, + museumVisitorBreakdown, channelBreakdown, pilgrimCapture, generatedAt: new Date().toLocaleDateString('en-GB'),