Restore frontend
This commit is contained in:
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { TodayProductionBlock } from './TodayProductionBlock';
|
|
||||||
export { TomorrowOrdersBlock } from './TomorrowOrdersBlock';
|
|
||||||
export { EnhancedDashboard } from './EnhancedDashboard';
|
|
||||||
export { DebugDashboard } from './DebugDashboard';
|
|
||||||
@@ -2,9 +2,6 @@ import React, { useState } from 'react';
|
|||||||
import { useDashboard } from '../../hooks/useDashboard';
|
import { useDashboard } from '../../hooks/useDashboard';
|
||||||
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
|
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
|
||||||
|
|
||||||
// Import enhanced dashboard components
|
|
||||||
import { EnhancedDashboard } from '../../components/dashboard/EnhancedDashboard';
|
|
||||||
|
|
||||||
// Import simplified components for fallback
|
// Import simplified components for fallback
|
||||||
import TodayRevenue from '../../components/simple/TodayRevenue';
|
import TodayRevenue from '../../components/simple/TodayRevenue';
|
||||||
import TodayProduction from '../../components/simple/TodayProduction';
|
import TodayProduction from '../../components/simple/TodayProduction';
|
||||||
@@ -30,7 +27,6 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
|||||||
onNavigateToRecipes,
|
onNavigateToRecipes,
|
||||||
onNavigateToSales
|
onNavigateToSales
|
||||||
}) => {
|
}) => {
|
||||||
const [useEnhancedView, setUseEnhancedView] = useState(true);
|
|
||||||
const {
|
const {
|
||||||
weather,
|
weather,
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -82,38 +78,6 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
|||||||
return 'Buenas noches';
|
return 'Buenas noches';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle between enhanced and classic view
|
|
||||||
const toggleDashboardView = () => {
|
|
||||||
setUseEnhancedView(!useEnhancedView);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show enhanced dashboard by default - use debug version for stability
|
|
||||||
if (useEnhancedView) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Dashboard View Toggle */}
|
|
||||||
<div className="fixed top-4 right-4 z-50 flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={toggleDashboardView}
|
|
||||||
className="px-3 py-1 text-xs bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-colors shadow-lg"
|
|
||||||
title="Cambiar a vista clásica"
|
|
||||||
>
|
|
||||||
Vista Clásica
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EnhancedDashboard
|
|
||||||
onNavigateToOrders={onNavigateToOrders}
|
|
||||||
onNavigateToReports={onNavigateToReports}
|
|
||||||
onNavigateToProduction={onNavigateToProduction}
|
|
||||||
onNavigateToInventory={onNavigateToInventory}
|
|
||||||
onNavigateToRecipes={onNavigateToRecipes}
|
|
||||||
onNavigateToSales={onNavigateToSales}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classic dashboard view
|
// Classic dashboard view
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -143,17 +107,7 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-6 space-y-6 bg-gray-50 min-h-screen">
|
<div className="p-4 md:p-6 space-y-6 bg-gray-50 min-h-screen">
|
||||||
{/* Dashboard View Toggle */}
|
|
||||||
<div className="fixed top-4 right-4 z-50">
|
|
||||||
<button
|
|
||||||
onClick={toggleDashboardView}
|
|
||||||
className="px-3 py-1 text-xs bg-gray-600 text-white rounded-full hover:bg-gray-700 transition-colors shadow-lg"
|
|
||||||
title="Cambiar a vista mejorada"
|
|
||||||
>
|
|
||||||
Vista Mejorada
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Welcome Header */}
|
{/* Welcome Header */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user