Improve the frontend and repository layer

This commit is contained in:
Urtzi Alfaro
2025-10-23 07:44:54 +02:00
parent 8d30172483
commit 07c33fa578
112 changed files with 14726 additions and 2733 deletions

View File

@@ -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;