Improve the frontend modals
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ShoppingCart, TrendingUp, Clock, AlertTriangle } from 'lucide-react';
|
||||
import { ShoppingCart, TrendingUp, Clock, AlertTriangle, Brain } from 'lucide-react';
|
||||
import { Card, Input } from '../../../../../components/ui';
|
||||
import type { ProcurementSettings } from '../../../../../api/types/settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ProcurementSettingsCardProps {
|
||||
settings: ProcurementSettings;
|
||||
@@ -14,6 +15,8 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation('ajustes');
|
||||
|
||||
const handleChange = (field: keyof ProcurementSettings) => (
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
@@ -27,7 +30,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
|
||||
<ShoppingCart className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
|
||||
Compras y Aprovisionamiento
|
||||
{t('procurement.title')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -35,7 +38,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
|
||||
<TrendingUp className="w-4 h-4 mr-2" />
|
||||
Auto-Aprobación de Órdenes de Compra
|
||||
{t('procurement.auto_approval')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
|
||||
<div className="flex items-center gap-2 md:col-span-2 xl:col-span-3">
|
||||
@@ -48,13 +51,13 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="auto_approve_enabled" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar auto-aprobación de órdenes de compra
|
||||
{t('procurement.auto_approve_enabled')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="Umbral de Auto-Aprobación (EUR)"
|
||||
label={t('procurement.auto_approve_threshold')}
|
||||
value={settings.auto_approve_threshold_eur}
|
||||
onChange={handleChange('auto_approve_threshold_eur')}
|
||||
disabled={disabled || !settings.auto_approve_enabled}
|
||||
@@ -66,7 +69,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="Puntuación Mínima de Proveedor"
|
||||
label={t('procurement.min_supplier_score')}
|
||||
value={settings.auto_approve_min_supplier_score}
|
||||
onChange={handleChange('auto_approve_min_supplier_score')}
|
||||
disabled={disabled || !settings.auto_approve_enabled}
|
||||
@@ -86,7 +89,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="require_approval_new_suppliers" className="text-sm text-[var(--text-secondary)]">
|
||||
Requiere aprobación para nuevos proveedores
|
||||
{t('procurement.require_approval_new_suppliers')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +103,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="require_approval_critical_items" className="text-sm text-[var(--text-secondary)]">
|
||||
Requiere aprobación para artículos críticos
|
||||
{t('procurement.require_approval_critical_items')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,12 +113,12 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Planificación y Previsión
|
||||
{t('procurement.planning')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
|
||||
<Input
|
||||
type="number"
|
||||
label="Tiempo de Entrega (días)"
|
||||
label={t('procurement.lead_time_days')}
|
||||
value={settings.procurement_lead_time_days}
|
||||
onChange={handleChange('procurement_lead_time_days')}
|
||||
disabled={disabled}
|
||||
@@ -127,7 +130,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="Días de Previsión de Demanda"
|
||||
label={t('procurement.demand_forecast_days')}
|
||||
value={settings.demand_forecast_days}
|
||||
onChange={handleChange('demand_forecast_days')}
|
||||
disabled={disabled}
|
||||
@@ -139,7 +142,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="Stock de Seguridad (%)"
|
||||
label={t('procurement.safety_stock_percentage')}
|
||||
value={settings.safety_stock_percentage}
|
||||
onChange={handleChange('safety_stock_percentage')}
|
||||
disabled={disabled}
|
||||
@@ -155,12 +158,12 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Flujo de Aprobación
|
||||
{t('procurement.workflow')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
|
||||
<Input
|
||||
type="number"
|
||||
label="Recordatorio de Aprobación (horas)"
|
||||
label={t('procurement.approval_reminder_hours')}
|
||||
value={settings.po_approval_reminder_hours}
|
||||
onChange={handleChange('po_approval_reminder_hours')}
|
||||
disabled={disabled}
|
||||
@@ -172,7 +175,7 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="Escalación Crítica (horas)"
|
||||
label={t('procurement.critical_escalation_hours')}
|
||||
value={settings.po_critical_escalation_hours}
|
||||
onChange={handleChange('po_critical_escalation_hours')}
|
||||
disabled={disabled}
|
||||
@@ -183,6 +186,110 @@ const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smart Procurement Calculation */}
|
||||
<div className="border-t border-[var(--border-primary)] pt-6">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
{t('procurement.smart_procurement')}
|
||||
</h4>
|
||||
<div className="space-y-3 pl-6">
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="use_reorder_rules"
|
||||
checked={settings.use_reorder_rules}
|
||||
onChange={handleChange('use_reorder_rules')}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)] mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="use_reorder_rules" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
{t('procurement.use_reorder_rules')}
|
||||
</label>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
{t('procurement.use_reorder_rules_desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="economic_rounding"
|
||||
checked={settings.economic_rounding}
|
||||
onChange={handleChange('economic_rounding')}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)] mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="economic_rounding" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
{t('procurement.economic_rounding')}
|
||||
</label>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
{t('procurement.economic_rounding_desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="respect_storage_limits"
|
||||
checked={settings.respect_storage_limits}
|
||||
onChange={handleChange('respect_storage_limits')}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)] mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="respect_storage_limits" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
{t('procurement.respect_storage_limits')}
|
||||
</label>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
{t('procurement.respect_storage_limits_desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="use_supplier_minimums"
|
||||
checked={settings.use_supplier_minimums}
|
||||
onChange={handleChange('use_supplier_minimums')}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)] mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="use_supplier_minimums" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
{t('procurement.use_supplier_minimums')}
|
||||
</label>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
{t('procurement.use_supplier_minimums_desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="optimize_price_tiers"
|
||||
checked={settings.optimize_price_tiers}
|
||||
onChange={handleChange('optimize_price_tiers')}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)] mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="optimize_price_tiers" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
{t('procurement.optimize_price_tiers')}
|
||||
</label>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
{t('procurement.optimize_price_tiers_desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react';
|
||||
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card } from '../../../../components/ui';
|
||||
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card, EmptyState } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
@@ -116,7 +116,7 @@ const ModelsConfigPage: React.FC = () => {
|
||||
hasModel: !!model,
|
||||
model,
|
||||
isTraining,
|
||||
lastTrainingDate: model?.created_at,
|
||||
lastTrainingDate: model?.created_at || undefined,
|
||||
accuracy: model ?
|
||||
(model.training_metrics?.mape !== undefined ? (100 - model.training_metrics.mape) :
|
||||
(model as any).mape !== undefined ? (100 - (model as any).mape) :
|
||||
@@ -209,13 +209,12 @@ const ModelsConfigPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración de Modelos IA"
|
||||
description="Gestiona el entrenamiento y configuración de modelos de predicción para cada ingrediente"
|
||||
/>
|
||||
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<StatsGrid
|
||||
stats={[
|
||||
@@ -232,39 +231,33 @@ const ModelsConfigPage: React.FC = () => {
|
||||
variant: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Modelos Huérfanos',
|
||||
value: orphanedModels.length,
|
||||
icon: AlertCircle,
|
||||
variant: 'info',
|
||||
title: 'Modelos Activos',
|
||||
value: modelStatuses.filter(s => s.status === 'active').length,
|
||||
icon: CheckCircle,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Precisión Promedio',
|
||||
value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${(100 - statistics.average_accuracy).toFixed(1)}%` : 'N/A'),
|
||||
value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${Number(statistics.average_accuracy).toFixed(1)}%` : 'N/A'),
|
||||
icon: TrendingUp,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Total Modelos',
|
||||
value: modelStatuses.length,
|
||||
icon: Brain,
|
||||
variant: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Modelos Huérfanos',
|
||||
value: orphanedModels.length,
|
||||
icon: AlertCircle,
|
||||
variant: 'error',
|
||||
},
|
||||
]}
|
||||
columns={4}
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Orphaned Models Warning */}
|
||||
{orphanedModels.length > 0 && (
|
||||
<Card className="p-4 bg-orange-50 border-orange-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-orange-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-orange-900 mb-1">
|
||||
Modelos Huérfanos Detectados
|
||||
</h4>
|
||||
<p className="text-sm text-orange-700">
|
||||
Se encontraron {orphanedModels.length} modelos entrenados para ingredientes que ya no existen en el inventario.
|
||||
Estos modelos pueden ser eliminados para optimizar el espacio de almacenamiento.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
@@ -289,18 +282,16 @@ const ModelsConfigPage: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Models Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredStatuses.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 col-span-full">
|
||||
<Brain className="w-12 h-12 text-[var(--color-secondary)] mb-4" />
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
No se encontraron ingredientes
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] text-center">
|
||||
No hay ingredientes que coincidan con los filtros aplicados.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
{filteredStatuses.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Brain}
|
||||
title="No se encontraron ingredientes"
|
||||
description="No hay ingredientes que coincidan con los filtros aplicados."
|
||||
className="col-span-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{(
|
||||
filteredStatuses.map((status) => {
|
||||
// Get status configuration for the StatusCard
|
||||
const statusConfig = {
|
||||
@@ -335,7 +326,7 @@ const ModelsConfigPage: React.FC = () => {
|
||||
id={status.ingredient.id}
|
||||
statusIndicator={statusConfig}
|
||||
title={status.ingredient.name}
|
||||
subtitle={status.ingredient.category}
|
||||
subtitle={status.ingredient.category || undefined}
|
||||
primaryValue={status.accuracy ? status.accuracy.toFixed(1) : 'N/A'}
|
||||
primaryValueLabel="Precisión"
|
||||
secondaryInfo={status.lastTrainingDate ? {
|
||||
@@ -371,7 +362,8 @@ const ModelsConfigPage: React.FC = () => {
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Modal */}
|
||||
<Modal
|
||||
@@ -463,4 +455,4 @@ const ModelsConfigPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsConfigPage;
|
||||
export default ModelsConfigPage;
|
||||
|
||||
@@ -9,11 +9,7 @@ import { QualityTemplateManager } from '../../../../components/domain/production
|
||||
* that are used during production processes.
|
||||
*/
|
||||
const QualityTemplatesPage: React.FC = () => {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<QualityTemplateManager />
|
||||
</div>
|
||||
);
|
||||
return <QualityTemplateManager />;
|
||||
};
|
||||
|
||||
export default QualityTemplatesPage;
|
||||
@@ -0,0 +1,580 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Leaf,
|
||||
TrendingDown,
|
||||
Euro,
|
||||
Award,
|
||||
Target,
|
||||
Droplets,
|
||||
TreeDeciduous,
|
||||
Calendar,
|
||||
Download,
|
||||
FileText,
|
||||
Info,
|
||||
HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { StatsGrid, Button, Card, Tooltip } from '../../../../components/ui';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { useSustainabilityMetrics } from '../../../../api/hooks/sustainability';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
|
||||
const SustainabilityPage: React.FC = () => {
|
||||
const { t } = useTranslation(['sustainability', 'common']);
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
// Date range state (default to last 30 days)
|
||||
const [dateRange, setDateRange] = useState<{ start?: string; end?: string }>({});
|
||||
|
||||
// Fetch sustainability metrics
|
||||
const {
|
||||
data: metrics,
|
||||
isLoading,
|
||||
error
|
||||
} = useSustainabilityMetrics(tenantId, dateRange.start, dateRange.end, {
|
||||
enabled: !!tenantId
|
||||
});
|
||||
|
||||
// Build stats for StatsGrid
|
||||
const sustainabilityStats = useMemo(() => {
|
||||
if (!metrics) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('sustainability:stats.total_waste_reduced', 'Total Waste Reduced'),
|
||||
value: `${metrics.waste_metrics.total_waste_kg.toFixed(0)} kg`,
|
||||
icon: TrendingDown,
|
||||
variant: 'success' as const,
|
||||
subtitle: t('sustainability:stats.from_baseline', 'From baseline'),
|
||||
trend: metrics.waste_metrics.waste_percentage < 25 ? {
|
||||
value: Math.abs(25 - metrics.waste_metrics.waste_percentage),
|
||||
direction: 'down' as const,
|
||||
label: t('sustainability:stats.vs_industry', 'vs industry avg')
|
||||
} : undefined
|
||||
},
|
||||
{
|
||||
title: t('sustainability:stats.waste_reduction_percentage', 'Waste Reduction'),
|
||||
value: `${Math.abs(metrics.sdg_compliance.sdg_12_3.reduction_achieved).toFixed(1)}%`,
|
||||
icon: Target,
|
||||
variant: metrics.sdg_compliance.sdg_12_3.reduction_achieved >= 15 ? ('success' as const) : ('info' as const),
|
||||
subtitle: t('sustainability:stats.progress_to_sdg', 'Progress to SDG 12.3'),
|
||||
trend: {
|
||||
value: metrics.sdg_compliance.sdg_12_3.progress_to_target,
|
||||
direction: 'up' as const,
|
||||
label: t('sustainability:stats.to_target', 'to 50% target')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('sustainability:stats.co2_avoided', 'CO₂ Avoided'),
|
||||
value: `${metrics.environmental_impact.co2_emissions.kg.toFixed(0)} kg`,
|
||||
icon: Leaf,
|
||||
variant: 'info' as const,
|
||||
subtitle: `≈ ${metrics.environmental_impact.co2_emissions.trees_to_offset.toFixed(1)} ${t('sustainability:stats.trees', 'trees')}`
|
||||
},
|
||||
{
|
||||
title: t('sustainability:stats.monthly_savings', 'Monthly Savings'),
|
||||
value: `€${metrics.financial_impact.potential_monthly_savings.toFixed(0)}`,
|
||||
icon: Euro,
|
||||
variant: 'success' as const,
|
||||
subtitle: t('sustainability:stats.from_waste_reduction', 'From waste reduction')
|
||||
},
|
||||
{
|
||||
title: t('sustainability:stats.sdg_progress', 'SDG 12.3 Progress'),
|
||||
value: `${Math.round(metrics.sdg_compliance.sdg_12_3.progress_to_target)}%`,
|
||||
icon: Award,
|
||||
variant: metrics.sdg_compliance.sdg_12_3.status === 'sdg_compliant' ? ('success' as const) :
|
||||
metrics.sdg_compliance.sdg_12_3.status === 'on_track' ? ('info' as const) : ('warning' as const),
|
||||
subtitle: metrics.sdg_compliance.sdg_12_3.status_label
|
||||
},
|
||||
{
|
||||
title: t('sustainability:stats.grant_programs', 'Grant Programs'),
|
||||
value: Object.values(metrics.grant_readiness.grant_programs).filter(p => p.eligible).length.toString(),
|
||||
icon: FileText,
|
||||
variant: 'info' as const,
|
||||
subtitle: t('sustainability:stats.eligible', 'Eligible programs')
|
||||
}
|
||||
];
|
||||
}, [metrics, t]);
|
||||
|
||||
// Get SDG status color
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
<PageHeader
|
||||
title={t('sustainability:page.title', 'Sostenibilidad')}
|
||||
description={t('sustainability:page.description', 'Seguimiento de impacto ambiental y cumplimiento SDG 12.3')}
|
||||
/>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !metrics) {
|
||||
return (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
<PageHeader
|
||||
title={t('sustainability:page.title', 'Sostenibilidad')}
|
||||
description={t('sustainability:page.description', 'Seguimiento de impacto ambiental y cumplimiento SDG 12.3')}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
{/* Page Header */}
|
||||
<PageHeader
|
||||
title={t('sustainability:page.title', 'Sostenibilidad')}
|
||||
description={t('sustainability:page.description', 'Seguimiento de impacto ambiental y cumplimiento SDG 12.3')}
|
||||
actions={[
|
||||
{
|
||||
id: "export-report",
|
||||
label: t('sustainability:actions.export_report', 'Exportar Informe'),
|
||||
icon: Download,
|
||||
onClick: () => {
|
||||
// TODO: Implement export
|
||||
console.log('Export sustainability report');
|
||||
},
|
||||
variant: "outline",
|
||||
size: "sm"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<StatsGrid
|
||||
stats={sustainabilityStats}
|
||||
columns={3}
|
||||
gap="lg"
|
||||
/>
|
||||
|
||||
{/* Main Content Sections */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Waste Analytics Section */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:sections.waste_analytics', 'Análisis de Residuos')}
|
||||
</h3>
|
||||
<Tooltip content={t('sustainability:tooltips.waste_analytics', 'Información detallada sobre los residuos generados en la producción')}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sections.waste_subtitle', 'Desglose de residuos por tipo')}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingDown className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Waste breakdown */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:waste.production', 'Residuos de Producción')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{metrics.waste_metrics.production_waste_kg.toFixed(1)} kg
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:waste.expired', 'Producto Expirado')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{metrics.waste_metrics.expired_waste_kg.toFixed(1)} kg
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[var(--border-primary)]">
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:waste.total', 'Total')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-[var(--text-primary)]">
|
||||
{metrics.waste_metrics.total_waste_kg.toFixed(1)} kg
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:waste.percentage', 'Porcentaje de Residuos')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{metrics.waste_metrics.waste_percentage.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Impact */}
|
||||
<div className="mt-4 p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Info className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-xs font-medium text-blue-700 dark:text-blue-400">
|
||||
{t('sustainability:ai.impact_title', 'Impacto de IA')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-300">
|
||||
{t('sustainability:ai.waste_avoided', 'Residuos evitados')}: <strong>{metrics.avoided_waste.waste_avoided_kg.toFixed(1)} kg</strong>
|
||||
</p>
|
||||
<p className="text-xs text-blue-600/80 dark:text-blue-300/80 mt-1">
|
||||
{t('sustainability:ai.batches', 'Lotes asistidos por IA')}: {metrics.avoided_waste.ai_assisted_batches}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Environmental Impact Section */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:sections.environmental_impact', 'Impacto Ambiental')}
|
||||
</h3>
|
||||
<Tooltip content={t('sustainability:tooltips.environmental_impact', 'Métricas de huella ambiental y su equivalencia en términos cotidianos')}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sections.environmental_subtitle', 'Métricas de huella ambiental')}
|
||||
</p>
|
||||
</div>
|
||||
<Leaf className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* CO2 */}
|
||||
<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-green-600" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">CO₂</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-[var(--text-primary)]">
|
||||
{metrics.environmental_impact.co2_emissions.kg.toFixed(0)} kg
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
≈ {metrics.environmental_impact.co2_emissions.trees_to_offset.toFixed(1)} {t('sustainability:metrics.trees', 'árboles')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Water */}
|
||||
<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" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">{t('sustainability:metrics.water', 'Agua')}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-[var(--text-primary)]">
|
||||
{metrics.environmental_impact.water_footprint.cubic_meters.toFixed(1)} m³
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{metrics.environmental_impact.water_footprint.liters.toFixed(0)} {t('common:liters', 'litros')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Land Use */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TreeDeciduous className="w-4 h-4 text-amber-600" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">{t('sustainability:metrics.land', 'Tierra')}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-[var(--text-primary)]">
|
||||
{metrics.environmental_impact.land_use.square_meters.toFixed(0)} m²
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{metrics.environmental_impact.land_use.hectares.toFixed(3)} {t('sustainability:metrics.hectares', 'hectáreas')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Human Equivalents */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Info className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">{t('sustainability:metrics.equivalents', 'Equivalentes')}</span>
|
||||
</div>
|
||||
<div className="text-xs space-y-1 text-[var(--text-secondary)]">
|
||||
<div>🚗 {metrics.environmental_impact.human_equivalents.car_km_equivalent.toFixed(0)} km</div>
|
||||
<div>📱 {metrics.environmental_impact.human_equivalents.smartphone_charges.toFixed(0)} cargas</div>
|
||||
<div>🚿 {metrics.environmental_impact.human_equivalents.showers_equivalent.toFixed(0)} duchas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* SDG Compliance & Grant Readiness */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* SDG 12.3 Compliance */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:sections.sdg_compliance', 'Cumplimiento SDG 12.3')}
|
||||
</h3>
|
||||
<Tooltip content={t('sustainability:tooltips.sdg_compliance', 'Progreso hacia el objetivo de desarrollo sostenible de la ONU para reducir residuos alimentarios')}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sections.sdg_subtitle', 'Progreso hacia objetivo ONU')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full border text-xs font-medium ${getSDGStatusColor(metrics.sdg_compliance.sdg_12_3.status)}`}>
|
||||
{metrics.sdg_compliance.sdg_12_3.status_label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{t('sustainability:sdg.progress_label', 'Progreso al Objetivo')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-[var(--color-primary)]">
|
||||
{Math.round(metrics.sdg_compliance.sdg_12_3.progress_to_target)}%
|
||||
</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"
|
||||
style={{ width: `${Math.min(metrics.sdg_compliance.sdg_12_3.progress_to_target, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-2">
|
||||
{t('sustainability:sdg.target_note', 'Objetivo: 50% reducción de residuos para 2030')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sdg.baseline', 'Línea Base')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{metrics.sdg_compliance.sdg_12_3.baseline_waste_percentage.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sdg.current', 'Actual')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{metrics.sdg_compliance.sdg_12_3.current_waste_percentage.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sdg.reduction', 'Reducción Lograda')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-green-600">
|
||||
{Math.abs(metrics.sdg_compliance.sdg_12_3.reduction_achieved).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[var(--border-primary)]">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sdg.certification_ready', 'Listo para Certificación')}
|
||||
</span>
|
||||
<span className={`text-sm font-medium ${metrics.sdg_compliance.certification_ready ? 'text-green-600' : 'text-amber-600'}`}>
|
||||
{metrics.sdg_compliance.certification_ready ? t('common:yes', 'Sí') : t('common:no', 'No')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Grant Readiness */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:sections.grant_readiness', 'Subvenciones Disponibles')}
|
||||
</h3>
|
||||
<Tooltip content={t('sustainability:tooltips.grant_readiness', 'Programas de financiación disponibles para empresas españolas según la Ley 1/2025 de prevención de residuos')}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sections.grant_subtitle', 'Programas de financiación elegibles')}
|
||||
</p>
|
||||
</div>
|
||||
<Award className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
|
||||
{/* Overall Readiness */}
|
||||
<div className="mb-4 p-4 bg-gradient-to-r from-amber-50 to-yellow-50 dark:from-amber-900/20 dark:to-yellow-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-amber-700 dark:text-amber-400">
|
||||
{t('sustainability:grant.overall_readiness', 'Preparación General')}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-amber-600 dark:text-amber-400">
|
||||
{Math.round(metrics.grant_readiness.overall_readiness_percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grant Programs List */}
|
||||
<div className="space-y-3">
|
||||
{Object.entries(metrics.grant_readiness.grant_programs).map(([key, program]) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`p-3 rounded-lg border ${
|
||||
program.eligible
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
|
||||
: 'bg-gray-50 dark:bg-gray-800/20 border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-medium ${
|
||||
program.eligible ? 'text-green-700 dark:text-green-400' : 'text-gray-600 dark:text-gray-400'
|
||||
}`}>
|
||||
{key.replace(/_/g, ' ')}
|
||||
</span>
|
||||
{program.eligible && (
|
||||
<span className="text-xs px-2 py-0.5 bg-green-500/20 text-green-700 dark:text-green-400 rounded-full">
|
||||
{t('sustainability:grant.eligible', 'Elegible')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{program.funding_eur && program.funding_eur > 0 && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{t('sustainability:grant.funding', 'Financiación')}: €{program.funding_eur.toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`text-xs px-2 py-1 rounded ${
|
||||
program.confidence === 'high'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: program.confidence === 'medium'
|
||||
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'
|
||||
}`}>
|
||||
{program.confidence}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Spain Compliance */}
|
||||
<div className="mt-4 pt-4 border-t border-[var(--border-primary)]">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('sustainability:grant.spain_compliance', 'Cumplimiento España')}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
{metrics.grant_readiness.spain_compliance?.law_1_2025 ? '✅' : '❌'}
|
||||
<span className="text-[var(--text-secondary)]">Ley 1/2025</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{metrics.grant_readiness.spain_compliance?.circular_economy_strategy ? '✅' : '❌'}
|
||||
<span className="text-[var(--text-secondary)]">Economía Circular 2030</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Financial Impact */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:sections.financial_impact', 'Impacto Financiero')}
|
||||
</h3>
|
||||
<Tooltip content={t('sustainability:tooltips.financial_impact', 'Costes asociados a residuos y ahorros potenciales mediante la reducción de desperdicio')}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:sections.financial_subtitle', 'Costes y ahorros de sostenibilidad')}
|
||||
</p>
|
||||
</div>
|
||||
<Euro className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('sustainability:financial.waste_cost', 'Coste de Residuos')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-red-600">
|
||||
€{metrics.financial_impact.waste_cost_eur.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
€{metrics.financial_impact.cost_per_kg.toFixed(2)}/kg
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p className="text-xs font-medium text-green-700 dark:text-green-400 mb-2">
|
||||
{t('sustainability:financial.monthly_savings', 'Ahorro Mensual')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
€{metrics.financial_impact.potential_monthly_savings.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-green-600/80 dark:text-green-400/80 mt-1">
|
||||
{t('sustainability:financial.from_reduction', 'Por reducción')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('sustainability:financial.annual_projection', 'Proyección Anual')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
€{metrics.financial_impact.annual_projection.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{t('sustainability:financial.estimated', 'Estimado')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-xs font-medium text-blue-700 dark:text-blue-400 mb-2">
|
||||
{t('sustainability:financial.roi', 'ROI de IA')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
€{(metrics.avoided_waste.waste_avoided_kg * metrics.financial_impact.cost_per_kg).toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-blue-600/80 dark:text-blue-400/80 mt-1">
|
||||
{t('sustainability:financial.ai_savings', 'Ahorrado por IA')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SustainabilityPage;
|
||||
Reference in New Issue
Block a user