Improve the frontend 2
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user