feat(report): visitors by museum, avg ticket price, chart label fix, VAT indicator
Deploy HiHala Dashboard / deploy (push) Successful in 10s
Deploy HiHala Dashboard / deploy (push) Successful in 10s
This commit is contained in:
@@ -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 && (
|
||||
<View style={S.sectionGap}>
|
||||
<SectionHeading title={T.keyMetrics} color={color} />
|
||||
<SectionHeading title={`${T.keyMetrics} — ${cfg.includeVAT ? T.inclVAT : T.exclVAT}`} color={color} />
|
||||
<View style={S.metricsTable}>
|
||||
<View style={S.metricsHeaderRow}>
|
||||
<Text style={S.metricsHeaderLabel}> </Text>
|
||||
@@ -207,13 +213,22 @@ export function ReportDocument({ data }: Props) {
|
||||
|
||||
{cfg.showMuseumBreakdown && museumBreakdown.length > 0 && (
|
||||
<View style={S.sectionGap}>
|
||||
<SectionHeading title={T.byMuseum} color={color} />
|
||||
<SectionHeading title={T.byMuseumRevenue} color={color} />
|
||||
<View style={S.chartWrap}>
|
||||
<PdfHBarChart items={museumBreakdown} color={color} width={460} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{cfg.showMuseumBreakdown && museumVisitorBreakdown.length > 0 && (
|
||||
<View style={S.sectionGap}>
|
||||
<SectionHeading title={T.byMuseumVisitors} color={color} />
|
||||
<View style={S.chartWrap}>
|
||||
<PdfHBarChart items={museumVisitorBreakdown} color={color} width={460} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{cfg.showChannelBreakdown && channelBreakdown.length > 0 && (
|
||||
<View style={S.sectionGap}>
|
||||
<SectionHeading title={T.byChannel} color={color} />
|
||||
@@ -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: 'معدل استيعاب الحجاج',
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<SvgText key={label}
|
||||
x={sx(origIdx).toFixed(1)} y={height - 1}
|
||||
fill="#94a3b8"
|
||||
textAnchor="middle"
|
||||
style={{ fontSize: 7 }}>
|
||||
x={sx(origIdx).toFixed(1)} y={height - 5}
|
||||
style={{ fontSize: 7, fill: '#94a3b8', textAnchor: 'middle' }}>
|
||||
{label}
|
||||
</SvgText>
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user