import { useState, useMemo, useRef } from 'react';
import { DollarSign, TrendingUp, Users, ShoppingCart, Wallet, Percent, Coffee, UtensilsCrossed, Download } from 'lucide-react';
import { LocationFilter } from './components/LocationFilter';
import { PeriodSelector } from './components/PeriodSelector';
import { KPICard } from './components/KPICard';
import { KPITrendChart } from './components/KPITrendChart';
import { DataTrendChart } from './components/DataTrendChart';
import {
locations,
getFinancialDataByLocationAndPeriod,
getAggregatedDataByTypeAndPeriod,
getHistoricalData,
calculateComparison,
getAvailablePeriods,
financialData
} from './data/mockData';
import { FinancialData, CoffeeBarData, CafeData, CateringData, PrivateDiningData } from './types/financial';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export default function App() {
const [selectedType, setSelectedType] = useState('coffee-bar');
const [selectedLocation, setSelectedLocation] = useState('all');
const [selectedYear, setSelectedYear] = useState(2025);
const [selectedMonth, setSelectedMonth] = useState(11);
const [selectedKPI, setSelectedKPI] = useState('participationRate');
const [selectedData, setSelectedData] = useState('totalSales');
const [isExporting, setIsExporting] = useState(false);
const contentRef = useRef(null);
const availablePeriods = useMemo(() => getAvailablePeriods(), []);
const currentData = useMemo((): FinancialData | null => {
if (selectedLocation !== 'all') {
return getFinancialDataByLocationAndPeriod(selectedLocation, selectedYear, selectedMonth) || null;
}
return getAggregatedDataByTypeAndPeriod(selectedType, selectedYear, selectedMonth);
}, [selectedType, selectedLocation, selectedYear, selectedMonth]);
const previousMonthData = useMemo((): FinancialData | null => {
let prevYear = selectedYear;
let prevMonth = selectedMonth - 1;
if (prevMonth === 0) {
prevMonth = 12;
prevYear -= 1;
}
if (selectedLocation !== 'all') {
return getFinancialDataByLocationAndPeriod(selectedLocation, prevYear, prevMonth) || null;
}
return getAggregatedDataByTypeAndPeriod(selectedType, prevYear, prevMonth);
}, [selectedType, selectedLocation, selectedYear, selectedMonth]);
const previousYearData = useMemo((): FinancialData | null => {
const prevYear = selectedYear - 1;
if (selectedLocation !== 'all') {
return getFinancialDataByLocationAndPeriod(selectedLocation, prevYear, selectedMonth) || null;
}
return getAggregatedDataByTypeAndPeriod(selectedType, prevYear, selectedMonth);
}, [selectedType, selectedLocation, selectedYear, selectedMonth]);
const previousQuarterData = useMemo((): FinancialData | null => {
let prevYear = selectedYear;
let prevMonth = selectedMonth - 3;
if (prevMonth <= 0) {
prevMonth += 12;
prevYear -= 1;
}
if (selectedLocation !== 'all') {
return getFinancialDataByLocationAndPeriod(selectedLocation, prevYear, prevMonth) || null;
}
return getAggregatedDataByTypeAndPeriod(selectedType, prevYear, prevMonth);
}, [selectedType, selectedLocation, selectedYear, selectedMonth]);
const historicalData = useMemo(() => {
return getHistoricalData(
selectedLocation !== 'all' ? selectedLocation : null,
selectedType,
12
);
}, [selectedType, selectedLocation]);
const locationNameMap = useMemo(() => {
return locations.reduce((acc, loc) => {
acc[loc.id] = loc.name;
return acc;
}, {} as { [key: string]: string });
}, []);
const getLocationTitle = () => {
if (selectedLocation !== 'all') {
const location = locations.find(loc => loc.id === selectedLocation);
return location ? location.name : 'Unknown Location';
}
const typeLabels: { [key: string]: string } = {
'all': 'All Locations',
'cafe': 'All Cafes',
'coffee-bar': 'All Coffee Bars',
'catering': 'All Catering Locations',
'private-dining': 'Private Dining',
};
return typeLabels[selectedType] || 'All Locations';
};
const calculateMoM = (getValue: (data: FinancialData) => number) => {
if (!currentData || !previousMonthData) return undefined;
const current = getValue(currentData);
const previous = getValue(previousMonthData);
return calculateComparison(current, previous);
};
const calculateYoY = (getValue: (data: FinancialData) => number) => {
if (!currentData || !previousYearData) return undefined;
const current = getValue(currentData);
const previous = getValue(previousYearData);
return calculateComparison(current, previous);
};
const calculateQoQ = (getValue: (data: FinancialData) => number) => {
if (!currentData || !previousQuarterData) return undefined;
const current = getValue(currentData);
const previous = getValue(previousQuarterData);
return calculateComparison(current, previous);
};
if (!currentData) {
return (
);
}
const locationType = currentData.type;
// Get available KPIs based on location type
const getAvailableKPIs = () => {
if (locationType === 'coffee-bar') {
return [
{ value: 'participationRate', label: 'Participation Rate' },
{ value: 'checkAverage', label: 'Check Average' },
{ value: 'productCostPerTransaction', label: 'Product Cost per Transaction' },
{ value: 'laborCostPerTransaction', label: 'Labor Cost per Transaction' },
{ value: 'overtimeCostPerTransaction', label: 'Overtime Cost per Transaction' },
{ value: 'temporaryLaborCostPerTransaction', label: 'Temporary Labor Cost per Transaction' },
{ value: 'indirectCostPerTransaction', label: 'Indirect Cost per Transaction' },
{ value: 'excessDeficitCostPerTransaction', label: 'Excess/Deficit Cost per Transaction' },
];
} else if (locationType === 'cafe') {
return [
{ value: 'breakfastCheckAverage', label: 'Breakfast Check Average' },
{ value: 'lunchCheckAverage', label: 'Lunch Check Average' },
{ value: 'breakfastParticipation', label: 'Breakfast Participation' },
{ value: 'lunchParticipation', label: 'Lunch Participation' },
{ value: 'productCostPerTransaction', label: 'Product Cost per Transaction' },
{ value: 'laborCostPerTransaction', label: 'Labor Cost per Transaction' },
{ value: 'overtimeCostPerTransaction', label: 'Overtime Cost per Transaction' },
{ value: 'temporaryLaborCostPerTransaction', label: 'Temporary Labor Cost per Transaction' },
{ value: 'indirectCostPerTransaction', label: 'Indirect Cost per Transaction' },
{ value: 'excessDeficitCostPerTransaction', label: 'Excess/Deficit Cost per Transaction' },
];
} else {
return [
{ value: 'productCostPercent', label: 'Product Cost %' },
{ value: 'laborCostPercent', label: 'Labor Cost %' },
{ value: 'overtimeCostPercent', label: 'Overtime Cost %' },
{ value: 'temporaryLaborCostPercent', label: 'Temporary Labor %' },
{ value: 'expensesCostPercent', label: 'Expenses Cost %' },
{ value: 'excessDeficitPercent', label: 'Excess/Deficit %' },
];
}
};
const availableKPIs = getAvailableKPIs();
// Get available data metrics based on location type
const getAvailableDataMetrics = () => {
if (locationType === 'coffee-bar') {
return [
{ value: 'totalSales', label: 'Total Sales' },
{ value: 'totalTransactions', label: 'Total Transactions' },
{ value: 'productCostAmount', label: 'Product Cost' },
{ value: 'laborCostAmount', label: 'Labor Cost' },
{ value: 'overtimeCostAmount', label: 'Overtime Cost' },
{ value: 'excessDeficitAmount', label: 'Excess/Deficit' },
];
} else if (locationType === 'cafe') {
return [
{ value: 'breakfastSales', label: 'Breakfast Sales' },
{ value: 'lunchSales', label: 'Lunch Sales' },
{ value: 'totalSales', label: 'Total Sales' },
{ value: 'breakfastTransactions', label: 'Breakfast Transactions' },
{ value: 'lunchTransactions', label: 'Lunch Transactions' },
{ value: 'totalTransactions', label: 'Total Transactions' },
{ value: 'productCostAmount', label: 'Product Cost' },
{ value: 'laborCostAmount', label: 'Labor Cost' },
{ value: 'excessDeficitAmount', label: 'Excess/Deficit' },
];
} else {
return [
{ value: 'totalRevenue', label: 'Total Revenue' },
{ value: 'productCostAmount', label: 'Product Cost' },
{ value: 'laborCostAmount', label: 'Labor Cost' },
{ value: 'overtimeCostAmount', label: 'Overtime Cost' },
{ value: 'temporaryLaborCostAmount', label: 'Temporary Labor Cost' },
{ value: 'expensesCostAmount', label: 'Expenses Cost' },
{ value: 'excessDeficitAmount', label: 'Excess/Deficit' },
];
}
};
const availableDataMetrics = getAvailableDataMetrics();
// Reset selected KPI when location type changes
useMemo(() => {
const kpiValues = availableKPIs.map(k => k.value);
if (!kpiValues.includes(selectedKPI)) {
setSelectedKPI(kpiValues[0]);
}
}, [locationType, selectedKPI]);
const exportPDF = async () => {
setIsExporting(true);
try {
const element = contentRef.current;
if (!element) return;
// Use html2canvas to capture the content
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#f9fafb'
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pdfWidth;
const imgHeight = (canvas.height * pdfWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
// Add first page
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pdfHeight;
// Add additional pages if content is longer than one page
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pdfHeight;
}
// Generate filename with current date and location
const dateStr = new Date().toISOString().split('T')[0];
const locationStr = getLocationTitle().replace(/\s+/g, '_');
pdf.save(`WorldBank_Dashboard_${locationStr}_${dateStr}.pdf`);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsExporting(false);
}
};
return (
No Data Available
Please select a specific location type to view KPIs.
{/* Header */}
{/* Main Content */}
{/* Filters */}
{/* Period Selector */}
{
setSelectedYear(year);
setSelectedMonth(month);
}}
availablePeriods={availablePeriods}
/>
{/* Current Selection Title */}
{/* Coffee Bar KPIs */}
{locationType === 'coffee-bar' && (
<>
(d as CoffeeBarData).participationRate)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).participationRate)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).participationRate)}
icon={ }
/>
(d as CoffeeBarData).checkAverage)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).checkAverage)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).checkAverage)}
icon={ }
/>
(d as CoffeeBarData).productCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).productCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).productCostPerTransaction)}
icon={ }
/>
(d as CoffeeBarData).laborCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).laborCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).laborCostPerTransaction)}
icon={ }
/>
(d as CoffeeBarData).overtimeCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).overtimeCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).overtimeCostPerTransaction)}
icon={ }
/>
(d as CoffeeBarData).temporaryLaborCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).temporaryLaborCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).temporaryLaborCostPerTransaction)}
icon={ }
/>
(d as CoffeeBarData).indirectCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).indirectCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).indirectCostPerTransaction)}
icon={ }
/>
(d as CoffeeBarData).excessDeficitCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CoffeeBarData).excessDeficitCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CoffeeBarData).excessDeficitCostPerTransaction)}
icon={ }
isNegativeIndicator={true}
/>
>
)}
{/* Cafe KPIs */}
{locationType === 'cafe' && (
<>
(d as CafeData).breakfastCheckAverage)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).breakfastCheckAverage)}
yearOverYear={calculateYoY((d) => (d as CafeData).breakfastCheckAverage)}
icon={ }
/>
(d as CafeData).lunchCheckAverage)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).lunchCheckAverage)}
yearOverYear={calculateYoY((d) => (d as CafeData).lunchCheckAverage)}
icon={ }
/>
(d as CafeData).breakfastParticipation)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).breakfastParticipation)}
yearOverYear={calculateYoY((d) => (d as CafeData).breakfastParticipation)}
icon={ }
/>
(d as CafeData).lunchParticipation)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).lunchParticipation)}
yearOverYear={calculateYoY((d) => (d as CafeData).lunchParticipation)}
icon={ }
/>
(d as CafeData).productCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).productCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).productCostPerTransaction)}
icon={ }
/>
(d as CafeData).laborCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).laborCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).laborCostPerTransaction)}
icon={ }
/>
(d as CafeData).overtimeCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).overtimeCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).overtimeCostPerTransaction)}
icon={ }
/>
(d as CafeData).temporaryLaborCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).temporaryLaborCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).temporaryLaborCostPerTransaction)}
icon={ }
/>
(d as CafeData).indirectCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).indirectCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).indirectCostPerTransaction)}
icon={ }
/>
(d as CafeData).excessDeficitCostPerTransaction)}
quarterOverQuarter={calculateQoQ((d) => (d as CafeData).excessDeficitCostPerTransaction)}
yearOverYear={calculateYoY((d) => (d as CafeData).excessDeficitCostPerTransaction)}
icon={ }
isNegativeIndicator={true}
/>
>
)}
{/* Catering & Private Dining KPIs */}
{(locationType === 'catering' || locationType === 'private-dining') && (
(d as CateringData).productCostPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).productCostPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).productCostPercent)}
icon={ }
/>
(d as CateringData).laborCostPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).laborCostPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).laborCostPercent)}
icon={ }
/>
(d as CateringData).overtimeCostPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).overtimeCostPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).overtimeCostPercent)}
icon={ }
/>
(d as CateringData).temporaryLaborCostPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).temporaryLaborCostPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).temporaryLaborCostPercent)}
icon={ }
/>
(d as CateringData).expensesCostPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).expensesCostPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).expensesCostPercent)}
icon={ }
/>
(d as CateringData).excessDeficitPercent)}
quarterOverQuarter={calculateQoQ((d) => (d as CateringData).excessDeficitPercent)}
yearOverYear={calculateYoY((d) => (d as CateringData).excessDeficitPercent)}
icon={ }
isNegativeIndicator={true}
/>
)}
{/* KPI Trend Over Time */}
{/* Data Trend Over Time */}
);
}The World Bank
Corporate Dining Financial Dashboard
Reporting Period
{selectedYear}
{getLocationTitle()}
{currentData.period}

