Improve the frontend and repository layer
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Leaf,
|
||||
Droplets,
|
||||
TreeDeciduous,
|
||||
TrendingDown,
|
||||
Award,
|
||||
FileText,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import Card from '../../ui/Card/Card';
|
||||
import { Button, Badge } from '../../ui';
|
||||
import { useSustainabilityWidget } from '../../../api/hooks/sustainability';
|
||||
import { useCurrentTenant } from '../../../stores/tenant.store';
|
||||
|
||||
interface SustainabilityWidgetProps {
|
||||
days?: number;
|
||||
onViewDetails?: () => void;
|
||||
onExportReport?: () => void;
|
||||
}
|
||||
|
||||
export const SustainabilityWidget: React.FC<SustainabilityWidgetProps> = ({
|
||||
days = 30,
|
||||
onViewDetails,
|
||||
onExportReport
|
||||
}) => {
|
||||
const { t } = useTranslation(['sustainability', 'common']);
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
const { data, isLoading, error } = useSustainabilityWidget(tenantId, days, {
|
||||
enabled: !!tenantId
|
||||
});
|
||||
|
||||
const getSDGStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'sdg_compliant':
|
||||
return 'bg-green-500/10 text-green-600 border-green-500/20';
|
||||
case 'on_track':
|
||||
return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
|
||||
case 'progressing':
|
||||
return 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20';
|
||||
default:
|
||||
return 'bg-gray-500/10 text-gray-600 border-gray-500/20';
|
||||
}
|
||||
};
|
||||
|
||||
const getSDGStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
sdg_compliant: t('sustainability:sdg.status.compliant', 'SDG Compliant'),
|
||||
on_track: t('sustainability:sdg.status.on_track', 'On Track'),
|
||||
progressing: t('sustainability:sdg.status.progressing', 'Progressing'),
|
||||
baseline: t('sustainability:sdg.status.baseline', 'Baseline')
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-6 bg-[var(--bg-secondary)] rounded w-1/3"></div>
|
||||
<div className="h-32 bg-[var(--bg-secondary)] rounded"></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-8">
|
||||
<Leaf className="w-12 h-12 mx-auto mb-3 text-[var(--text-secondary)] opacity-50" />
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:errors.load_failed', 'Unable to load sustainability metrics')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="p-6 pb-4 border-b border-[var(--border-primary)] bg-gradient-to-r from-green-50/50 to-blue-50/50 dark:from-green-900/10 dark:to-blue-900/10">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/10 rounded-lg">
|
||||
<Leaf className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:widget.title', 'Sustainability Impact')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:widget.subtitle', 'Environmental & SDG 12.3 Compliance')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full border text-xs font-medium ${getSDGStatusColor(data.sdg_status)}`}>
|
||||
{getSDGStatusLabel(data.sdg_status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SDG Progress Bar */}
|
||||
<div className="p-6 pb-4 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{t('sustainability:sdg.progress_label', 'SDG 12.3 Target Progress')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-[var(--color-primary)]">
|
||||
{Math.round(data.sdg_progress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-green-500 to-emerald-600 rounded-full transition-all duration-500 relative overflow-hidden"
|
||||
style={{ width: `${Math.min(data.sdg_progress, 100)}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-2">
|
||||
{t('sustainability:sdg.target_note', 'Target: 50% food waste reduction by 2030')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="p-6 grid grid-cols-2 gap-4">
|
||||
{/* Waste Reduction */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingDown className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.waste_reduction', 'Waste Reduction')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{Math.abs(data.waste_reduction_percentage).toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{data.total_waste_kg.toFixed(0)} kg {t('common:saved', 'saved')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CO2 Impact */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Leaf className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.co2_avoided', 'CO₂ Avoided')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{data.co2_saved_kg.toFixed(0)} kg
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
≈ {data.trees_equivalent.toFixed(1)} {t('sustainability:metrics.trees', 'trees')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Water Saved */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Droplets className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.water_saved', 'Water Saved')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{(data.water_saved_liters / 1000).toFixed(1)} m³
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{data.water_saved_liters.toFixed(0)} {t('common:liters', 'liters')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grant Programs */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Award className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.grants_eligible', 'Grants Eligible')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{data.grant_programs_ready}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{t('sustainability:metrics.programs', 'programs')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Financial Impact */}
|
||||
<div className="px-6 pb-4">
|
||||
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-green-700 dark:text-green-400 mb-1">
|
||||
{t('sustainability:financial.potential_savings', 'Potential Monthly Savings')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
€{data.financial_savings_eur.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<TreeDeciduous className="w-10 h-10 text-green-600/30 dark:text-green-400/30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="p-6 pt-4 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]/30">
|
||||
<div className="flex items-center gap-2">
|
||||
{onViewDetails && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onViewDetails}
|
||||
className="flex-1"
|
||||
>
|
||||
<Info className="w-4 h-4 mr-1" />
|
||||
{t('sustainability:actions.view_details', 'View Details')}
|
||||
</Button>
|
||||
)}
|
||||
{onExportReport && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onExportReport}
|
||||
className="flex-1"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{t('sustainability:actions.export_report', 'Export Report')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] text-center mt-3">
|
||||
{t('sustainability:widget.footer', 'Aligned with UN SDG 12.3 & EU Green Deal')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SustainabilityWidget;
|
||||
Reference in New Issue
Block a user