Fix UI issues 2
This commit is contained in:
@@ -162,7 +162,8 @@ const EnvironmentalConstants = {
|
||||
LAND_USE_PER_KG: 3.4, // m² per kg
|
||||
TREES_PER_TON_CO2: 50,
|
||||
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 =====
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -337,11 +410,22 @@ export async function getSustainabilityMetrics(
|
||||
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
|
||||
const productionWaste = (productionData.total_production_waste || 0) + (productionData.total_defects || 0);
|
||||
const totalWasteKg = productionWaste + (inventoryData.inventory_waste_kg || 0);
|
||||
|
||||
const totalProductionKg = productionData.total_planned || 0;
|
||||
const wastePercentage = totalProductionKg > 0
|
||||
? (totalWasteKg / totalProductionKg) * 100
|
||||
: 0;
|
||||
@@ -403,7 +487,10 @@ export async function getSustainabilityMetrics(
|
||||
sdg_compliance: sdgCompliance,
|
||||
avoided_waste: avoidedWaste,
|
||||
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) {
|
||||
|
||||
@@ -114,6 +114,10 @@ export interface SustainabilityMetrics {
|
||||
avoided_waste: AvoidedWaste;
|
||||
financial_impact: FinancialImpact;
|
||||
grant_readiness: GrantReadiness;
|
||||
// Data sufficiency flags
|
||||
data_sufficient: boolean;
|
||||
minimum_production_required_kg?: number;
|
||||
current_production_kg?: number;
|
||||
}
|
||||
|
||||
export interface SustainabilityWidgetData {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Users,
|
||||
UserPlus,
|
||||
Euro as EuroIcon,
|
||||
Sparkles,
|
||||
FileText,
|
||||
Factory,
|
||||
Search,
|
||||
@@ -159,21 +158,6 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="space-y-3">
|
||||
{/* Search Bar */}
|
||||
@@ -256,7 +240,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`
|
||||
@@ -272,8 +256,8 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1 text-left">
|
||||
<h3 className="text-base md:text-lg font-semibold text-[var(--text-primary)] mb-0.5 group-hover:text-[var(--color-primary)] transition-colors">
|
||||
<div className="flex-1 text-left pr-16">
|
||||
<h3 className="text-base md:text-lg font-semibold text-[var(--text-primary)] group-hover:text-[var(--color-primary)] transition-colors">
|
||||
{itemType.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] leading-snug mt-1">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, ChevronLeft, ChevronRight, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
|
||||
export interface WizardStep {
|
||||
@@ -48,12 +49,26 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
dataRef,
|
||||
onDataChange
|
||||
}) => {
|
||||
const { t } = useTranslation('wizards');
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [validationSuccess, setValidationSuccess] = useState(false);
|
||||
|
||||
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 isLastStep = currentStepIndex === steps.length - 1;
|
||||
|
||||
@@ -178,11 +193,11 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
||||
<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()}
|
||||
>
|
||||
{/* 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 */}
|
||||
<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">
|
||||
@@ -232,7 +247,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
? 'bg-[var(--color-primary)] shadow-md'
|
||||
: '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 && (
|
||||
<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 */}
|
||||
<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>
|
||||
</div>
|
||||
@@ -250,7 +265,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs sm:text-sm">
|
||||
<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 && (
|
||||
<span className="px-2 py-0.5 text-xs bg-[var(--bg-secondary)] text-[var(--text-tertiary)] rounded-full flex-shrink-0">
|
||||
Opcional
|
||||
@@ -265,7 +280,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
{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">
|
||||
@@ -304,7 +319,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<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">
|
||||
|
||||
@@ -337,6 +337,97 @@
|
||||
},
|
||||
"customerOrder": {
|
||||
"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": {
|
||||
"customerSelection": "Selección de Cliente",
|
||||
"orderItems": "Artículos del Pedido",
|
||||
@@ -348,20 +439,23 @@
|
||||
"searchPlaceholder": "Buscar clientes...",
|
||||
"createNew": "Crear nuevo cliente",
|
||||
"backToList": "← Volver a la lista de clientes",
|
||||
"fields": {
|
||||
"customerName": "Nombre del Cliente",
|
||||
"customerNamePlaceholder": "Ej: Restaurante El Molino",
|
||||
"customerType": "Tipo de Cliente",
|
||||
"phone": "Teléfono",
|
||||
"phonePlaceholder": "+34 123 456 789",
|
||||
"email": "Correo Electrónico",
|
||||
"emailPlaceholder": "contacto@restaurante.com"
|
||||
},
|
||||
"emailPlaceholder": "contacto@restaurante.com",
|
||||
"customerTypes": {
|
||||
"retail": "Minorista",
|
||||
"wholesale": "Mayorista",
|
||||
"event": "Evento",
|
||||
"restaurant": "Restaurante"
|
||||
"restaurant": "Restaurante",
|
||||
"hotel": "Hotel",
|
||||
"enterprise": "Empresa",
|
||||
"individual": "Individual",
|
||||
"business": "Negocio",
|
||||
"central_bakery": "Panadería Central"
|
||||
}
|
||||
},
|
||||
"orderItems": {
|
||||
@@ -371,7 +465,7 @@
|
||||
"removeItem": "Eliminar artículo",
|
||||
"customer": "Cliente",
|
||||
"orderProducts": "Productos del Pedido",
|
||||
"productNumber": "Producto #{{number}}",
|
||||
"productNumber": "Producto #{number}",
|
||||
"product": "Producto",
|
||||
"productPlaceholder": "Seleccionar producto...",
|
||||
"selectProduct": "Seleccionar producto...",
|
||||
@@ -396,6 +490,74 @@
|
||||
"deliveryPayment": {
|
||||
"title": "Detalles de Entrega y Pago",
|
||||
"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": {
|
||||
"requestedDeliveryDate": "Fecha de Entrega Solicitada",
|
||||
"orderNumber": "Número de Pedido",
|
||||
@@ -454,11 +616,6 @@
|
||||
"invoice": "Factura",
|
||||
"account": "Cuenta"
|
||||
},
|
||||
"paymentTerms": {
|
||||
"immediate": "Inmediato",
|
||||
"net_30": "Neto 30",
|
||||
"net_60": "Neto 60"
|
||||
},
|
||||
"paymentStatuses": {
|
||||
"pending": "Pendiente",
|
||||
"partial": "Parcial",
|
||||
@@ -483,6 +640,7 @@
|
||||
"pending": "Pendiente",
|
||||
"passed": "Aprobado",
|
||||
"failed": "Reprobado"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"loadingCustomers": "Cargando clientes...",
|
||||
@@ -580,7 +738,6 @@
|
||||
"metadataPlaceholder": "{\"campo_personalizado\": \"valor\"}",
|
||||
"metadataTooltip": "Datos personalizados adicionales en formato JSON"
|
||||
}
|
||||
}
|
||||
},
|
||||
"itemTypeSelector": {
|
||||
"title": "Seleccionar Tipo",
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Target,
|
||||
Droplets,
|
||||
TreeDeciduous,
|
||||
Calendar,
|
||||
Download,
|
||||
FileText,
|
||||
Info
|
||||
@@ -45,34 +44,34 @@ const SustainabilityPage: React.FC = () => {
|
||||
['WASTE METRICS'],
|
||||
['Total Waste (kg)', metrics.waste_metrics.total_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)],
|
||||
[],
|
||||
['SDG 12.3 COMPLIANCE'],
|
||||
['Status', metrics.sdg_compliance.sdg_12_3.status_label],
|
||||
['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)],
|
||||
['Target (%)', metrics.sdg_compliance.sdg_12_3.target_percentage],
|
||||
['Target (%)', metrics.sdg_compliance.sdg_12_3.target_reduction],
|
||||
[],
|
||||
['ENVIRONMENTAL IMPACT'],
|
||||
['CO2 Emissions (kg)', metrics.environmental_impact.co2_emissions.kg.toFixed(2)],
|
||||
['CO2 Emissions (tons)', metrics.environmental_impact.co2_emissions.tons.toFixed(4)],
|
||||
['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 (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 (hectares)', metrics.environmental_impact.land_use.hectares.toFixed(4)],
|
||||
[],
|
||||
['FINANCIAL IMPACT'],
|
||||
['Waste Cost (EUR)', metrics.financial_impact.waste_cost_eur.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)'],
|
||||
['Waste Avoided (kg)', metrics.avoided_waste.total_waste_avoided_kg.toFixed(2)],
|
||||
['Cost Savings (EUR)', metrics.avoided_waste.cost_savings_eur.toFixed(2)],
|
||||
['Waste Avoided (kg)', metrics.avoided_waste.waste_avoided_kg.toFixed(2)],
|
||||
['AI Assisted Batches', metrics.avoided_waste.ai_assisted_batches],
|
||||
['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)],
|
||||
[],
|
||||
@@ -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 (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
{/* Page Header */}
|
||||
|
||||
Reference in New Issue
Block a user