import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { PageHeader } from '../../components/layout'; import StatsGrid from '../../components/ui/Stats/StatsGrid'; import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts'; import { IncompleteIngredientsAlert } from '../../components/domain/dashboard/IncompleteIngredientsAlert'; import { ConfigurationProgressWidget } from '../../components/domain/dashboard/ConfigurationProgressWidget'; import PendingPOApprovals from '../../components/domain/dashboard/PendingPOApprovals'; import TodayProduction from '../../components/domain/dashboard/TodayProduction'; // Sustainability widget removed - now using stats in StatsGrid import { EditViewModal } from '../../components/ui'; import { useTenant } from '../../stores/tenant.store'; import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding'; import { useDashboardStats } from '../../api/hooks/dashboard'; import { usePurchaseOrder, useApprovePurchaseOrder, useRejectPurchaseOrder } from '../../api/hooks/purchase-orders'; import { useBatchDetails, useUpdateBatchStatus } from '../../api/hooks/production'; import { useRunDailyWorkflow } from '../../api'; import { ProductionStatusEnum } from '../../api'; import { AlertTriangle, Clock, Euro, Package, FileText, Building2, Calendar, CheckCircle, X, ShoppingCart, Factory, Timer, TrendingDown, Leaf, Play } from 'lucide-react'; import { showToast } from '../../utils/toast'; const DashboardPage: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { availableTenants, currentTenant } = useTenant(); const { startTour } = useDemoTour(); const isDemoMode = localStorage.getItem('demo_mode') === 'true'; // Modal state management const [selectedPOId, setSelectedPOId] = useState(null); const [selectedBatchId, setSelectedBatchId] = useState(null); const [showPOModal, setShowPOModal] = useState(false); const [showBatchModal, setShowBatchModal] = useState(false); const [approvalNotes, setApprovalNotes] = useState(''); // Fetch real dashboard statistics const { data: dashboardStats, isLoading: isLoadingStats, error: statsError } = useDashboardStats( currentTenant?.id || '', { enabled: !!currentTenant?.id, } ); // Fetch PO details when modal is open const { data: poDetails, isLoading: isLoadingPO } = usePurchaseOrder( currentTenant?.id || '', selectedPOId || '', { enabled: !!currentTenant?.id && !!selectedPOId && showPOModal } ); // Fetch Production batch details when modal is open const { data: batchDetails, isLoading: isLoadingBatch } = useBatchDetails( currentTenant?.id || '', selectedBatchId || '', { enabled: !!currentTenant?.id && !!selectedBatchId && showBatchModal } ); // Mutations const approvePOMutation = useApprovePurchaseOrder(); const rejectPOMutation = useRejectPurchaseOrder(); const updateBatchStatusMutation = useUpdateBatchStatus(); const orchestratorMutation = useRunDailyWorkflow(); const handleRunOrchestrator = async () => { try { await orchestratorMutation.mutateAsync(currentTenant?.id || ''); showToast.success('Flujo de planificación ejecutado exitosamente'); } catch (error) { console.error('Error running orchestrator:', error); showToast.error('Error al ejecutar flujo de planificación'); } }; useEffect(() => { console.log('[Dashboard] Demo mode:', isDemoMode); console.log('[Dashboard] Should start tour:', shouldStartTour()); console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start')); console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step')); // Check if there's a tour intent from redirection (higher priority) const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true'; const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10); if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) { console.log('[Dashboard] Starting tour in 1.5s...'); const timer = setTimeout(() => { console.log('[Dashboard] Executing startTour()'); if (shouldStartFromRedirect) { // Start tour from the specific step that was intended startTour(redirectStartStep); // Clear the redirect intent sessionStorage.removeItem('demo_tour_should_start'); sessionStorage.removeItem('demo_tour_start_step'); } else { // Start tour normally (from beginning or resume) startTour(); clearTourStartPending(); } }, 1500); return () => clearTimeout(timer); } }, [isDemoMode, startTour]); const handleViewAllProcurement = () => { navigate('/app/operations/procurement'); }; const handleViewAllProduction = () => { navigate('/app/operations/production'); }; const handleOrderItem = (itemId: string) => { console.log('Ordering item:', itemId); navigate('/app/operations/procurement'); }; const handleStartBatch = async (batchId: string) => { try { await updateBatchStatusMutation.mutateAsync({ tenantId: currentTenant?.id || '', batchId, statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS } }); showToast.success('Lote iniciado'); } catch (error) { console.error('Error starting batch:', error); showToast.error('Error al iniciar lote'); } }; const handlePauseBatch = async (batchId: string) => { try { await updateBatchStatusMutation.mutateAsync({ tenantId: currentTenant?.id || '', batchId, statusUpdate: { status: ProductionStatusEnum.ON_HOLD } }); showToast.success('Lote pausado'); } catch (error) { console.error('Error pausing batch:', error); showToast.error('Error al pausar lote'); } }; const handleViewDetails = (batchId: string) => { setSelectedBatchId(batchId); setShowBatchModal(true); }; const handleApprovePO = async (poId: string) => { try { await approvePOMutation.mutateAsync({ tenantId: currentTenant?.id || '', poId, notes: 'Aprobado desde el dashboard' }); showToast.success('Orden aprobada'); } catch (error) { console.error('Error approving PO:', error); showToast.error('Error al aprobar orden'); } }; const handleRejectPO = async (poId: string) => { try { await rejectPOMutation.mutateAsync({ tenantId: currentTenant?.id || '', poId, reason: 'Rechazado desde el dashboard' }); showToast.success('Orden rechazada'); } catch (error) { console.error('Error rejecting PO:', error); showToast.error('Error al rechazar orden'); } }; const handleViewPODetails = (poId: string) => { setSelectedPOId(poId); setShowPOModal(true); }; const handleViewAllPOs = () => { navigate('/app/operations/procurement'); }; // Build stats from real API data (Sales analytics removed - Professional/Enterprise tier only) const criticalStats = React.useMemo(() => { if (!dashboardStats) { // Return loading/empty state return []; } // Determine trend direction const getTrendDirection = (value: number): 'up' | 'down' | 'neutral' => { if (value > 0) return 'up'; if (value < 0) return 'down'; return 'neutral'; }; return [ { title: t('dashboard:stats.pending_orders', 'Pending Orders'), value: dashboardStats.pendingOrders.toString(), icon: Clock, variant: dashboardStats.pendingOrders > 10 ? ('warning' as const) : ('info' as const), trend: dashboardStats.ordersTrend !== 0 ? { value: Math.abs(dashboardStats.ordersTrend), direction: getTrendDirection(dashboardStats.ordersTrend), label: t('dashboard:trends.vs_yesterday', '% vs yesterday') } : undefined, subtitle: dashboardStats.pendingOrders > 0 ? t('dashboard:messages.require_attention', 'Require attention') : t('dashboard:messages.all_caught_up', 'All caught up!') }, { title: t('dashboard:stats.stock_alerts', 'Critical Stock'), value: dashboardStats.criticalStock.toString(), icon: AlertTriangle, variant: dashboardStats.criticalStock > 0 ? ('error' as const) : ('success' as const), trend: undefined, // Stock alerts don't have historical trends subtitle: dashboardStats.criticalStock > 0 ? t('dashboard:messages.action_required', 'Action required') : t('dashboard:messages.stock_healthy', 'Stock levels healthy') }, { title: t('dashboard:stats.waste_reduction', 'Waste Reduction'), value: dashboardStats.wasteReductionPercentage ? `${Math.abs(dashboardStats.wasteReductionPercentage).toFixed(1)}%` : '0%', icon: TrendingDown, variant: (dashboardStats.wasteReductionPercentage || 0) >= 15 ? ('success' as const) : ('info' as const), trend: undefined, subtitle: (dashboardStats.wasteReductionPercentage || 0) >= 15 ? t('dashboard:messages.excellent_progress', 'Excellent progress!') : t('dashboard:messages.keep_improving', 'Keep improving') }, { title: t('dashboard:stats.monthly_savings', 'Monthly Savings'), value: dashboardStats.monthlySavingsEur ? `€${dashboardStats.monthlySavingsEur.toFixed(0)}` : '€0', icon: Leaf, variant: 'success' as const, trend: undefined, subtitle: t('dashboard:messages.from_sustainability', 'From sustainability') } ]; }, [dashboardStats, t]); // Helper function to build PO detail sections (reused from ProcurementPage) const buildPODetailsSections = (po: any) => { if (!po) return []; const getPOStatusConfig = (status: string) => { const normalizedStatus = status?.toUpperCase().replace(/_/g, '_'); const configs: Record = { PENDING_APPROVAL: { text: 'Pendiente de Aprobación', color: 'var(--color-warning)' }, APPROVED: { text: 'Aprobado', color: 'var(--color-success)' }, SENT_TO_SUPPLIER: { text: 'Enviado al Proveedor', color: 'var(--color-info)' }, CONFIRMED: { text: 'Confirmado', color: 'var(--color-success)' }, RECEIVED: { text: 'Recibido', color: 'var(--color-success)' }, COMPLETED: { text: 'Completado', color: 'var(--color-success)' }, CANCELLED: { text: 'Cancelado', color: 'var(--color-error)' }, }; return configs[normalizedStatus] || { text: status, color: 'var(--color-info)' }; }; const statusConfig = getPOStatusConfig(po.status); return [ { title: 'Información General', icon: FileText, fields: [ { label: 'Número de Orden', value: po.po_number, type: 'text' as const }, { label: 'Estado', value: statusConfig.text, type: 'status' as const }, { label: 'Prioridad', value: po.priority === 'urgent' ? 'Urgente' : po.priority === 'high' ? 'Alta' : po.priority === 'low' ? 'Baja' : 'Normal', type: 'text' as const }, { label: 'Fecha de Creación', value: new Date(po.created_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }), type: 'text' as const } ] }, { title: 'Información del Proveedor', icon: Building2, fields: [ { label: 'Proveedor', value: po.supplier?.name || po.supplier_name || 'N/A', type: 'text' as const }, { label: 'Email', value: po.supplier?.contact_email || 'N/A', type: 'text' as const }, { label: 'Teléfono', value: po.supplier?.contact_phone || 'N/A', type: 'text' as const } ] }, { title: 'Resumen Financiero', icon: Euro, fields: [ { label: 'Subtotal', value: `€${(typeof po.subtotal === 'string' ? parseFloat(po.subtotal) : po.subtotal || 0).toFixed(2)}`, type: 'text' as const }, { label: 'Impuestos', value: `€${(typeof po.tax_amount === 'string' ? parseFloat(po.tax_amount) : po.tax_amount || 0).toFixed(2)}`, type: 'text' as const }, { label: 'TOTAL', value: `€${(typeof po.total_amount === 'string' ? parseFloat(po.total_amount) : po.total_amount || 0).toFixed(2)}`, type: 'text' as const, highlight: true } ] }, { title: 'Entrega', icon: Calendar, fields: [ { label: 'Fecha Requerida', value: po.required_delivery_date ? new Date(po.required_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }) : 'No especificada', type: 'text' as const }, { label: 'Fecha Esperada', value: po.expected_delivery_date ? new Date(po.expected_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }) : 'No especificada', type: 'text' as const } ] } ]; }; // Helper function to build Production batch detail sections const buildBatchDetailsSections = (batch: any) => { if (!batch) return []; return [ { title: 'Información General', icon: Package, fields: [ { label: 'Producto', value: batch.product_name, type: 'text' as const, highlight: true }, { label: 'Número de Lote', value: batch.batch_number, type: 'text' as const }, { label: 'Cantidad Planificada', value: `${batch.planned_quantity} unidades`, type: 'text' as const }, { label: 'Cantidad Real', value: batch.actual_quantity ? `${batch.actual_quantity} unidades` : 'Pendiente', type: 'text' as const }, { label: 'Estado', value: batch.status, type: 'text' as const }, { label: 'Prioridad', value: batch.priority, type: 'text' as const } ] }, { title: 'Cronograma', icon: Clock, fields: [ { label: 'Inicio Planificado', value: batch.planned_start_time ? new Date(batch.planned_start_time).toLocaleString('es-ES') : 'No especificado', type: 'text' as const }, { label: 'Fin Planificado', value: batch.planned_end_time ? new Date(batch.planned_end_time).toLocaleString('es-ES') : 'No especificado', type: 'text' as const }, { label: 'Inicio Real', value: batch.actual_start_time ? new Date(batch.actual_start_time).toLocaleString('es-ES') : 'Pendiente', type: 'text' as const }, { label: 'Fin Real', value: batch.actual_end_time ? new Date(batch.actual_end_time).toLocaleString('es-ES') : 'Pendiente', type: 'text' as const } ] }, { title: 'Producción', icon: Factory, fields: [ { label: 'Personal Asignado', value: batch.staff_assigned?.join(', ') || 'No asignado', type: 'text' as const }, { label: 'Estación', value: batch.station_id || 'No asignada', type: 'text' as const }, { label: 'Duración Planificada', value: batch.planned_duration_minutes ? `${batch.planned_duration_minutes} minutos` : 'No especificada', type: 'text' as const } ] }, { title: 'Calidad y Costos', icon: CheckCircle, fields: [ { label: 'Puntuación de Calidad', value: batch.quality_score ? `${batch.quality_score}/10` : 'Pendiente', type: 'text' as const }, { label: 'Rendimiento', value: batch.yield_percentage ? `${batch.yield_percentage}%` : 'Calculando...', type: 'text' as const }, { label: 'Costo Estimado', value: batch.estimated_cost ? `€${batch.estimated_cost}` : '€0.00', type: 'text' as const }, { label: 'Costo Real', value: batch.actual_cost ? `€${batch.actual_cost}` : '€0.00', type: 'text' as const } ] } ]; }; return (
{/* Critical Metrics using StatsGrid */}
{isLoadingStats ? (
{[1, 2, 3, 4].map((i) => (
))}
) : statsError ? (

{t('dashboard:errors.failed_to_load_stats', 'Failed to load dashboard statistics. Please try again.')}

) : ( )}
{/* Dashboard Content - Main Sections */}
{/* 0. Configuration Progress Widget */} {/* 1. Real-time Alerts */}
{/* 1.5. Incomplete Ingredients Alert */} {/* 2. Pending PO Approvals - What purchase orders need approval? */}
{/* 3. Today's Production - What needs to be produced today? */}
{/* Purchase Order Details Modal */} {showPOModal && poDetails && ( { setShowPOModal(false); setSelectedPOId(null); }} title={`Orden de Compra: ${poDetails.po_number}`} subtitle={`Proveedor: ${poDetails.supplier?.name || poDetails.supplier_name || 'N/A'}`} mode="view" sections={buildPODetailsSections(poDetails)} loading={isLoadingPO} statusIndicator={{ color: poDetails.status === 'PENDING_APPROVAL' ? 'var(--color-warning)' : poDetails.status === 'APPROVED' ? 'var(--color-success)' : 'var(--color-info)', text: poDetails.status === 'PENDING_APPROVAL' ? 'Pendiente de Aprobación' : poDetails.status === 'APPROVED' ? 'Aprobado' : poDetails.status || 'N/A', icon: ShoppingCart }} actions={ poDetails.status === 'PENDING_APPROVAL' ? [ { label: 'Aprobar', onClick: async () => { try { await approvePOMutation.mutateAsync({ tenantId: currentTenant?.id || '', poId: poDetails.id, notes: 'Aprobado desde el dashboard' }); showToast.success('Orden aprobada'); setShowPOModal(false); setSelectedPOId(null); } catch (error) { console.error('Error approving PO:', error); showToast.error('Error al aprobar orden'); } }, variant: 'primary' as const, icon: CheckCircle }, { label: 'Rechazar', onClick: async () => { try { await rejectPOMutation.mutateAsync({ tenantId: currentTenant?.id || '', poId: poDetails.id, reason: 'Rechazado desde el dashboard' }); showToast.success('Orden rechazada'); setShowPOModal(false); setSelectedPOId(null); } catch (error) { console.error('Error rejecting PO:', error); showToast.error('Error al rechazar orden'); } }, variant: 'outline' as const, icon: X } ] : undefined } /> )} {/* Production Batch Details Modal */} {showBatchModal && batchDetails && ( { setShowBatchModal(false); setSelectedBatchId(null); }} title={batchDetails.product_name} subtitle={`Lote #${batchDetails.batch_number}`} mode="view" sections={buildBatchDetailsSections(batchDetails)} loading={isLoadingBatch} statusIndicator={{ color: batchDetails.status === 'PENDING' ? 'var(--color-warning)' : batchDetails.status === 'IN_PROGRESS' ? 'var(--color-info)' : batchDetails.status === 'COMPLETED' ? 'var(--color-success)' : batchDetails.status === 'FAILED' ? 'var(--color-error)' : 'var(--color-info)', text: batchDetails.status === 'PENDING' ? 'Pendiente' : batchDetails.status === 'IN_PROGRESS' ? 'En Progreso' : batchDetails.status === 'COMPLETED' ? 'Completado' : batchDetails.status === 'FAILED' ? 'Fallido' : batchDetails.status === 'ON_HOLD' ? 'Pausado' : batchDetails.status || 'N/A', icon: Factory }} actions={ batchDetails.status === 'PENDING' ? [ { label: 'Iniciar Lote', onClick: async () => { try { await updateBatchStatusMutation.mutateAsync({ tenantId: currentTenant?.id || '', batchId: batchDetails.id, statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS } }); showToast.success('Lote iniciado'); setShowBatchModal(false); setSelectedBatchId(null); } catch (error) { console.error('Error starting batch:', error); showToast.error('Error al iniciar lote'); } }, variant: 'primary' as const, icon: CheckCircle } ] : batchDetails.status === 'IN_PROGRESS' ? [ { label: 'Pausar Lote', onClick: async () => { try { await updateBatchStatusMutation.mutateAsync({ tenantId: currentTenant?.id || '', batchId: batchDetails.id, statusUpdate: { status: ProductionStatusEnum.ON_HOLD } }); showToast.success('Lote pausado'); setShowBatchModal(false); setSelectedBatchId(null); } catch (error) { console.error('Error pausing batch:', error); showToast.error('Error al pausar lote'); } }, variant: 'outline' as const, icon: X } ] : undefined } /> )}
); }; export default DashboardPage;