Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -13,7 +13,7 @@ import {
useModelPerformance,
useTenantTrainingStatistics
} from '../../../../api/hooks/training';
import { ModelDetailsModal } from '../../../../components/domain/forecasting';
import { ModelDetailsModal, RetrainModelModal } from '../../../../components/domain/forecasting';
import type { IngredientResponse } from '../../../../api/types/inventory';
import type { TrainedModelResponse, SingleProductTrainingRequest } from '../../../../api/types/training';
@@ -47,6 +47,7 @@ const ModelsConfigPage: React.FC = () => {
const [selectedIngredient, setSelectedIngredient] = useState<IngredientResponse | null>(null);
const [selectedModel, setSelectedModel] = useState<TrainedModelResponse | null>(null);
const [showTrainingModal, setShowTrainingModal] = useState(false);
const [showRetrainModal, setShowRetrainModal] = useState(false);
const [showModelDetailsModal, setShowModelDetailsModal] = useState(false);
const [trainingSettings, setTrainingSettings] = useState<Partial<SingleProductTrainingRequest>>({
seasonality_mode: 'additive',
@@ -183,9 +184,38 @@ const ModelsConfigPage: React.FC = () => {
setShowTrainingModal(true);
};
const handleStartRetraining = (ingredient: IngredientResponse) => {
setSelectedIngredient(ingredient);
// Find and set the model for this ingredient
const model = modelStatuses.find(status => status.ingredient.id === ingredient.id)?.model;
if (model) {
setSelectedModel(model);
}
setShowRetrainModal(true);
};
const handleRetrain = async (settings: SingleProductTrainingRequest) => {
if (!selectedIngredient) return;
try {
await trainMutation.mutateAsync({
tenantId,
inventoryProductId: selectedIngredient.id,
request: settings
});
addToast(`Reentrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
setShowRetrainModal(false);
setSelectedIngredient(null);
setSelectedModel(null);
} catch (error) {
addToast('Error al reentrenar el modelo', { type: 'error' });
}
};
if (ingredientsLoading || modelsLoading) {
return (
@@ -238,7 +268,7 @@ const ModelsConfigPage: React.FC = () => {
},
{
title: 'Precisión Promedio',
value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${Number(statistics.average_accuracy).toFixed(1)}%` : 'N/A'),
value: statsError ? 'N/A' : (statistics?.models?.average_accuracy !== undefined && statistics?.models?.average_accuracy !== null ? `${Number(statistics.models.average_accuracy).toFixed(1)}%` : 'N/A'),
icon: TrendingUp,
variant: 'success',
},
@@ -354,7 +384,7 @@ const ModelsConfigPage: React.FC = () => {
...(status.hasModel ? [{
label: 'Reentrenar',
icon: RotateCcw,
onClick: () => handleStartTraining(status.ingredient),
onClick: () => handleStartRetraining(status.ingredient),
priority: 'secondary' as const
}] : [])
]}
@@ -451,6 +481,22 @@ const ModelsConfigPage: React.FC = () => {
model={selectedModel}
/>
)}
{/* Retrain Model Modal */}
{selectedIngredient && (
<RetrainModelModal
isOpen={showRetrainModal}
onClose={() => {
setShowRetrainModal(false);
setSelectedIngredient(null);
setSelectedModel(null);
}}
ingredient={selectedIngredient}
currentModel={selectedModel}
onRetrain={handleRetrain}
isLoading={trainMutation.isPending}
/>
)}
</div>
);
};

View File

@@ -11,11 +11,10 @@ import {
Calendar,
Download,
FileText,
Info,
HelpCircle
Info
} from 'lucide-react';
import { PageHeader } from '../../../../components/layout';
import { StatsGrid, Button, Card, Tooltip } from '../../../../components/ui';
import { StatsGrid, Button, Card } from '../../../../components/ui';
import { LoadingSpinner } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { useSustainabilityMetrics } from '../../../../api/hooks/sustainability';
@@ -146,6 +145,76 @@ const SustainabilityPage: React.FC = () => {
);
}
// Check if we have insufficient data
if (metrics.data_sufficient === false) {
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-8">
<div className="text-center py-12 max-w-2xl mx-auto">
<div className="mb-6 inline-flex items-center justify-center w-20 h-20 bg-blue-500/10 rounded-full">
<Info className="w-10 h-10 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
{t('sustainability:insufficient_data.title', 'Collecting Sustainability Data')}
</h3>
<p className="text-base text-[var(--text-secondary)] mb-6">
{t('sustainability:insufficient_data.description',
'Start producing batches to see your sustainability metrics and SDG compliance status.'
)}
</p>
<div className="bg-[var(--bg-secondary)] rounded-lg p-6 mb-6">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('sustainability:insufficient_data.requirements_title', 'Minimum Requirements')}
</h4>
<ul className="text-sm text-[var(--text-secondary)] space-y-2 text-left max-w-md mx-auto">
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_production',
'At least 50kg of production over the analysis period'
)}
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_baseline',
'90 days of production history for accurate baseline calculation'
)}
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_tracking',
'Production batches with waste tracking enabled'
)}
</span>
</li>
</ul>
</div>
<div className="flex items-center justify-center gap-2 text-sm text-[var(--text-tertiary)]">
<Calendar className="w-4 h-4" />
<span>
{t('sustainability:insufficient_data.current_production',
'Current production: {{production}}kg of {{required}}kg minimum',
{
production: metrics.current_production_kg?.toFixed(1) || '0.0',
required: metrics.minimum_production_required_kg || 50
}
)}
</span>
</div>
</div>
</Card>
</div>
);
}
return (
<div className="space-y-6 p-4 sm:p-6">
{/* Page Header */}
@@ -180,14 +249,9 @@ const SustainabilityPage: React.FC = () => {
<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>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.waste_analytics', 'Análisis de Residuos')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.waste_subtitle', 'Desglose de residuos por tipo')}
</p>
@@ -254,14 +318,9 @@ const SustainabilityPage: React.FC = () => {
<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>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.environmental_impact', 'Impacto Ambiental')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.environmental_subtitle', 'Métricas de huella ambiental')}
</p>
@@ -334,14 +393,9 @@ const SustainabilityPage: React.FC = () => {
<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>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.sdg_compliance', 'Cumplimiento SDG 12.3')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.sdg_subtitle', 'Progreso hacia objetivo ONU')}
</p>
@@ -413,14 +467,9 @@ const SustainabilityPage: React.FC = () => {
<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>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.grant_readiness', 'Subvenciones Disponibles')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.grant_subtitle', 'Programas de financiación elegibles')}
</p>
@@ -508,14 +557,9 @@ const SustainabilityPage: React.FC = () => {
<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>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.financial_impact', 'Impacto Financiero')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.financial_subtitle', 'Costes y ahorros de sostenibilidad')}
</p>