Restore frontend

This commit is contained in:
Urtzi Alfaro
2025-08-23 16:39:33 +02:00
parent b7eb4888a6
commit 62ff755f25
5 changed files with 1 additions and 1146 deletions

View File

@@ -1,342 +0,0 @@
import React from 'react';
import { useTenantId } from '../../hooks/useTenantId';
import { TodayProductionBlock } from './TodayProductionBlock';
import { TomorrowOrdersBlock } from './TomorrowOrdersBlock';
// Import existing simplified components for comparison
import TodayRevenue from '../simple/TodayRevenue';
import QuickActions from '../simple/QuickActions';
import WeatherContext from '../simple/WeatherContext';
interface EnhancedDashboardProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
onNavigateToProduction?: () => void;
onNavigateToInventory?: () => void;
onNavigateToRecipes?: () => void;
onNavigateToSales?: () => void;
}
export const EnhancedDashboard: React.FC<EnhancedDashboardProps> = ({
onNavigateToOrders,
onNavigateToReports,
onNavigateToProduction,
onNavigateToInventory,
onNavigateToRecipes,
onNavigateToSales
}) => {
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [metrics, setMetrics] = React.useState<any>(null);
const [weather, setWeather] = React.useState<any>(null);
const { tenantId } = useTenantId();
// Simplified dashboard data loading without complex forecasting
React.useEffect(() => {
const loadBasicData = async () => {
if (!tenantId) {
setIsLoading(false);
return;
}
try {
setIsLoading(true);
setError(null);
// Load basic metrics without complex operations
const metricsData = {
totalSales: 287.50 // This would come from a simple sales API call
};
setMetrics(metricsData);
// Skip weather for now to avoid blocking
setWeather(null);
} catch (err) {
console.error('Error loading dashboard data:', err);
setError('Error al cargar datos básicos');
} finally {
setIsLoading(false);
}
};
loadBasicData();
}, [tenantId]);
const reload = () => {
window.location.reload();
};
// Helper function for greeting
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return 'Buenos días';
if (hour < 18) return 'Buenas tardes';
return 'Buenas noches';
};
const getWelcomeMessage = () => {
const hour = new Date().getHours();
if (hour < 6) return 'Trabajo nocturno en la panadería';
if (hour < 12) return 'Comenzando el día en la panadería';
if (hour < 18) return 'Continuando con las operaciones';
return 'Finalizando las operaciones del día';
};
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Cargando panel de control...</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Reiniciar
</button>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md mx-auto mt-20">
<h3 className="text-red-800 font-medium">Error al cargar el panel</h3>
<p className="text-red-700 mt-1">{error}</p>
<button
onClick={() => reload()}
className="mt-4 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg transition-colors"
>
Reintentar
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div className="p-4 md:p-6 space-y-6">
{/* Enhanced Welcome Header */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3">
<div className="text-3xl">🥖</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">
{getGreeting()}! 👋
</h1>
<p className="text-gray-600 mt-1">
{getWelcomeMessage()} {new Date().toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</p>
</div>
</div>
</div>
<div className="mt-4 lg:mt-0 flex items-center space-x-4">
{/* Weather Widget */}
{weather && (
<div className="flex items-center text-sm text-gray-600 bg-gray-50 rounded-lg px-4 py-3">
<span className="text-lg mr-2">
{weather.precipitation && weather.precipitation > 0 ? '🌧️' : weather.temperature && weather.temperature > 20 ? '☀️' : '⛅'}
</span>
<div className="flex flex-col">
<span className="font-medium">{weather.temperature?.toFixed(1) || '--'}°C</span>
<span className="text-xs text-gray-500">AEMET Madrid</span>
</div>
</div>
)}
{/* System Status */}
<div className="text-right">
<div className="text-sm font-medium text-gray-900">Sistema</div>
<div className="text-xs text-green-600 flex items-center justify-end">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></div>
Operativo
</div>
</div>
</div>
</div>
</div>
{/* Primary KPIs Row - Most Critical Information */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Today's Revenue - Financial KPI */}
<TodayRevenue
currentRevenue={metrics?.totalSales || 287.50}
previousRevenue={256.25}
dailyTarget={350}
className="lg:col-span-1"
/>
{/* Quick Actions - Operational Access */}
<QuickActions
onActionClick={(actionId) => {
console.log('Action clicked:', actionId);
switch (actionId) {
case 'view_orders':
onNavigateToOrders?.();
break;
case 'view_sales':
onNavigateToReports?.();
break;
case 'view_production':
onNavigateToProduction?.();
break;
case 'view_inventory':
onNavigateToInventory?.();
break;
default:
break;
}
}}
className="lg:col-span-1"
/>
</div>
{/* Core Operations Row - Production & Orders */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Today's Production Requirements */}
<TodayProductionBlock className="xl:col-span-1" />
{/* Tomorrow's Order Requirements */}
<TomorrowOrdersBlock className="xl:col-span-1" />
</div>
{/* Secondary Information Row */}
<div className="grid grid-cols-1 gap-6">
{/* Weather Context & Recommendations */}
<WeatherContext />
{/* Real-time Alerts - Using existing AlertDashboard component */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">Sistema de Alertas</h3>
<p className="text-sm text-gray-600 mt-1">
Monitoreo en tiempo real de operaciones críticas
</p>
</div>
<div className="text-center py-8">
<div className="text-4xl mb-2"></div>
<h4 className="font-medium text-gray-900">Sistema Operativo</h4>
<p className="text-sm text-gray-600 mt-1">
No hay alertas críticas en este momento
</p>
<div className="mt-4 grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-sm font-medium text-gray-900">Producción</div>
<div className="text-xs text-green-600 mt-1">
🟢 Operativo
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-900">Inventario</div>
<div className="text-xs text-green-600 mt-1">
🟢 Normal
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-900">Equipos</div>
<div className="text-xs text-green-600 mt-1">
🟢 Funcionando
</div>
</div>
</div>
</div>
</div>
</div>
{/* Success Status Footer */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="text-4xl">🎯</div>
<div>
<h4 className="font-medium text-gray-900">Panel de Control Operativo</h4>
<p className="text-sm text-gray-600 mt-1">
Todos los sistemas funcionando correctamente Última actualización: {new Date().toLocaleTimeString('es-ES')}
</p>
</div>
</div>
{/* Quick Performance Indicators */}
<div className="flex items-center space-x-6">
<div className="text-center">
<div className="text-sm font-medium text-gray-900">Eficiencia</div>
<div className="text-lg font-bold text-green-600">94%</div>
</div>
<div className="text-center">
<div className="text-sm font-medium text-gray-900">Calidad</div>
<div className="text-lg font-bold text-blue-600">98%</div>
</div>
<div className="text-center">
<div className="text-sm font-medium text-gray-900">Satisfacción</div>
<div className="text-lg font-bold text-purple-600">4.8</div>
</div>
</div>
</div>
</div>
{/* Navigation Quick Links */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100 p-6">
<h4 className="font-medium text-gray-900 mb-4">Acceso Rápido a Secciones</h4>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<button
onClick={onNavigateToProduction}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">🏭</span>
<span className="text-xs font-medium text-gray-700">Producción</span>
</button>
<button
onClick={onNavigateToOrders}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">📋</span>
<span className="text-xs font-medium text-gray-700">Pedidos</span>
</button>
<button
onClick={onNavigateToInventory}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">📦</span>
<span className="text-xs font-medium text-gray-700">Inventario</span>
</button>
<button
onClick={onNavigateToSales}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">💰</span>
<span className="text-xs font-medium text-gray-700">Ventas</span>
</button>
<button
onClick={onNavigateToRecipes}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">📖</span>
<span className="text-xs font-medium text-gray-700">Recetas</span>
</button>
<button
onClick={onNavigateToReports}
className="flex flex-col items-center p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors"
>
<span className="text-2xl mb-1">📊</span>
<span className="text-xs font-medium text-gray-700">Reportes</span>
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,344 +0,0 @@
import React, { useState, useEffect } from 'react';
import { apiClient } from '../../api/client';
import { useTenantId } from '../../hooks/useTenantId';
interface ProductionRequirement {
id: string;
recipe_name: string;
quantity: number;
unit: string;
priority: 'high' | 'medium' | 'low';
scheduled_time: string;
status: 'pending' | 'in_progress' | 'completed' | 'delayed';
progress: number;
equipment_needed?: string;
estimated_completion?: string;
}
interface TodayProductionBlockProps {
className?: string;
}
export const TodayProductionBlock: React.FC<TodayProductionBlockProps> = ({
className = ''
}) => {
const { tenantId } = useTenantId();
const [requirements, setRequirements] = useState<ProductionRequirement[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const fetchTodayProduction = async () => {
if (!tenantId) {
setIsLoading(false);
return;
}
try {
setIsLoading(true);
setError(null);
// Use the correct API endpoint for daily production requirements
const today = new Date().toISOString().split('T')[0];
// Use the API client for proper authentication and error handling
const data = await apiClient.get(`/tenants/${tenantId}/production/daily-requirements`, {
params: { date: today }
});
// Transform API data to component format based on the actual API response structure
const transformedRequirements: ProductionRequirement[] = data.production_items?.map((item: any) => ({
id: item.id || item.batch_id || `req-${Math.random()}`,
recipe_name: item.recipe_name || item.product_name || 'Producto sin nombre',
quantity: item.quantity || item.required_quantity || 0,
unit: item.unit || 'unidades',
priority: item.priority || 'medium',
scheduled_time: item.scheduled_time || item.start_time || '08:00',
status: item.status || 'pending',
progress: item.progress || 0,
equipment_needed: item.equipment_name || item.equipment,
estimated_completion: item.estimated_completion || item.end_time
})) || [];
setRequirements(transformedRequirements);
} catch (err) {
console.error('Error fetching production data:', err);
setError('No hay datos de producción disponibles para hoy');
setRequirements([]);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTodayProduction();
}, [tenantId, refreshKey]);
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
};
const handleUpdateStatus = async (itemId: string, newStatus: 'pending' | 'in_progress' | 'completed' | 'delayed') => {
try {
// Update locally for immediate feedback
setRequirements(prev => prev.map(req =>
req.id === itemId ? { ...req, status: newStatus } : req
));
// API call would go here
// await productionService.updateBatchStatus(itemId, { status: newStatus });
} catch (err) {
console.error('Error updating status:', err);
// Revert on error
fetchTodayProduction();
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'text-green-700 bg-green-100';
case 'in_progress': return 'text-blue-700 bg-blue-100';
case 'delayed': return 'text-red-700 bg-red-100';
default: return 'text-gray-700 bg-gray-100';
}
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'high': return '🔥';
case 'medium': return '⚡';
case 'low': return '📋';
default: return '📋';
}
};
const getProductEmoji = (productName: string) => {
const name = productName.toLowerCase();
if (name.includes('croissant')) return '🥐';
if (name.includes('pan') || name.includes('bread')) return '🍞';
if (name.includes('magdalena') || name.includes('muffin')) return '🧁';
if (name.includes('tarta') || name.includes('cake')) return '🎂';
if (name.includes('cookie') || name.includes('galleta')) return '🍪';
return '🥖';
};
const completedCount = requirements.filter(r => r.status === 'completed').length;
const inProgressCount = requirements.filter(r => r.status === 'in_progress').length;
const pendingCount = requirements.filter(r => r.status === 'pending').length;
const delayedCount = requirements.filter(r => r.status === 'delayed').length;
if (isLoading) {
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">¿Qué debo producir hoy?</h3>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
</div>
<div className="space-y-3">
{[1, 2, 3].map(i => (
<div key={i} className="animate-pulse flex space-x-4">
<div className="rounded-full bg-gray-200 h-10 w-10"></div>
<div className="flex-1 space-y-2 py-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
</div>
</div>
);
}
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 ${className}`}>
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900">¿Qué debo producir hoy?</h3>
<p className="text-sm text-gray-600 mt-1">
{new Date().toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long'
})}
</p>
</div>
<button
onClick={handleRefresh}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
title="Actualizar datos"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{completedCount}</div>
<div className="text-xs text-gray-600">Completado</div>
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
<div
className="bg-green-500 h-1 rounded-full"
style={{ width: `${requirements.length ? (completedCount / requirements.length) * 100 : 0}%` }}
></div>
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{inProgressCount}</div>
<div className="text-xs text-gray-600">En Proceso</div>
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
<div
className="bg-blue-500 h-1 rounded-full"
style={{ width: `${requirements.length ? (inProgressCount / requirements.length) * 100 : 0}%` }}
></div>
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-700">{pendingCount}</div>
<div className="text-xs text-gray-600">Pendiente</div>
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
<div
className="bg-gray-500 h-1 rounded-full"
style={{ width: `${requirements.length ? (pendingCount / requirements.length) * 100 : 0}%` }}
></div>
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600">{delayedCount}</div>
<div className="text-xs text-gray-600">Retrasado</div>
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
<div
className="bg-red-500 h-1 rounded-full"
style={{ width: `${requirements.length ? (delayedCount / requirements.length) * 100 : 0}%` }}
></div>
</div>
</div>
</div>
{/* Error State */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
{/* Production Requirements List */}
{requirements.length === 0 ? (
<div className="text-center py-8">
<div className="text-4xl mb-2">🎉</div>
<h4 className="font-medium text-gray-900">¡Sin producción programada!</h4>
<p className="text-sm text-gray-600 mt-1">
No hay elementos de producción para hoy.
</p>
</div>
) : (
<div className="space-y-3">
{requirements.map((requirement) => (
<div
key={requirement.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<span className="text-2xl">{getProductEmoji(requirement.recipe_name)}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium text-gray-900 truncate">
{requirement.recipe_name}
</p>
<span className="text-lg">{getPriorityIcon(requirement.priority)}</span>
</div>
<div className="flex items-center space-x-4 mt-1">
<p className="text-sm text-gray-600">
{requirement.quantity} {requirement.unit}
</p>
<p className="text-sm text-gray-600">
📅 {requirement.scheduled_time}
</p>
{requirement.equipment_needed && (
<p className="text-sm text-gray-600">
🔧 {requirement.equipment_needed}
</p>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(requirement.status)}`}
>
{requirement.status === 'pending' && 'Pendiente'}
{requirement.status === 'in_progress' && 'En Proceso'}
{requirement.status === 'completed' && 'Completado'}
{requirement.status === 'delayed' && 'Retrasado'}
</span>
{requirement.status !== 'completed' && (
<div className="flex space-x-1">
{requirement.status === 'pending' && (
<button
onClick={() => handleUpdateStatus(requirement.id, 'in_progress')}
className="p-1 text-blue-600 hover:bg-blue-100 rounded transition-colors"
title="Iniciar producción"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
)}
{requirement.status === 'in_progress' && (
<button
onClick={() => handleUpdateStatus(requirement.id, 'completed')}
className="p-1 text-green-600 hover:bg-green-100 rounded transition-colors"
title="Marcar como completado"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
)}
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Quick Actions */}
{requirements.length > 0 && (
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Total: {requirements.length} elementos de producción
</div>
<div className="flex space-x-2">
<button className="px-3 py-1 text-xs font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-full transition-colors">
Ver Cronograma Completo
</button>
<button className="px-3 py-1 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors">
Optimizar Orden
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,409 +0,0 @@
import React, { useState, useEffect } from 'react';
import { apiClient } from '../../api/client';
import { useTenantId } from '../../hooks/useTenantId';
interface OrderRequirement {
id: string;
ingredient_name: string;
required_quantity: number;
current_stock: number;
net_requirement: number;
unit: string;
priority: 'critical' | 'high' | 'medium' | 'low';
supplier_name?: string;
estimated_cost?: number;
required_by_date: string;
category: string;
status: 'needed' | 'ordered' | 'confirmed' | 'delivered';
}
interface TomorrowOrdersBlockProps {
className?: string;
}
export const TomorrowOrdersBlock: React.FC<TomorrowOrdersBlockProps> = ({
className = ''
}) => {
const { tenantId } = useTenantId();
const [requirements, setRequirements] = useState<OrderRequirement[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const fetchTomorrowOrders = async () => {
if (!tenantId) {
setIsLoading(false);
return;
}
try {
setIsLoading(true);
setError(null);
// Get tomorrow's date
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowDateString = tomorrow.toISOString().split('T')[0];
// Use the API client for proper authentication and error handling
const data = await apiClient.get(`/tenants/${tenantId}/orders/demand-requirements`, {
params: { target_date: tomorrowDateString }
});
// Transform API data to component format based on the actual API response structure
const transformedRequirements: OrderRequirement[] = data.procurement_requirements?.map((item: any) => ({
id: item.id || `ord-${Math.random()}`,
ingredient_name: item.ingredient_name || item.product_name || 'Ingrediente sin nombre',
required_quantity: item.required_quantity || 0,
current_stock: item.current_stock_level || item.current_stock || 0,
net_requirement: item.net_requirement || Math.max(0, (item.required_quantity || 0) - (item.current_stock_level || 0)),
unit: item.unit || 'kg',
priority: item.priority || 'medium',
supplier_name: item.supplier_name,
estimated_cost: item.estimated_cost,
required_by_date: item.required_by_date || tomorrowDateString,
category: item.category || 'ingredientes',
status: item.status || 'needed'
})) || [];
setRequirements(transformedRequirements);
} catch (err) {
console.error('Error fetching orders data:', err);
setError('No hay datos de pedidos disponibles para mañana');
setRequirements([]);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTomorrowOrders();
}, [tenantId, refreshKey]);
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
};
const handleUpdateStatus = async (itemId: string, newStatus: 'needed' | 'ordered' | 'confirmed' | 'delivered') => {
try {
// Update locally for immediate feedback
setRequirements(prev => prev.map(req =>
req.id === itemId ? { ...req, status: newStatus } : req
));
// API call would go here
// await ordersService.updateProcurementStatus(itemId, { status: newStatus });
} catch (err) {
console.error('Error updating status:', err);
// Revert on error
fetchTomorrowOrders();
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'delivered': return 'text-green-700 bg-green-100';
case 'confirmed': return 'text-blue-700 bg-blue-100';
case 'ordered': return 'text-yellow-700 bg-yellow-100';
default: return 'text-red-700 bg-red-100';
}
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'critical': return '🚨';
case 'high': return '🔴';
case 'medium': return '🟡';
case 'low': return '🟢';
default: return '📦';
}
};
const getCategoryEmoji = (category: string) => {
switch (category.toLowerCase()) {
case 'harinas': return '🌾';
case 'lácteos': return '🥛';
case 'fermentos': return '🧪';
case 'azúcares': return '🍯';
case 'grasas': return '🧈';
case 'especias': return '🌿';
case 'frutas': return '🍓';
case 'chocolate': return '🍫';
case 'empaques': return '📦';
default: return '🥄';
}
};
const getStockStatus = (current: number, required: number, netRequirement: number) => {
if (netRequirement <= 0) return { status: 'sufficient', color: 'text-green-600', text: 'Stock suficiente' };
if (current === 0) return { status: 'critical', color: 'text-red-600', text: 'Sin stock' };
if (current < required * 0.5) return { status: 'low', color: 'text-orange-600', text: 'Stock bajo' };
return { status: 'moderate', color: 'text-yellow-600', text: 'Stock moderado' };
};
const neededCount = requirements.filter(r => r.status === 'needed').length;
const orderedCount = requirements.filter(r => r.status === 'ordered').length;
const confirmedCount = requirements.filter(r => r.status === 'confirmed').length;
const deliveredCount = requirements.filter(r => r.status === 'delivered').length;
const totalCost = requirements.reduce((sum, req) => sum + (req.estimated_cost || 0), 0);
const criticalItems = requirements.filter(r => r.priority === 'critical' && r.status === 'needed').length;
if (isLoading) {
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">¿Qué debo pedir para mañana?</h3>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
</div>
<div className="space-y-3">
{[1, 2, 3].map(i => (
<div key={i} className="animate-pulse flex space-x-4">
<div className="rounded-full bg-gray-200 h-10 w-10"></div>
<div className="flex-1 space-y-2 py-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
</div>
</div>
);
}
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 ${className}`}>
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900">¿Qué debo pedir para mañana?</h3>
<p className="text-sm text-gray-600 mt-1">
Planificación para {new Date(Date.now() + 24*60*60*1000).toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long'
})}
</p>
</div>
<button
onClick={handleRefresh}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
title="Actualizar datos"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-red-50 p-3 rounded-lg">
<div className="text-xl font-bold text-red-600">{neededCount}</div>
<div className="text-xs text-red-600">Por Pedir</div>
</div>
<div className="bg-yellow-50 p-3 rounded-lg">
<div className="text-xl font-bold text-yellow-600">{orderedCount}</div>
<div className="text-xs text-yellow-600">Pedido</div>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<div className="text-xl font-bold text-blue-600">{confirmedCount}</div>
<div className="text-xs text-blue-600">Confirmado</div>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<div className="text-xl font-bold text-green-600">{deliveredCount}</div>
<div className="text-xs text-green-600">Entregado</div>
</div>
</div>
{/* Critical Alerts */}
{criticalItems > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{criticalItems} ingrediente{criticalItems !== 1 ? 's' : ''} crítico{criticalItems !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-red-700 mt-1">
Requieren pedido urgente para la producción de mañana
</p>
</div>
</div>
</div>
)}
{/* Cost Summary */}
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900">Costo Estimado Total</h4>
<p className="text-2xl font-bold text-gray-900">{totalCost.toFixed(2)}</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-600">{requirements.length} ingredientes</p>
<p className="text-xs text-gray-500">Estimación basada en precios anteriores</p>
</div>
</div>
</div>
{/* Error State */}
{error && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
{error} - Mostrando datos de ejemplo
</p>
</div>
</div>
</div>
)}
{/* Orders Requirements List */}
{requirements.length === 0 ? (
<div className="text-center py-8">
<div className="text-4xl mb-2"></div>
<h4 className="font-medium text-gray-900">¡Todo listo para mañana!</h4>
<p className="text-sm text-gray-600 mt-1">
No necesitas hacer pedidos adicionales.
</p>
</div>
) : (
<div className="space-y-3">
{requirements
.sort((a, b) => {
// Sort by priority first (critical -> high -> medium -> low)
const priorityOrder = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0) return priorityDiff;
// Then by status (needed -> ordered -> confirmed -> delivered)
const statusOrder = { 'needed': 0, 'ordered': 1, 'confirmed': 2, 'delivered': 3 };
return statusOrder[a.status] - statusOrder[b.status];
})
.map((requirement) => {
const stockStatus = getStockStatus(requirement.current_stock, requirement.required_quantity, requirement.net_requirement);
return (
<div
key={requirement.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="flex items-center space-x-4 flex-1">
<div className="flex-shrink-0">
<span className="text-2xl">{getCategoryEmoji(requirement.category)}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium text-gray-900 truncate">
{requirement.ingredient_name}
</p>
<span className="text-lg">{getPriorityIcon(requirement.priority)}</span>
</div>
<div className="flex items-center space-x-4 mt-1">
<div className="text-xs text-gray-600">
<span className="font-medium">Necesario:</span> {requirement.required_quantity} {requirement.unit}
</div>
<div className="text-xs text-gray-600">
<span className="font-medium">Stock:</span> {requirement.current_stock} {requirement.unit}
</div>
{requirement.net_requirement > 0 && (
<div className="text-xs text-red-600 font-medium">
Falta: {requirement.net_requirement} {requirement.unit}
</div>
)}
</div>
<div className="flex items-center space-x-4 mt-1">
{requirement.supplier_name && (
<p className="text-xs text-gray-500">
🏪 {requirement.supplier_name}
</p>
)}
{requirement.estimated_cost && (
<p className="text-xs text-gray-500">
💰 {requirement.estimated_cost.toFixed(2)}
</p>
)}
<span className={`text-xs ${stockStatus.color}`}>
{stockStatus.text}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(requirement.status)}`}
>
{requirement.status === 'needed' && 'Por Pedir'}
{requirement.status === 'ordered' && 'Pedido'}
{requirement.status === 'confirmed' && 'Confirmado'}
{requirement.status === 'delivered' && 'Entregado'}
</span>
{requirement.status !== 'delivered' && requirement.net_requirement > 0 && (
<div className="flex space-x-1">
{requirement.status === 'needed' && (
<button
onClick={() => handleUpdateStatus(requirement.id, 'ordered')}
className="p-1 text-blue-600 hover:bg-blue-100 rounded transition-colors"
title="Marcar como pedido"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4m0 0L7 13m0 0l-2.5 5M12 21a9 9 0 100-18 9 9 0 000 18z" />
</svg>
</button>
)}
{requirement.status === 'ordered' && (
<button
onClick={() => handleUpdateStatus(requirement.id, 'confirmed')}
className="p-1 text-green-600 hover:bg-green-100 rounded transition-colors"
title="Confirmar pedido"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
)}
</div>
)}
</div>
</div>
);
})}
</div>
)}
{/* Quick Actions */}
{requirements.length > 0 && (
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Total: {requirements.length} ingredientes {neededCount} por pedir
</div>
<div className="flex space-x-2">
<button className="px-3 py-1 text-xs font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-full transition-colors">
Generar Pedidos Automáticos
</button>
<button className="px-3 py-1 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors">
Ver Proveedores
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,4 +0,0 @@
export { TodayProductionBlock } from './TodayProductionBlock';
export { TomorrowOrdersBlock } from './TomorrowOrdersBlock';
export { EnhancedDashboard } from './EnhancedDashboard';
export { DebugDashboard } from './DebugDashboard';