Fix UI issues 2

This commit is contained in:
Urtzi Alfaro
2025-12-29 21:52:53 +01:00
parent 02f0c91a15
commit e494ea8635
6 changed files with 395 additions and 219 deletions

View File

@@ -162,7 +162,8 @@ const EnvironmentalConstants = {
LAND_USE_PER_KG: 3.4, // m² per kg LAND_USE_PER_KG: 3.4, // m² per kg
TREES_PER_TON_CO2: 50, TREES_PER_TON_CO2: 50,
SDG_TARGET_REDUCTION: 0.50, // 50% reduction target SDG_TARGET_REDUCTION: 0.50, // 50% reduction target
EU_BAKERY_BASELINE_WASTE: 0.25 // 25% baseline EU_BAKERY_BASELINE_WASTE: 0.25, // 25% baseline
MINIMUM_PRODUCTION_KG: 50 // Minimum production to show meaningful metrics
}; };
/** /**
@@ -320,6 +321,78 @@ function assessGrantReadiness(sdgCompliance: SDGCompliance): GrantReadiness {
// ===== MAIN AGGREGATION FUNCTION ===== // ===== MAIN AGGREGATION FUNCTION =====
/**
* Get default metrics for insufficient data state
*/
function getInsufficientDataMetrics(
totalProductionKg: number,
startDate?: string,
endDate?: string
): SustainabilityMetrics {
return {
period: {
start_date: startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
end_date: endDate || new Date().toISOString(),
days: 30
},
waste_metrics: {
total_waste_kg: 0,
production_waste_kg: 0,
expired_waste_kg: 0,
waste_percentage: 0,
waste_by_reason: {}
},
environmental_impact: {
co2_emissions: { kg: 0, tons: 0, trees_to_offset: 0 },
water_footprint: { liters: 0, cubic_meters: 0 },
land_use: { square_meters: 0, hectares: 0 },
human_equivalents: { car_km_equivalent: 0, smartphone_charges: 0, showers_equivalent: 0, trees_planted: 0 }
},
sdg_compliance: {
sdg_12_3: {
baseline_waste_percentage: 0,
current_waste_percentage: 0,
reduction_achieved: 0,
target_reduction: 50,
progress_to_target: 0,
status: 'baseline',
status_label: 'Collecting Baseline Data',
target_waste_percentage: 0
},
baseline_period: 'not_available',
certification_ready: false,
improvement_areas: ['start_production_tracking']
},
avoided_waste: {
waste_avoided_kg: 0,
ai_assisted_batches: 0,
environmental_impact_avoided: { co2_kg: 0, water_liters: 0 },
methodology: 'insufficient_data'
},
financial_impact: {
waste_cost_eur: 0,
cost_per_kg: 3.50,
potential_monthly_savings: 0,
annual_projection: 0
},
grant_readiness: {
overall_readiness_percentage: 0,
grant_programs: {
life_circular_economy: { eligible: false, confidence: 'low', requirements_met: false, funding_eur: 73_000_000 },
horizon_europe_cluster_6: { eligible: false, confidence: 'low', requirements_met: false, funding_eur: 880_000_000 },
fedima_sustainability_grant: { eligible: false, confidence: 'low', requirements_met: false, funding_eur: 20_000 },
eit_food_retail: { eligible: false, confidence: 'low', requirements_met: false, funding_eur: 45_000 },
un_sdg_certified: { eligible: false, confidence: 'low', requirements_met: false, funding_eur: 0 }
},
recommended_applications: [],
spain_compliance: { law_1_2025: false, circular_economy_strategy: false }
},
data_sufficient: false,
minimum_production_required_kg: EnvironmentalConstants.MINIMUM_PRODUCTION_KG,
current_production_kg: totalProductionKg
};
}
/** /**
* Get comprehensive sustainability metrics by aggregating production and inventory data * Get comprehensive sustainability metrics by aggregating production and inventory data
*/ */
@@ -337,11 +410,22 @@ export async function getSustainabilityMetrics(
getProductionAIImpact(tenantId, startDate, endDate) getProductionAIImpact(tenantId, startDate, endDate)
]); ]);
// Calculate total production
const totalProductionKg = productionData.total_planned || 0;
// Check if we have sufficient data for meaningful metrics
// Minimum: 50kg production to avoid false metrics on empty accounts
const hasDataSufficient = totalProductionKg >= EnvironmentalConstants.MINIMUM_PRODUCTION_KG;
// If insufficient data, return a "collecting data" state
if (!hasDataSufficient) {
return getInsufficientDataMetrics(totalProductionKg, startDate, endDate);
}
// Aggregate waste metrics // Aggregate waste metrics
const productionWaste = (productionData.total_production_waste || 0) + (productionData.total_defects || 0); const productionWaste = (productionData.total_production_waste || 0) + (productionData.total_defects || 0);
const totalWasteKg = productionWaste + (inventoryData.inventory_waste_kg || 0); const totalWasteKg = productionWaste + (inventoryData.inventory_waste_kg || 0);
const totalProductionKg = productionData.total_planned || 0;
const wastePercentage = totalProductionKg > 0 const wastePercentage = totalProductionKg > 0
? (totalWasteKg / totalProductionKg) * 100 ? (totalWasteKg / totalProductionKg) * 100
: 0; : 0;
@@ -403,7 +487,10 @@ export async function getSustainabilityMetrics(
sdg_compliance: sdgCompliance, sdg_compliance: sdgCompliance,
avoided_waste: avoidedWaste, avoided_waste: avoidedWaste,
financial_impact: financialImpact, financial_impact: financialImpact,
grant_readiness: grantReadiness grant_readiness: grantReadiness,
data_sufficient: true,
minimum_production_required_kg: EnvironmentalConstants.MINIMUM_PRODUCTION_KG,
current_production_kg: totalProductionKg
}; };
} catch (error) { } catch (error) {

View File

@@ -114,6 +114,10 @@ export interface SustainabilityMetrics {
avoided_waste: AvoidedWaste; avoided_waste: AvoidedWaste;
financial_impact: FinancialImpact; financial_impact: FinancialImpact;
grant_readiness: GrantReadiness; grant_readiness: GrantReadiness;
// Data sufficiency flags
data_sufficient: boolean;
minimum_production_required_kg?: number;
current_production_kg?: number;
} }
export interface SustainabilityWidgetData { export interface SustainabilityWidgetData {

View File

@@ -10,7 +10,6 @@ import {
Users, Users,
UserPlus, UserPlus,
Euro as EuroIcon, Euro as EuroIcon,
Sparkles,
FileText, FileText,
Factory, Factory,
Search, Search,
@@ -159,21 +158,6 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */}
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<div className="flex items-center justify-center mb-3">
<div className="p-3 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 rounded-full">
<Sparkles className="w-8 h-8 text-[var(--color-primary)]" />
</div>
</div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
{t('itemTypeSelector.title')}
</h2>
<p className="text-[var(--text-secondary)] max-w-md mx-auto">
{t('itemTypeSelector.description')}
</p>
</div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="space-y-3"> <div className="space-y-3">
{/* Search Bar */} {/* Search Bar */}
@@ -256,7 +240,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
)} )}
{/* Content */} {/* Content */}
<div className="flex items-center gap-4"> <div className="flex items-start gap-4">
{/* Icon */} {/* Icon */}
<div <div
className={` className={`
@@ -272,8 +256,8 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
</div> </div>
{/* Text */} {/* Text */}
<div className="flex-1 text-left"> <div className="flex-1 text-left pr-16">
<h3 className="text-base md:text-lg font-semibold text-[var(--text-primary)] mb-0.5 group-hover:text-[var(--color-primary)] transition-colors"> <h3 className="text-base md:text-lg font-semibold text-[var(--text-primary)] group-hover:text-[var(--color-primary)] transition-colors">
{itemType.title} {itemType.title}
</h3> </h3>
<p className="text-sm text-[var(--text-secondary)] leading-snug mt-1"> <p className="text-sm text-[var(--text-secondary)] leading-snug mt-1">

View File

@@ -1,4 +1,5 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { X, ChevronLeft, ChevronRight, AlertCircle, CheckCircle } from 'lucide-react'; import { X, ChevronLeft, ChevronRight, AlertCircle, CheckCircle } from 'lucide-react';
export interface WizardStep { export interface WizardStep {
@@ -48,12 +49,26 @@ export const WizardModal: React.FC<WizardModalProps> = ({
dataRef, dataRef,
onDataChange onDataChange
}) => { }) => {
const { t } = useTranslation('wizards');
const [currentStepIndex, setCurrentStepIndex] = useState(0); const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [isValidating, setIsValidating] = useState(false); const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null); const [validationError, setValidationError] = useState<string | null>(null);
const [validationSuccess, setValidationSuccess] = useState(false); const [validationSuccess, setValidationSuccess] = useState(false);
const currentStep = steps[currentStepIndex]; const currentStep = steps[currentStepIndex];
// Helper to translate step title - if it looks like a translation key (contains dots), translate it
const translateStepTitle = (stepTitle: string): string => {
if (stepTitle.includes('.')) {
const translated = t(stepTitle);
// If translation returns the key itself, return just the last part as fallback
if (translated === stepTitle) {
return stepTitle.split('.').pop() || stepTitle;
}
return translated;
}
return stepTitle;
};
const isFirstStep = currentStepIndex === 0; const isFirstStep = currentStepIndex === 0;
const isLastStep = currentStepIndex === steps.length - 1; const isLastStep = currentStepIndex === steps.length - 1;
@@ -178,11 +193,11 @@ export const WizardModal: React.FC<WizardModalProps> = ({
{/* Modal */} {/* Modal */}
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
<div <div
className={`bg-[var(--bg-primary)] rounded-xl shadow-2xl ${sizeClasses[size]} w-full max-h-[90vh] overflow-hidden pointer-events-auto animate-slideUp`} className={`bg-[var(--bg-primary)] rounded-xl shadow-2xl ${sizeClasses[size]} w-full max-h-[90vh] flex flex-col overflow-hidden pointer-events-auto animate-slideUp`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="sticky top-0 z-10 bg-[var(--bg-primary)] border-b border-[var(--border-secondary)] shadow-sm"> <div className="flex-shrink-0 z-10 bg-[var(--bg-primary)] border-b border-[var(--border-secondary)] shadow-sm">
{/* Title Bar */} {/* Title Bar */}
<div className="flex items-center justify-between p-4 sm:p-6 pb-3 sm:pb-4"> <div className="flex items-center justify-between p-4 sm:p-6 pb-3 sm:pb-4">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
@@ -232,7 +247,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
? 'bg-[var(--color-primary)] shadow-md' ? 'bg-[var(--color-primary)] shadow-md'
: 'bg-[var(--bg-tertiary)] cursor-not-allowed' : 'bg-[var(--bg-tertiary)] cursor-not-allowed'
}`} }`}
aria-label={`${step.title} - ${isCompleted ? 'Completado' : isCurrent ? 'En progreso' : 'Pendiente'}`} aria-label={`${translateStepTitle(step.title)} - ${isCompleted ? 'Completado' : isCurrent ? 'En progreso' : 'Pendiente'}`}
> >
{isCurrent && ( {isCurrent && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" /> <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
@@ -241,7 +256,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
{/* Tooltip on hover */} {/* Tooltip on hover */}
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded shadow-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-20"> <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded shadow-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-20">
{step.title} {translateStepTitle(step.title)}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-[var(--border-secondary)]" /> <div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-[var(--border-secondary)]" />
</div> </div>
</div> </div>
@@ -250,7 +265,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
</div> </div>
<div className="flex items-center justify-between text-xs sm:text-sm"> <div className="flex items-center justify-between text-xs sm:text-sm">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
<span className="font-semibold text-[var(--text-primary)] truncate">{currentStep.title}</span> <span className="font-semibold text-[var(--text-primary)] truncate">{translateStepTitle(currentStep.title)}</span>
{currentStep.isOptional && ( {currentStep.isOptional && (
<span className="px-2 py-0.5 text-xs bg-[var(--bg-secondary)] text-[var(--text-tertiary)] rounded-full flex-shrink-0"> <span className="px-2 py-0.5 text-xs bg-[var(--bg-secondary)] text-[var(--text-tertiary)] rounded-full flex-shrink-0">
Opcional Opcional
@@ -265,7 +280,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
</div> </div>
{/* Step Content */} {/* Step Content */}
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 220px)' }}> <div className="flex-1 min-h-0 p-4 sm:p-6 overflow-y-auto">
{/* Validation Messages */} {/* Validation Messages */}
{validationError && ( {validationError && (
<div className="mb-4 p-3 sm:p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3 animate-slideDown"> <div className="mb-4 p-3 sm:p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3 animate-slideDown">
@@ -304,7 +319,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
</div> </div>
{/* Footer Navigation */} {/* Footer Navigation */}
<div className="sticky bottom-0 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)]/80 backdrop-blur-md px-4 sm:px-6 py-3 sm:py-4 shadow-lg"> <div className="flex-shrink-0 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)]/80 backdrop-blur-md px-4 sm:px-6 py-3 sm:py-4 shadow-lg">
{/* Keyboard Shortcuts Hint */} {/* Keyboard Shortcuts Hint */}
<div className="hidden md:flex items-center justify-center gap-4 text-xs text-[var(--text-tertiary)] mb-2 pb-2 border-b border-[var(--border-secondary)]/50"> <div className="hidden md:flex items-center justify-center gap-4 text-xs text-[var(--text-tertiary)] mb-2 pb-2 border-b border-[var(--border-secondary)]/50">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">

View File

@@ -337,6 +337,97 @@
}, },
"customerOrder": { "customerOrder": {
"title": "Agregar Pedido", "title": "Agregar Pedido",
"customerTypes": {
"retail": "Minorista",
"wholesale": "Mayorista",
"event": "Evento",
"restaurant": "Restaurante",
"hotel": "Hotel",
"enterprise": "Empresa",
"individual": "Individual",
"business": "Negocio",
"central_bakery": "Panadería Central"
},
"paymentTerms": {
"immediate": "Inmediato",
"net_30": "Neto 30",
"net_60": "Neto 60"
},
"sections": {
"basicInfo": "Información Básica del Pedido",
"deliveryInfo": "Detalles de Entrega",
"paymentInfo": "Detalles de Pago",
"orderSummary": "Resumen del Pedido",
"advancedOptions": "Opciones Avanzadas",
"advancedOptionsDescription": "Campos opcionales para gestión completa de pedidos",
"pricingDetails": "Detalles de Precios",
"productionScheduling": "Producción y Programación",
"fulfillmentTracking": "Cumplimiento y Seguimiento",
"sourceChannel": "Origen y Canal",
"communicationNotes": "Comunicación y Notas",
"notifications": "Notificaciones",
"qualityRequirements": "Calidad y Requisitos",
"additionalOptions": "Opciones Adicionales"
},
"orderTypes": {
"standard": "Estándar",
"custom": "Personalizado",
"bulk": "A Granel",
"urgent": "Urgente"
},
"priorities": {
"low": "Baja",
"normal": "Normal",
"high": "Alta",
"urgent": "Urgente"
},
"statuses": {
"pending": "Pendiente",
"confirmed": "Confirmado",
"in_production": "En Producción",
"ready": "Listo",
"delivered": "Entregado"
},
"deliveryMethods": {
"pickup": "Recogida",
"pickupDesc": "Recogida del cliente",
"delivery": "Entrega",
"deliveryDesc": "Entrega a domicilio",
"shipping": "Envío",
"shippingDesc": "Servicio de mensajería"
},
"paymentMethods": {
"cash": "Efectivo",
"card": "Tarjeta",
"bank_transfer": "Transferencia Bancaria",
"invoice": "Factura",
"account": "Cuenta"
},
"paymentStatuses": {
"pending": "Pendiente",
"partial": "Parcial",
"paid": "Pagado",
"overdue": "Vencido"
},
"orderSources": {
"manual": "Manual",
"phone": "Teléfono",
"email": "Correo Electrónico",
"website": "Sitio Web",
"app": "Aplicación Móvil"
},
"salesChannels": {
"direct": "Directo",
"wholesale": "Mayorista",
"retail": "Minorista",
"online": "En Línea"
},
"qualityCheckStatuses": {
"not_started": "No Iniciado",
"pending": "Pendiente",
"passed": "Aprobado",
"failed": "Reprobado"
},
"steps": { "steps": {
"customerSelection": "Selección de Cliente", "customerSelection": "Selección de Cliente",
"orderItems": "Artículos del Pedido", "orderItems": "Artículos del Pedido",
@@ -348,20 +439,23 @@
"searchPlaceholder": "Buscar clientes...", "searchPlaceholder": "Buscar clientes...",
"createNew": "Crear nuevo cliente", "createNew": "Crear nuevo cliente",
"backToList": "← Volver a la lista de clientes", "backToList": "← Volver a la lista de clientes",
"fields": { "customerName": "Nombre del Cliente",
"customerName": "Nombre del Cliente", "customerNamePlaceholder": "Ej: Restaurante El Molino",
"customerNamePlaceholder": "Ej: Restaurante El Molino", "customerType": "Tipo de Cliente",
"customerType": "Tipo de Cliente", "phone": "Teléfono",
"phone": "Teléfono", "phonePlaceholder": "+34 123 456 789",
"phonePlaceholder": "+34 123 456 789", "email": "Correo Electrónico",
"email": "Correo Electrónico", "emailPlaceholder": "contacto@restaurante.com",
"emailPlaceholder": "contacto@restaurante.com"
},
"customerTypes": { "customerTypes": {
"retail": "Minorista", "retail": "Minorista",
"wholesale": "Mayorista", "wholesale": "Mayorista",
"event": "Evento", "event": "Evento",
"restaurant": "Restaurante" "restaurant": "Restaurante",
"hotel": "Hotel",
"enterprise": "Empresa",
"individual": "Individual",
"business": "Negocio",
"central_bakery": "Panadería Central"
} }
}, },
"orderItems": { "orderItems": {
@@ -371,7 +465,7 @@
"removeItem": "Eliminar artículo", "removeItem": "Eliminar artículo",
"customer": "Cliente", "customer": "Cliente",
"orderProducts": "Productos del Pedido", "orderProducts": "Productos del Pedido",
"productNumber": "Producto #{{number}}", "productNumber": "Producto #{number}",
"product": "Producto", "product": "Producto",
"productPlaceholder": "Seleccionar producto...", "productPlaceholder": "Seleccionar producto...",
"selectProduct": "Seleccionar producto...", "selectProduct": "Seleccionar producto...",
@@ -396,6 +490,74 @@
"deliveryPayment": { "deliveryPayment": {
"title": "Detalles de Entrega y Pago", "title": "Detalles de Entrega y Pago",
"subtitle": "Configurar entrega, pago y detalles del pedido", "subtitle": "Configurar entrega, pago y detalles del pedido",
"requestedDeliveryDate": "Fecha de Entrega Solicitada",
"orderNumberLabel": "Número de Pedido",
"orderNumberTooltip": "Generado automáticamente por el backend al crear el pedido (formato: ORD-AAAAMMDD-####)",
"autoGeneratedLabel": "Auto-generado",
"autoGeneratedPlaceholder": "Se generará automáticamente",
"status": "Estado",
"orderType": "Tipo de Pedido",
"priority": "Prioridad",
"products": "Productos",
"deliveryMethod": "Método de Entrega",
"deliveryAddressPlaceholder": "Calle, número, piso, código postal, ciudad...",
"deliveryContactName": "Nombre de Contacto para Entrega",
"deliveryContactNamePlaceholder": "Persona de contacto",
"deliveryContactPhone": "Teléfono de Contacto para Entrega",
"phoneNumberPlaceholder": "+34 123 456 789",
"paymentTerms": "Términos de Pago",
"paymentStatus": "Estado de Pago",
"paymentDueDate": "Fecha de Vencimiento del Pago",
"discount": "Descuento (%)",
"deliveryFee": "Tarifa de Entrega (€)",
"productionStartDate": "Fecha de Inicio de Producción",
"productionDueDate": "Fecha de Vencimiento de Producción",
"productionBatchNumber": "Número de Lote de Producción",
"deliveryTimeWindow": "Ventana de Tiempo de Entrega",
"deliveryTimeWindowPlaceholder": "Ej: 9:00 AM - 11:00 AM",
"productionNotes": "Notas de Producción",
"productionNotesPlaceholder": "Requisitos especiales de producción o notas",
"shippingTrackingNumber": "Número de Seguimiento de Envío",
"shippingTrackingNumberPlaceholder": "Número de seguimiento",
"shippingCarrier": "Transportista de Envío",
"shippingCarrierPlaceholder": "Ej: DHL, UPS, FedEx",
"pickupLocation": "Ubicación de Recogida",
"pickupLocationPlaceholder": "Ubicación de tienda para recogida",
"actualDeliveryDate": "Fecha Real de Entrega",
"orderSource": "Origen del Pedido",
"salesChannel": "Canal de Ventas",
"salesRepId": "ID del Representante de Ventas",
"salesRepIdPlaceholder": "ID o nombre del representante de ventas",
"customerPurchaseOrder": "Orden de Compra del Cliente #",
"customerPurchaseOrderPlaceholder": "Número de OC del cliente",
"deliveryInstructions": "Instrucciones de Entrega",
"deliveryInstructionsPlaceholder": "Instrucciones especiales de entrega",
"specialInstructions": "Instrucciones Especiales",
"specialInstructionsPlaceholder": "Cualquier requisito o instrucción especial",
"internalNotes": "Notas Internas",
"internalNotesPlaceholder": "Notas internas (no visibles para el cliente)",
"customerNotes": "Notas del Cliente",
"customerNotesPlaceholder": "Notas de/para el cliente",
"notifyOnStatusChange": "Notificar en Cambio de Estado",
"notifyOnDelivery": "Notificar en Entrega",
"notificationEmail": "Correo de Notificación",
"notificationEmailPlaceholder": "cliente@correo.com",
"notificationPhone": "Teléfono de Notificación",
"qualityCheckRequired": "Control de Calidad Requerido",
"qualityCheckStatus": "Estado del Control de Calidad",
"packagingInstructions": "Instrucciones de Empaquetado",
"packagingInstructionsPlaceholder": "Requisitos especiales de empaquetado",
"labelingRequirements": "Requisitos de Etiquetado",
"labelingRequirementsPlaceholder": "Requisitos de etiqueta personalizados",
"recurringOrder": "Pedido Recurrente",
"recurringSchedule": "Programa Recurrente",
"recurringSchedulePlaceholder": "Ej: Semanalmente los lunes, Cada 2 semanas",
"tags": "Etiquetas",
"tagsPlaceholder": "urgente, vip, mayorista",
"tagsTooltip": "Etiquetas separadas por comas para búsqueda y filtrado más fácil",
"metadata": "Metadatos (JSON)",
"metadataPlaceholder": "{\"campo_personalizado\": \"valor\"}",
"metadataTooltip": "Datos personalizados adicionales en formato JSON",
"fields": { "fields": {
"requestedDeliveryDate": "Fecha de Entrega Solicitada", "requestedDeliveryDate": "Fecha de Entrega Solicitada",
"orderNumber": "Número de Pedido", "orderNumber": "Número de Pedido",
@@ -454,11 +616,6 @@
"invoice": "Factura", "invoice": "Factura",
"account": "Cuenta" "account": "Cuenta"
}, },
"paymentTerms": {
"immediate": "Inmediato",
"net_30": "Neto 30",
"net_60": "Neto 60"
},
"paymentStatuses": { "paymentStatuses": {
"pending": "Pendiente", "pending": "Pendiente",
"partial": "Parcial", "partial": "Parcial",
@@ -483,103 +640,103 @@
"pending": "Pendiente", "pending": "Pendiente",
"passed": "Aprobado", "passed": "Aprobado",
"failed": "Reprobado" "failed": "Reprobado"
},
"messages": {
"loadingCustomers": "Cargando clientes...",
"loadingProducts": "Cargando productos...",
"errorLoadingCustomers": "Error al cargar clientes",
"errorLoadingProducts": "Error al cargar productos",
"noCustomersFound": "No se encontraron clientes",
"tryDifferentSearch": "Intenta con un término de búsqueda diferente",
"noProductsInOrder": "No hay productos en el pedido",
"clickAddProduct": "Haz clic en \"Agregar Producto\" para comenzar",
"newCustomer": "Nuevo Cliente",
"customer": "Cliente",
"products": "Productos",
"items": "artículos",
"total": "Total",
"productNumber": "Producto #",
"searchByName": "Buscar cliente por nombre...",
"selectCustomer": "Seleccionar Cliente",
"searchForCustomer": "Buscar un cliente existente o crear uno nuevo",
"orderItems": "Artículos del Pedido",
"addProducts": "Agregar Productos al Pedido",
"customerLabel": "Cliente:",
"productsLabel": "Productos:",
"totalLabel": "Total:",
"orderTotal": "Total del Pedido:",
"newCustomerHeader": "Nuevo Cliente",
"orderProducts": "Productos del Pedido",
"addProduct": "Agregar Producto",
"removeItem": "Eliminar artículo",
"optionalEmail": "Correo Electrónico (Opcional)",
"readOnlyAutoGenerated": "Número de Pedido (Solo lectura - Auto-generado)",
"willBeGeneratedAutomatically": "Se generará automáticamente",
"autoGeneratedOnSave": "Auto-generado al guardar",
"orderNumberFormat": "formato: ORD-AAAAMMDD-####",
"selectProduct": "Seleccionar producto...",
"deliveryAddress": "Dirección de Entrega",
"deliveryAddressPlaceholder": "Calle, número, piso, código postal, ciudad...",
"deliveryContactName": "Nombre de Contacto para Entrega",
"deliveryContactNamePlaceholder": "Persona de contacto",
"deliveryContactPhone": "Teléfono de Contacto para Entrega",
"deliveryMethod": "Método de Entrega",
"paymentMethod": "Método de Pago",
"paymentTerms": "Términos de Pago",
"paymentStatus": "Estado de Pago",
"paymentDueDate": "Fecha de Vencimiento del Pago",
"discountPercent": "Descuento (%)",
"deliveryFee": "Tarifa de Entrega (€)",
"productionStartDate": "Fecha de Inicio de Producción",
"productionDueDate": "Fecha de Vencimiento de Producción",
"productionBatchNumber": "Número de Lote de Producción",
"productionBatchNumberPlaceholder": "LOTE-001",
"deliveryTimeWindow": "Ventana de Tiempo de Entrega",
"deliveryTimeWindowPlaceholder": "Ej: 9:00 AM - 11:00 AM",
"productionNotes": "Notas de Producción",
"productionNotesPlaceholder": "Requisitos especiales de producción o notas",
"shippingTrackingNumber": "Número de Seguimiento de Envío",
"shippingTrackingNumberPlaceholder": "Número de seguimiento",
"shippingCarrier": "Transportista de Envío",
"shippingCarrierPlaceholder": "Ej: DHL, UPS, FedEx",
"pickupLocation": "Ubicación de Recogida",
"pickupLocationPlaceholder": "Ubicación de tienda para recogida",
"actualDeliveryDate": "Fecha Real de Entrega",
"orderSource": "Origen del Pedido",
"salesChannel": "Canal de Ventas",
"salesRepId": "ID del Representante de Ventas",
"salesRepIdPlaceholder": "ID o nombre del representante de ventas",
"customerPurchaseOrder": "Orden de Compra del Cliente #",
"customerPurchaseOrderPlaceholder": "Número de OC del cliente",
"deliveryInstructions": "Instrucciones de Entrega",
"deliveryInstructionsPlaceholder": "Instrucciones especiales de entrega",
"specialInstructions": "Instrucciones Especiales",
"specialInstructionsPlaceholder": "Cualquier requisito o instrucción especial",
"internalNotes": "Notas Internas",
"internalNotesPlaceholder": "Notas internas (no visibles para el cliente)",
"customerNotes": "Notas del Cliente",
"customerNotesPlaceholder": "Notas de/para el cliente",
"notifyOnStatusChange": "Notificar en Cambio de Estado",
"notifyOnDelivery": "Notificar en Entrega",
"notificationEmail": "Correo de Notificación",
"notificationEmailPlaceholder": "cliente@correo.com",
"notificationPhone": "Teléfono de Notificación",
"qualityCheckRequired": "Control de Calidad Requerido",
"qualityCheckStatus": "Estado del Control de Calidad",
"packagingInstructions": "Instrucciones de Empaquetado",
"packagingInstructionsPlaceholder": "Requisitos especiales de empaquetado",
"labelingRequirements": "Requisitos de Etiquetado",
"labelingRequirementsPlaceholder": "Requisitos de etiqueta personalizados",
"recurringOrder": "Pedido Recurrente",
"recurringSchedule": "Programa Recurrente",
"recurringSchedulePlaceholder": "Ej: Semanalmente los lunes, Cada 2 semanas",
"tags": "Etiquetas",
"tagsPlaceholder": "urgente, vip, mayorista",
"tagsTooltip": "Etiquetas separadas por comas para búsqueda y filtrado más fácil",
"metadata": "Metadatos (JSON)",
"metadataPlaceholder": "{\"campo_personalizado\": \"valor\"}",
"metadataTooltip": "Datos personalizados adicionales en formato JSON"
} }
},
"messages": {
"loadingCustomers": "Cargando clientes...",
"loadingProducts": "Cargando productos...",
"errorLoadingCustomers": "Error al cargar clientes",
"errorLoadingProducts": "Error al cargar productos",
"noCustomersFound": "No se encontraron clientes",
"tryDifferentSearch": "Intenta con un término de búsqueda diferente",
"noProductsInOrder": "No hay productos en el pedido",
"clickAddProduct": "Haz clic en \"Agregar Producto\" para comenzar",
"newCustomer": "Nuevo Cliente",
"customer": "Cliente",
"products": "Productos",
"items": "artículos",
"total": "Total",
"productNumber": "Producto #",
"searchByName": "Buscar cliente por nombre...",
"selectCustomer": "Seleccionar Cliente",
"searchForCustomer": "Buscar un cliente existente o crear uno nuevo",
"orderItems": "Artículos del Pedido",
"addProducts": "Agregar Productos al Pedido",
"customerLabel": "Cliente:",
"productsLabel": "Productos:",
"totalLabel": "Total:",
"orderTotal": "Total del Pedido:",
"newCustomerHeader": "Nuevo Cliente",
"orderProducts": "Productos del Pedido",
"addProduct": "Agregar Producto",
"removeItem": "Eliminar artículo",
"optionalEmail": "Correo Electrónico (Opcional)",
"readOnlyAutoGenerated": "Número de Pedido (Solo lectura - Auto-generado)",
"willBeGeneratedAutomatically": "Se generará automáticamente",
"autoGeneratedOnSave": "Auto-generado al guardar",
"orderNumberFormat": "formato: ORD-AAAAMMDD-####",
"selectProduct": "Seleccionar producto...",
"deliveryAddress": "Dirección de Entrega",
"deliveryAddressPlaceholder": "Calle, número, piso, código postal, ciudad...",
"deliveryContactName": "Nombre de Contacto para Entrega",
"deliveryContactNamePlaceholder": "Persona de contacto",
"deliveryContactPhone": "Teléfono de Contacto para Entrega",
"deliveryMethod": "Método de Entrega",
"paymentMethod": "Método de Pago",
"paymentTerms": "Términos de Pago",
"paymentStatus": "Estado de Pago",
"paymentDueDate": "Fecha de Vencimiento del Pago",
"discountPercent": "Descuento (%)",
"deliveryFee": "Tarifa de Entrega (€)",
"productionStartDate": "Fecha de Inicio de Producción",
"productionDueDate": "Fecha de Vencimiento de Producción",
"productionBatchNumber": "Número de Lote de Producción",
"productionBatchNumberPlaceholder": "LOTE-001",
"deliveryTimeWindow": "Ventana de Tiempo de Entrega",
"deliveryTimeWindowPlaceholder": "Ej: 9:00 AM - 11:00 AM",
"productionNotes": "Notas de Producción",
"productionNotesPlaceholder": "Requisitos especiales de producción o notas",
"shippingTrackingNumber": "Número de Seguimiento de Envío",
"shippingTrackingNumberPlaceholder": "Número de seguimiento",
"shippingCarrier": "Transportista de Envío",
"shippingCarrierPlaceholder": "Ej: DHL, UPS, FedEx",
"pickupLocation": "Ubicación de Recogida",
"pickupLocationPlaceholder": "Ubicación de tienda para recogida",
"actualDeliveryDate": "Fecha Real de Entrega",
"orderSource": "Origen del Pedido",
"salesChannel": "Canal de Ventas",
"salesRepId": "ID del Representante de Ventas",
"salesRepIdPlaceholder": "ID o nombre del representante de ventas",
"customerPurchaseOrder": "Orden de Compra del Cliente #",
"customerPurchaseOrderPlaceholder": "Número de OC del cliente",
"deliveryInstructions": "Instrucciones de Entrega",
"deliveryInstructionsPlaceholder": "Instrucciones especiales de entrega",
"specialInstructions": "Instrucciones Especiales",
"specialInstructionsPlaceholder": "Cualquier requisito o instrucción especial",
"internalNotes": "Notas Internas",
"internalNotesPlaceholder": "Notas internas (no visibles para el cliente)",
"customerNotes": "Notas del Cliente",
"customerNotesPlaceholder": "Notas de/para el cliente",
"notifyOnStatusChange": "Notificar en Cambio de Estado",
"notifyOnDelivery": "Notificar en Entrega",
"notificationEmail": "Correo de Notificación",
"notificationEmailPlaceholder": "cliente@correo.com",
"notificationPhone": "Teléfono de Notificación",
"qualityCheckRequired": "Control de Calidad Requerido",
"qualityCheckStatus": "Estado del Control de Calidad",
"packagingInstructions": "Instrucciones de Empaquetado",
"packagingInstructionsPlaceholder": "Requisitos especiales de empaquetado",
"labelingRequirements": "Requisitos de Etiquetado",
"labelingRequirementsPlaceholder": "Requisitos de etiqueta personalizados",
"recurringOrder": "Pedido Recurrente",
"recurringSchedule": "Programa Recurrente",
"recurringSchedulePlaceholder": "Ej: Semanalmente los lunes, Cada 2 semanas",
"tags": "Etiquetas",
"tagsPlaceholder": "urgente, vip, mayorista",
"tagsTooltip": "Etiquetas separadas por comas para búsqueda y filtrado más fácil",
"metadata": "Metadatos (JSON)",
"metadataPlaceholder": "{\"campo_personalizado\": \"valor\"}",
"metadataTooltip": "Datos personalizados adicionales en formato JSON"
} }
}, },
"itemTypeSelector": { "itemTypeSelector": {

View File

@@ -8,7 +8,6 @@ import {
Target, Target,
Droplets, Droplets,
TreeDeciduous, TreeDeciduous,
Calendar,
Download, Download,
FileText, FileText,
Info Info
@@ -45,34 +44,34 @@ const SustainabilityPage: React.FC = () => {
['WASTE METRICS'], ['WASTE METRICS'],
['Total Waste (kg)', metrics.waste_metrics.total_waste_kg.toFixed(2)], ['Total Waste (kg)', metrics.waste_metrics.total_waste_kg.toFixed(2)],
['Production Waste (kg)', metrics.waste_metrics.production_waste_kg.toFixed(2)], ['Production Waste (kg)', metrics.waste_metrics.production_waste_kg.toFixed(2)],
['Inventory Waste (kg)', metrics.waste_metrics.inventory_waste_kg.toFixed(2)], ['Expired Waste (kg)', metrics.waste_metrics.expired_waste_kg.toFixed(2)],
['Waste Percentage (%)', metrics.waste_metrics.waste_percentage.toFixed(2)], ['Waste Percentage (%)', metrics.waste_metrics.waste_percentage.toFixed(2)],
[], [],
['SDG 12.3 COMPLIANCE'], ['SDG 12.3 COMPLIANCE'],
['Status', metrics.sdg_compliance.sdg_12_3.status_label], ['Status', metrics.sdg_compliance.sdg_12_3.status_label],
['Reduction Achieved (%)', metrics.sdg_compliance.sdg_12_3.reduction_achieved.toFixed(2)], ['Reduction Achieved (%)', metrics.sdg_compliance.sdg_12_3.reduction_achieved.toFixed(2)],
['Progress to Target (%)', metrics.sdg_compliance.sdg_12_3.progress_to_target.toFixed(2)], ['Progress to Target (%)', metrics.sdg_compliance.sdg_12_3.progress_to_target.toFixed(2)],
['Target (%)', metrics.sdg_compliance.sdg_12_3.target_percentage], ['Target (%)', metrics.sdg_compliance.sdg_12_3.target_reduction],
[], [],
['ENVIRONMENTAL IMPACT'], ['ENVIRONMENTAL IMPACT'],
['CO2 Emissions (kg)', metrics.environmental_impact.co2_emissions.kg.toFixed(2)], ['CO2 Emissions (kg)', metrics.environmental_impact.co2_emissions.kg.toFixed(2)],
['CO2 Emissions (tons)', metrics.environmental_impact.co2_emissions.tons.toFixed(4)], ['CO2 Emissions (tons)', metrics.environmental_impact.co2_emissions.tons.toFixed(4)],
['Trees to Offset', metrics.environmental_impact.co2_emissions.trees_to_offset.toFixed(1)], ['Trees to Offset', metrics.environmental_impact.co2_emissions.trees_to_offset.toFixed(1)],
['Equivalent Car KM', metrics.environmental_impact.co2_emissions.car_km_equivalent.toFixed(0)], ['Equivalent Car KM', metrics.environmental_impact.human_equivalents.car_km_equivalent.toFixed(0)],
['Water Footprint (liters)', metrics.environmental_impact.water_footprint.liters.toFixed(0)], ['Water Footprint (liters)', metrics.environmental_impact.water_footprint.liters.toFixed(0)],
['Water Footprint (m³)', metrics.environmental_impact.water_footprint.cubic_meters.toFixed(2)], ['Water Footprint (m³)', metrics.environmental_impact.water_footprint.cubic_meters.toFixed(2)],
['Equivalent Showers', metrics.environmental_impact.water_footprint.shower_equivalent.toFixed(0)], ['Equivalent Showers', metrics.environmental_impact.human_equivalents.showers_equivalent.toFixed(0)],
['Land Use (m²)', metrics.environmental_impact.land_use.square_meters.toFixed(2)], ['Land Use (m²)', metrics.environmental_impact.land_use.square_meters.toFixed(2)],
['Land Use (hectares)', metrics.environmental_impact.land_use.hectares.toFixed(4)], ['Land Use (hectares)', metrics.environmental_impact.land_use.hectares.toFixed(4)],
[], [],
['FINANCIAL IMPACT'], ['FINANCIAL IMPACT'],
['Waste Cost (EUR)', metrics.financial_impact.waste_cost_eur.toFixed(2)], ['Waste Cost (EUR)', metrics.financial_impact.waste_cost_eur.toFixed(2)],
['Potential Monthly Savings (EUR)', metrics.financial_impact.potential_monthly_savings.toFixed(2)], ['Potential Monthly Savings (EUR)', metrics.financial_impact.potential_monthly_savings.toFixed(2)],
['ROI on Prevention (%)', metrics.financial_impact.roi_on_waste_prevention.toFixed(2)], ['Annual Projection (EUR)', metrics.financial_impact.annual_projection.toFixed(2)],
[], [],
['AVOIDED WASTE (AI PREDICTIONS)'], ['AVOIDED WASTE (AI PREDICTIONS)'],
['Waste Avoided (kg)', metrics.avoided_waste.total_waste_avoided_kg.toFixed(2)], ['Waste Avoided (kg)', metrics.avoided_waste.waste_avoided_kg.toFixed(2)],
['Cost Savings (EUR)', metrics.avoided_waste.cost_savings_eur.toFixed(2)], ['AI Assisted Batches', metrics.avoided_waste.ai_assisted_batches],
['CO2 Avoided (kg)', metrics.avoided_waste.environmental_impact_avoided.co2_kg.toFixed(2)], ['CO2 Avoided (kg)', metrics.avoided_waste.environmental_impact_avoided.co2_kg.toFixed(2)],
['Water Saved (liters)', metrics.avoided_waste.environmental_impact_avoided.water_liters.toFixed(0)], ['Water Saved (liters)', metrics.avoided_waste.environmental_impact_avoided.water_liters.toFixed(0)],
[], [],
@@ -214,76 +213,6 @@ 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 ( return (
<div className="space-y-6 p-4 sm:p-6"> <div className="space-y-6 p-4 sm:p-6">
{/* Page Header */} {/* Page Header */}