/**
* EqualizationCompactView Component
* A no-scroll, mobile-enhanced equalization visualization
* Shows step-contextual data, legend, and territories
*/
'use client';
import React, { useMemo } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import { ChevronLeft, ChevronRight, Info, DollarSign, BarChart3, Scale, Calculator, Trophy } from 'lucide-react';
import { useVisualizerStore } from '@/hooks/useVisualizerStore';
import { useEqualizationData } from '@/contexts/EqualizationDataContext';
import { TOTAL_EQUALIZATION_STEPS, EqualizationStep } from '@/lib/visualizer/equalization';
import { isTerritory } from '@/lib/visualizer/equalization/equalizationData';
const stepIcons: Record<number, React.ReactNode> = {
1: <Info className="w-4 h-4" />,
2: <DollarSign className="w-4 h-4" />,
3: <BarChart3 className="w-4 h-4" />,
4: <Scale className="w-4 h-4" />,
5: <BarChart3 className="w-4 h-4" />,
6: <Calculator className="w-4 h-4" />,
7: <Trophy className="w-4 h-4" />,
};
const stepShortNames: Record<number, { en: string; fr: string }> = {
1: { en: 'Intro', fr: 'Intro' },
2: { en: 'Revenue', fr: 'Revenus' },
3: { en: 'Capacity', fr: 'Capacité' },
4: { en: 'Standard', fr: 'Norme' },
5: { en: 'Compare', fr: 'Comparer' },
6: { en: 'Calculate', fr: 'Calculer' },
7: { en: 'Results', fr: 'Résultats' },
};
interface StepLegendItem {
color: string;
label: string;
}
export function EqualizationCompactView() {
const t = useTranslations('equalization');
const tProvinces = useTranslations('provinces');
const locale = useLocale() as 'en' | 'fr';
const {
equalizationStep,
setEqualizationStep,
nextEqualizationStep,
prevEqualizationStep,
selectedProvince,
setSelectedProvince,
} = useVisualizerStore();
const { data, nationalStandard, formatPaymentAmount, formatPerCapitaAmount, revenueCategories } = useEqualizationData();
const isFirstStep = equalizationStep === 1;
const isLastStep = equalizationStep === TOTAL_EQUALIZATION_STEPS;
// Get step-contextual legend (colors match equalizationColors.ts)
const stepLegend = useMemo((): StepLegendItem[] => {
switch (equalizationStep) {
case 1:
case 2:
// Introduction steps - all provinces show neutral color
return [
{ color: '#94A3B8', label: locale === 'en' ? 'All provinces' : 'Toutes les provinces' },
{ color: '#9CA3AF', label: locale === 'en' ? 'Territory (TFF)' : 'Territoire (FFT)' },
];
case 3:
case 4:
// Fiscal capacity steps - show capacity gradient
return [
{ color: '#D97706', label: locale === 'en' ? 'Below average (<95%)' : 'Sous la moyenne (<95%)' },
{ color: '#6B7280', label: locale === 'en' ? 'At average (95-105%)' : 'À la moyenne (95-105%)' },
{ color: '#3B82F6', label: locale === 'en' ? 'Above average (>105%)' : 'Au-dessus de la moyenne (>105%)' },
{ color: '#9CA3AF', label: locale === 'en' ? 'Territory (TFF)' : 'Territoire (FFT)' },
];
case 5:
case 6:
case 7:
// Payment steps - show receiving/contributing status
return [
{ color: '#059669', label: locale === 'en' ? 'Receives payment' : 'Reçoit un paiement' },
{ color: '#6B7280', label: locale === 'en' ? 'No payment (above standard)' : 'Aucun paiement (au-dessus)' },
{ color: '#9CA3AF', label: locale === 'en' ? 'Territory (TFF)' : 'Territoire (FFT)' },
];
default:
return [];
}
}, [equalizationStep, locale]);
// Get step-contextual data view
const stepData = useMemo(() => {
const allData = Object.values(data);
const provinces = allData.filter(d => !isTerritory(d.provinceCode));
const territories = allData.filter(d => isTerritory(d.provinceCode));
switch (equalizationStep) {
case 1:
case 2:
// Overview: Show all regions with status
return {
title: locale === 'en' ? 'Overview' : 'Aperçu',
columns: [
{ key: 'region', label: locale === 'en' ? 'Region' : 'Région' },
{ key: 'status', label: locale === 'en' ? 'Status' : 'Statut' },
{ key: 'payment', label: locale === 'en' ? 'Transfer' : 'Transfert' },
],
rows: [
...provinces.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
status: p.isReceiving
? (locale === 'en' ? 'Receiving' : 'Bénéficiaire')
: (locale === 'en' ? 'Above std.' : 'Au-dessus'),
statusColor: p.isReceiving ? 'text-emerald-400' : 'text-gray-400',
payment: p.isReceiving ? formatPaymentAmount(p.paymentMillions, locale) : '—',
})),
...territories.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
status: 'TFF',
statusColor: 'text-purple-400',
payment: formatPaymentAmount(p.paymentMillions, locale),
})),
],
};
case 3:
case 4:
case 5:
// Fiscal capacity comparison
return {
title: locale === 'en' ? 'Fiscal Capacity' : 'Capacité fiscale',
columns: [
{ key: 'region', label: locale === 'en' ? 'Region' : 'Région' },
{ key: 'capacity', label: locale === 'en' ? 'Capacity' : 'Capacité' },
{ key: 'gap', label: locale === 'en' ? 'vs Avg' : 'vs Moy.' },
],
rows: [
...provinces
.sort((a, b) => b.fiscalCapacityIndex - a.fiscalCapacityIndex)
.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
capacity: `${p.fiscalCapacityIndex}%`,
capacityColor: p.fiscalCapacityIndex > 105 ? 'text-emerald-400' :
p.fiscalCapacityIndex >= 95 ? 'text-amber-400' : 'text-red-400',
gap: p.fiscalCapacityIndex >= 100
? `+${p.fiscalCapacityIndex - 100}%`
: `${p.fiscalCapacityIndex - 100}%`,
gapColor: p.fiscalCapacityIndex >= 100 ? 'text-emerald-400' : 'text-red-400',
})),
...territories.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
capacity: 'N/A',
capacityColor: 'text-purple-400',
gap: 'TFF',
gapColor: 'text-purple-400',
})),
],
};
case 6:
case 7:
// Payment results
return {
title: locale === 'en' ? 'Payments' : 'Paiements',
columns: [
{ key: 'region', label: locale === 'en' ? 'Region' : 'Région' },
{ key: 'payment', label: locale === 'en' ? 'Amount' : 'Montant' },
{ key: 'perCapita', label: locale === 'en' ? 'Per Capita' : 'Par hab.' },
],
rows: [
...provinces
.sort((a, b) => b.paymentMillions - a.paymentMillions)
.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
payment: p.paymentMillions > 0 ? formatPaymentAmount(p.paymentMillions, locale) : '—',
paymentColor: p.paymentMillions > 0 ? 'text-emerald-400' : 'text-gray-500',
perCapita: p.perCapitaPayment > 0 ? formatPerCapitaAmount(p.perCapitaPayment, locale) : '—',
})),
...territories.map(p => ({
code: p.provinceCode,
region: tProvinces(p.provinceCode),
payment: formatPaymentAmount(p.paymentMillions, locale),
paymentColor: 'text-purple-400',
perCapita: formatPerCapitaAmount(p.perCapitaPayment, locale),
})),
],
};
default:
return null;
}
}, [equalizationStep, data, locale, tProvinces, formatPaymentAmount, formatPerCapitaAmount]);
// Step description
const stepDescription = useMemo(() => {
const descriptions: Record<number, { en: string; fr: string }> = {
1: {
en: 'Federal transfers ensuring comparable public services across provinces.',
fr: 'Transferts fédéraux assurant des services publics comparables entre provinces.',
},
2: {
en: 'Five revenue categories measure provincial fiscal capacity.',
fr: 'Cinq catégories de revenus mesurent la capacité fiscale provinciale.',
},
3: {
en: 'Each province\'s ability to raise revenue, shown as % of national average.',
fr: 'Capacité de chaque province à générer des revenus, en % de la moyenne nationale.',
},
4: {
en: `National standard: ${formatPerCapitaAmount(nationalStandard.averageFiscalCapacity, locale)} per capita.`,
fr: `Norme nationale: ${formatPerCapitaAmount(nationalStandard.averageFiscalCapacity, locale)} par habitant.`,
},
5: {
en: 'Provinces below 100% are eligible for equalization payments.',
fr: 'Les provinces sous 100% sont admissibles aux paiements de péréquation.',
},
6: {
en: 'Payment = (Gap below standard) × Population',
fr: 'Paiement = (Écart sous la norme) × Population',
},
7: {
en: `Total envelope: ${formatPaymentAmount(nationalStandard.totalEnvelopeMillions, locale)} for ${nationalStandard.fiscalYear}.`,
fr: `Enveloppe totale: ${formatPaymentAmount(nationalStandard.totalEnvelopeMillions, locale)} pour ${nationalStandard.fiscalYear}.`,
},
};
return descriptions[equalizationStep]?.[locale] || '';
}, [equalizationStep, locale, nationalStandard, formatPaymentAmount, formatPerCapitaAmount]);
return (
<div className="flex flex-col h-full">
{/* Step Tabs - Horizontal scrollable on mobile */}
<div className="flex items-center gap-1 pb-3 overflow-x-auto scrollbar-hide">
{Array.from({ length: TOTAL_EQUALIZATION_STEPS }, (_, i) => i + 1).map((step) => (
<button
key={step}
onClick={() => setEqualizationStep(step as EqualizationStep)}
className={`
flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all
${step === equalizationStep
? 'bg-accent-red text-white'
: 'bg-bg-elevated text-text-secondary hover:text-text-primary hover:bg-bg-secondary'
}
`}
>
{stepIcons[step]}
<span className="hidden sm:inline">{stepShortNames[step][locale]}</span>
<span className="sm:hidden">{step}</span>
</button>
))}
</div>
{/* Step Description */}
<div className="py-2 px-1 text-sm text-text-secondary border-b border-border-subtle">
{stepDescription}
</div>
{/* Legend - Inline, compact */}
<div className="flex flex-wrap gap-3 py-2 px-1 text-xs">
{stepLegend.map((item, i) => (
<div key={i} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: item.color }}
/>
<span className="text-text-secondary">{item.label}</span>
</div>
))}
</div>
{/* Data Table - Compact, scrollable if needed */}
{stepData && (
<div className="flex-1 overflow-auto mt-2">
<table className="w-full text-xs">
<thead className="bg-bg-elevated sticky top-0">
<tr>
{stepData.columns.map(col => (
<th
key={col.key}
className={`px-2 py-1.5 font-medium text-text-secondary ${
col.key === 'region' ? 'text-left' : 'text-right'
}`}
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border-subtle">
{stepData.rows.map((row) => (
<tr
key={row.code}
onClick={() => setSelectedProvince(selectedProvince === row.code ? null : row.code, 'table')}
className={`
cursor-pointer transition-colors
${selectedProvince === row.code ? 'bg-accent-red/10' : 'hover:bg-bg-elevated'}
`}
>
<td className="px-2 py-1.5 font-medium text-text-primary">
{row.region}
</td>
{stepData.columns.slice(1).map(col => (
<td
key={col.key}
className={`px-2 py-1.5 text-right ${
(row as Record<string, string>)[`${col.key}Color`] || 'text-text-secondary'
}`}
>
{(row as Record<string, string>)[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Navigation - Fixed at bottom */}
<div className="flex items-center justify-between pt-3 mt-auto border-t border-border-subtle">
<button
onClick={prevEqualizationStep}
disabled={isFirstStep}
className={`
flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors
${isFirstStep
? 'text-text-tertiary cursor-not-allowed'
: 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary'
}
`}
>
<ChevronLeft className="w-4 h-4" />
<span className="hidden sm:inline">{t('navigation.previous')}</span>
</button>
<span className="text-xs text-text-tertiary">
{equalizationStep} / {TOTAL_EQUALIZATION_STEPS}
</span>
<button
onClick={nextEqualizationStep}
disabled={isLastStep}
className={`
flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors
${isLastStep
? 'text-text-tertiary cursor-not-allowed'
: 'bg-accent-red text-white hover:bg-accent-red/90'
}
`}
>
<span className="hidden sm:inline">{t('navigation.next')}</span>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
);
}