ui-nyla/src/pages/billing/BillingDashboard.tsx
2026-04-11 00:07:30 +02:00

254 lines
8.5 KiB
TypeScript

/**
* Billing Dashboard Page
*
* Zeigt Guthaben, Statistiken und Transaktionen für den Benutzer.
*/
import React, { useState, useEffect, useMemo } from 'react';
import { useBilling, type BillingBalance, type UsageReport } from '../../hooks/useBilling';
import { BillingNav } from './BillingNav';
import styles from './Billing.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
// ============================================================================
// BALANCE CARD COMPONENT
// ============================================================================
interface BalanceCardProps {
balance: BillingBalance;
onClick?: () => void;
}
const BalanceCard: React.FC<BalanceCardProps> = ({ balance, onClick }) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
return (
<div
className={`${styles.balanceCard} ${balance.isWarning ? styles.warning : ''}`}
onClick={onClick}
>
<div className={styles.balanceHeader}>
<h3 className={styles.mandateName}>{balance.mandateName}</h3>
</div>
<div className={styles.balanceAmount}>
{formatCurrency(balance.balance)}
</div>
{balance.isWarning && (
<div className={styles.warningBadge}>
Niedriges Guthaben
</div>
)}
</div>
);
};
// ============================================================================
// STATISTICS CHART COMPONENT
// ============================================================================
interface StatisticsChartProps {
statistics: UsageReport | null;
loading?: boolean;
}
const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }) => {
const { t } = useLanguage();
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
if (loading) {
return <div className={styles.loadingPlaceholder}>{t('Statistiken laden')}</div>;
}
if (!statistics) {
return <div className={styles.noData}>{t('Keine Statistiken verfügbar')}</div>;
}
// Calculate max cost for bar scaling
const maxProviderCost = Math.max(...Object.values(statistics.costByProvider), 1);
return (
<div className={styles.statisticsChart}>
<div className={styles.totalCost}>
<span className={styles.totalLabel}>Gesamtkosten</span>
<span className={styles.totalAmount}>{formatCurrency(statistics.totalCost)}</span>
</div>
<div className={styles.chartSection}>
<h4>{t('Kosten nach Anbieter')}</h4>
{Object.entries(statistics.costByProvider).length === 0 ? (
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.barChart}>
{Object.entries(statistics.costByProvider).map(([provider, cost]) => (
<div key={provider} className={styles.barRow}>
<span className={styles.barLabel}>{provider}</span>
<div className={styles.barContainer}>
<div
className={styles.bar}
style={{ width: `${(cost / maxProviderCost) * 100}%` }}
/>
</div>
<span className={styles.barValue}>{formatCurrency(cost)}</span>
</div>
))}
</div>
)}
</div>
<div className={styles.chartSection}>
<h4>{t('Kosten nach Modell')}</h4>
{Object.entries(statistics.costByModel || {}).length === 0 ? (
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.barChart}>
{Object.entries(statistics.costByModel || {}).map(([model, cost]) => (
<div key={model} className={styles.barRow}>
<span className={styles.barLabel}>{model}</span>
<div className={styles.barContainer}>
<div
className={styles.bar}
style={{ width: `${(cost / maxProviderCost) * 100}%` }}
/>
</div>
<span className={styles.barValue}>{formatCurrency(cost)}</span>
</div>
))}
</div>
)}
</div>
<div className={styles.chartSection}>
<h4>{t('Kosten nach Feature')}</h4>
{Object.entries(statistics.costByFeature).length === 0 ? (
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.featureList}>
{Object.entries(statistics.costByFeature).map(([feature, cost]) => (
<div key={feature} className={styles.featureRow}>
<span className={styles.featureLabel}>{feature}</span>
<span className={styles.featureValue}>{formatCurrency(cost)}</span>
</div>
))}
</div>
)}
</div>
</div>
);
};
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export const BillingDashboard: React.FC = () => {
const { t } = useLanguage();
const {
balances,
statistics,
loading,
loadStatistics
} = useBilling();
const [selectedPeriod, setSelectedPeriod] = useState<'month' | 'year'>('month');
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth() + 1);
// Load statistics when period changes
useEffect(() => {
if (selectedPeriod === 'month') {
loadStatistics('month', selectedYear);
} else {
loadStatistics('year', selectedYear);
}
}, [selectedPeriod, selectedYear, loadStatistics]);
// Available years (current and last 2 years)
const availableYears = useMemo(() => {
const current = new Date().getFullYear();
return [current, current - 1, current - 2];
}, []);
// Available months
const availableMonths = Array.from({ length: 12 }, (_, i) => ({
value: i + 1,
label: String(i + 1),
}));
return (
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<h1>{t('Abrechnung')}</h1>
<p className={styles.subtitle}>{t('Übersicht über Guthaben und Nutzung')}</p>
</header>
<BillingNav />
{/* Balance Cards */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('Guthaben')}</h2>
{loading ? (
<div className={styles.loadingPlaceholder}>{t('Guthaben laden')}</div>
) : balances.length === 0 ? (
<div className={styles.noData}>{t('Keine Abrechnungskonten vorhanden')}</div>
) : (
<div className={styles.balanceGrid}>
{balances.map((balance) => (
<BalanceCard key={balance.mandateId} balance={balance} />
))}
</div>
)}
</section>
{/* Statistics */}
<section className={styles.section}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Nutzungsstatistik</h2>
<div className={styles.periodSelector}>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value as 'month' | 'year')}
className={styles.select}
>
<option value="month">Monat</option>
<option value="year">Jahr</option>
</select>
<select
value={selectedYear}
onChange={(e) => setSelectedYear(Number(e.target.value))}
className={styles.select}
>
{availableYears.map((year) => (
<option key={year} value={year}>{year}</option>
))}
</select>
{selectedPeriod === 'month' && (
<select
value={selectedMonth}
onChange={(e) => setSelectedMonth(Number(e.target.value))}
className={styles.select}
>
{availableMonths.map((month) => (
<option key={month.value} value={month.value}>{month.label}</option>
))}
</select>
)}
</div>
</div>
<StatisticsChart statistics={statistics} loading={loading} />
</section>
</div>
);
};
export default BillingDashboard;