Improve the design of the frontend

This commit is contained in:
Urtzi Alfaro
2025-08-08 19:21:23 +02:00
parent 488bb3ef93
commit 62ca49d4b8
53 changed files with 5395 additions and 5387 deletions

View File

@@ -9,6 +9,7 @@ import LoginPage from './pages/auth/LoginPage';
import RegisterPage from './pages/auth/RegisterPage';
import OnboardingPage from './pages/onboarding/OnboardingPage';
import DashboardPage from './pages/dashboard/DashboardPage';
import ProductionPage from './pages/production/ProductionPage';
import ForecastPage from './pages/forecast/ForecastPage';
import OrdersPage from './pages/orders/OrdersPage';
import SettingsPage from './pages/settings/SettingsPage';
@@ -24,7 +25,7 @@ import './i18n';
// Global styles
import './styles/globals.css';
type CurrentPage = 'landing' | 'login' | 'register' | 'onboarding' | 'dashboard' | 'forecast' | 'orders' | 'settings';
type CurrentPage = 'landing' | 'login' | 'register' | 'onboarding' | 'dashboard' | 'reports' | 'orders' | 'production' | 'settings';
interface User {
id: string;
@@ -178,14 +179,20 @@ const App: React.FC = () => {
// Main app pages with layout
const pageComponent = () => {
switch (appState.currentPage) {
case 'forecast':
case 'reports':
return <ForecastPage />;
case 'orders':
return <OrdersPage />;
case 'production':
return <ProductionPage />;
case 'settings':
return <SettingsPage user={appState.user!} onLogout={handleLogout} />;
default:
return <DashboardPage user={appState.user!} />;
return <DashboardPage
onNavigateToOrders={() => navigateTo('orders')}
onNavigateToReports={() => navigateTo('reports')}
onNavigateToProduction={() => navigateTo('production')}
/>;
}
};

View File

@@ -9,7 +9,8 @@ import {
LogOut,
User,
Bell,
ChevronDown
ChevronDown,
ChefHat
} from 'lucide-react';
interface LayoutProps {
@@ -38,9 +39,10 @@ const Layout: React.FC<LayoutProps> = ({
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const navigation: NavigationItem[] = [
{ id: 'dashboard', label: 'Panel Principal', icon: Home, href: '/dashboard' },
{ id: 'forecast', label: 'Predicciones', icon: TrendingUp, href: '/forecast' },
{ id: 'dashboard', label: 'Inicio', icon: Home, href: '/dashboard' },
{ id: 'orders', label: 'Pedidos', icon: Package, href: '/orders' },
{ id: 'production', label: 'Producción', icon: ChefHat, href: '/production' },
{ id: 'reports', label: 'Informes', icon: TrendingUp, href: '/reports' },
{ id: 'settings', label: 'Configuración', icon: Settings, href: '/settings' },
];

View File

@@ -0,0 +1,180 @@
import React from 'react';
import { AlertTriangle, Cloud, Package, Clock, ChevronRight } from 'lucide-react';
export interface Alert {
id: string;
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
action?: string;
time?: string;
}
interface CriticalAlertsProps {
alerts?: Alert[];
onAlertClick?: (alertId: string) => void;
className?: string;
}
const CriticalAlerts: React.FC<CriticalAlertsProps> = ({
alerts = [
{
id: '1',
type: 'stock',
severity: 'high',
title: 'Stock Bajo',
description: 'Pan integral: solo 5 unidades',
action: 'Hacer más',
time: '10:30'
},
{
id: '2',
type: 'weather',
severity: 'medium',
title: 'Lluvia Esperada',
description: 'Precipitaciones 14:00 - 17:00',
action: 'Ajustar producción',
time: '14:00'
},
{
id: '3',
type: 'order',
severity: 'low',
title: 'Pedido Especial',
description: 'Tarta de cumpleaños Ana - Viernes',
action: 'Ver detalles',
time: 'Viernes'
}
],
onAlertClick,
className = ''
}) => {
const getAlertIcon = (type: Alert['type']) => {
switch (type) {
case 'stock': return Package;
case 'weather': return Cloud;
case 'order': return Clock;
case 'production': return AlertTriangle;
default: return AlertTriangle;
}
};
const getAlertColors = (severity: Alert['severity']) => {
switch (severity) {
case 'high': return {
bg: 'bg-red-50 border-red-200',
icon: 'text-red-600',
title: 'text-red-900',
description: 'text-red-700'
};
case 'medium': return {
bg: 'bg-yellow-50 border-yellow-200',
icon: 'text-yellow-600',
title: 'text-yellow-900',
description: 'text-yellow-700'
};
case 'low': return {
bg: 'bg-blue-50 border-blue-200',
icon: 'text-blue-600',
title: 'text-blue-900',
description: 'text-blue-700'
};
}
};
const visibleAlerts = alerts.slice(0, 3); // Show max 3 alerts
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<AlertTriangle className="h-5 w-5 mr-2 text-orange-600" />
Atención Requerida
</h3>
{alerts.length > 3 && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
+{alerts.length - 3} más
</span>
)}
</div>
{/* Alerts List */}
{visibleAlerts.length > 0 ? (
<div className="space-y-3">
{visibleAlerts.map((alert) => {
const IconComponent = getAlertIcon(alert.type);
const colors = getAlertColors(alert.severity);
return (
<div
key={alert.id}
onClick={() => onAlertClick?.(alert.id)}
className={`${colors.bg} border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-all duration-200`}
>
<div className="flex items-start space-x-3">
<IconComponent className={`h-4 w-4 mt-0.5 ${colors.icon} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`text-sm font-medium ${colors.title}`}>
{alert.title}
</h4>
<p className={`text-xs ${colors.description} mt-1`}>
{alert.description}
</p>
{alert.action && (
<p className={`text-xs ${colors.icon} font-medium mt-1`}>
{alert.action}
</p>
)}
</div>
<div className="flex items-center space-x-2 ml-3">
{alert.time && (
<span className={`text-xs ${colors.description}`}>
{alert.time}
</span>
)}
<ChevronRight className={`h-3 w-3 ${colors.icon}`} />
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-6">
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertTriangle className="h-6 w-6 text-green-600" />
</div>
<h4 className="text-sm font-medium text-gray-900">Todo bajo control</h4>
<p className="text-xs text-gray-500 mt-1">No hay alertas que requieran atención</p>
</div>
)}
{/* Quick Summary */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'high').length} Urgentes
</span>
<span className="flex items-center">
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'medium').length} Importantes
</span>
</div>
<button className="text-xs text-gray-600 hover:text-gray-900 font-medium">
Ver todas
</button>
</div>
</div>
);
};
export default CriticalAlerts;

View File

@@ -0,0 +1,413 @@
import React, { useState } from 'react';
import {
ShoppingCart,
Calendar,
TrendingUp,
AlertTriangle,
CheckCircle,
Clock,
Plus,
Minus,
Eye,
ArrowRight
} from 'lucide-react';
// Types for order suggestions
export interface DailyOrderItem {
id: string;
product: string;
emoji: string;
suggestedQuantity: number;
currentQuantity: number;
unit: string;
urgency: 'high' | 'medium' | 'low';
reason: string;
confidence: number;
supplier: string;
estimatedCost: number;
lastOrderDate: string;
}
export interface WeeklyOrderItem {
id: string;
product: string;
emoji: string;
suggestedQuantity: number;
currentStock: number;
unit: string;
frequency: 'weekly' | 'biweekly';
nextOrderDate: string;
supplier: string;
estimatedCost: number;
stockDays: number; // Days until stock runs out
confidence: number;
}
interface OrderSuggestionsProps {
dailyOrders: DailyOrderItem[];
weeklyOrders: WeeklyOrderItem[];
onUpdateQuantity: (orderId: string, quantity: number, type: 'daily' | 'weekly') => void;
onCreateOrder: (items: (DailyOrderItem | WeeklyOrderItem)[], type: 'daily' | 'weekly') => void;
onViewDetails: () => void;
className?: string;
}
const OrderSuggestions: React.FC<OrderSuggestionsProps> = ({
dailyOrders,
weeklyOrders,
onUpdateQuantity,
onCreateOrder,
onViewDetails,
className = ''
}) => {
const [activeTab, setActiveTab] = useState<'daily' | 'weekly'>('daily');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const getUrgencyColor = (urgency: DailyOrderItem['urgency']) => {
switch (urgency) {
case 'high':
return 'bg-red-100 text-red-800 border-red-200';
case 'medium':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low':
return 'bg-green-100 text-green-800 border-green-200';
}
};
const getUrgencyLabel = (urgency: DailyOrderItem['urgency']) => {
switch (urgency) {
case 'high':
return 'Urgente';
case 'medium':
return 'Normal';
case 'low':
return 'Bajo';
}
};
const getStockStatusColor = (stockDays: number) => {
if (stockDays <= 2) return 'bg-red-100 text-red-800';
if (stockDays <= 5) return 'bg-yellow-100 text-yellow-800';
return 'bg-green-100 text-green-800';
};
const handleQuantityChange = (itemId: string, delta: number, type: 'daily' | 'weekly') => {
const items = type === 'daily' ? dailyOrders : weeklyOrders;
const item = items.find(i => i.id === itemId);
if (item) {
const newQuantity = Math.max(0, item.suggestedQuantity + delta);
onUpdateQuantity(itemId, newQuantity, type);
}
};
const toggleItemSelection = (itemId: string) => {
const newSelected = new Set(selectedItems);
if (newSelected.has(itemId)) {
newSelected.delete(itemId);
} else {
newSelected.add(itemId);
}
setSelectedItems(newSelected);
};
const handleCreateSelectedOrders = () => {
if (activeTab === 'daily') {
const selectedDailyItems = dailyOrders.filter(item => selectedItems.has(item.id));
if (selectedDailyItems.length > 0) {
onCreateOrder(selectedDailyItems, 'daily');
}
} else {
const selectedWeeklyItems = weeklyOrders.filter(item => selectedItems.has(item.id));
if (selectedWeeklyItems.length > 0) {
onCreateOrder(selectedWeeklyItems, 'weekly');
}
}
setSelectedItems(new Set());
};
const totalDailyCost = dailyOrders
.filter(item => selectedItems.has(item.id))
.reduce((sum, item) => sum + item.estimatedCost, 0);
const totalWeeklyCost = weeklyOrders
.filter(item => selectedItems.has(item.id))
.reduce((sum, item) => sum + item.estimatedCost, 0);
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<ShoppingCart className="h-5 w-5 mr-2 text-primary-600" />
Pedidos Sugeridos por IA
</h3>
<p className="text-sm text-gray-600 mt-1">
Optimización inteligente basada en predicciones de demanda
</p>
</div>
<button
onClick={onViewDetails}
className="flex items-center px-3 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-colors"
>
<Eye className="h-4 w-4 mr-1" />
Ver Detalles
<ArrowRight className="h-3 w-3 ml-1" />
</button>
</div>
{/* Tabs */}
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg mb-6">
<button
onClick={() => setActiveTab('daily')}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-all flex items-center justify-center ${
activeTab === 'daily'
? 'bg-white text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Calendar className="h-4 w-4 mr-2" />
Diarios ({dailyOrders.length})
</button>
<button
onClick={() => setActiveTab('weekly')}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-all flex items-center justify-center ${
activeTab === 'weekly'
? 'bg-white text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Clock className="h-4 w-4 mr-2" />
Semanales ({weeklyOrders.length})
</button>
</div>
{/* Daily Orders Tab */}
{activeTab === 'daily' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Pedidos para mañana desde la panadería central
</div>
<div className="text-sm font-medium text-primary-600">
Total seleccionado: {totalDailyCost.toFixed(2)}
</div>
</div>
<div className="space-y-3">
{dailyOrders.map((item) => (
<div
key={item.id}
className={`border border-gray-200 rounded-lg p-4 transition-all ${
selectedItems.has(item.id) ? 'ring-2 ring-primary-500 bg-primary-50' : 'hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => toggleItemSelection(item.id)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="text-2xl">{item.emoji}</span>
<div>
<div className="font-medium text-gray-900">{item.product}</div>
<div className="text-sm text-gray-600">{item.supplier}</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span className={`px-2 py-1 text-xs font-medium rounded-full border ${getUrgencyColor(item.urgency)}`}>
{getUrgencyLabel(item.urgency)}
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => handleQuantityChange(item.id, -5, 'daily')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
disabled={item.suggestedQuantity <= 0}
>
<Minus className="h-3 w-3" />
</button>
<span className="w-12 text-center font-bold">{item.suggestedQuantity}</span>
<button
onClick={() => handleQuantityChange(item.id, 5, 'daily')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
>
<Plus className="h-3 w-3" />
</button>
<span className="text-sm text-gray-600 ml-2">{item.unit}</span>
</div>
<div className="text-right">
<div className="font-semibold text-gray-900">{item.estimatedCost.toFixed(2)}</div>
<div className="text-xs text-gray-500">{item.confidence}% confianza</div>
</div>
</div>
</div>
<div className="mt-3 text-sm text-gray-600 flex items-start">
<TrendingUp className="h-4 w-4 mr-2 text-blue-500 flex-shrink-0 mt-0.5" />
<span>{item.reason}</span>
</div>
{item.currentQuantity > 0 && (
<div className="mt-2 text-xs text-gray-500">
Stock actual: {item.currentQuantity} {item.unit}
</div>
)}
</div>
))}
</div>
{selectedItems.size > 0 && (
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-primary-900">
{selectedItems.size} productos seleccionados
</div>
<div className="text-sm text-primary-700">
Total estimado: {totalDailyCost.toFixed(2)}
</div>
</div>
<button
onClick={handleCreateSelectedOrders}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
>
Crear Pedido Diario
</button>
</div>
</div>
)}
</div>
)}
{/* Weekly Orders Tab */}
{activeTab === 'weekly' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Pedidos semanales para ingredientes y suministros
</div>
<div className="text-sm font-medium text-primary-600">
Total seleccionado: {totalWeeklyCost.toFixed(2)}
</div>
</div>
<div className="space-y-3">
{weeklyOrders.map((item) => (
<div
key={item.id}
className={`border border-gray-200 rounded-lg p-4 transition-all ${
selectedItems.has(item.id) ? 'ring-2 ring-primary-500 bg-primary-50' : 'hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => toggleItemSelection(item.id)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="text-2xl">{item.emoji}</span>
<div>
<div className="font-medium text-gray-900">{item.product}</div>
<div className="text-sm text-gray-600">{item.supplier}</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span className={`px-2 py-1 text-xs font-medium rounded ${getStockStatusColor(item.stockDays)}`}>
{item.stockDays} días de stock
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => handleQuantityChange(item.id, -1, 'weekly')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
disabled={item.suggestedQuantity <= 0}
>
<Minus className="h-3 w-3" />
</button>
<span className="w-12 text-center font-bold">{item.suggestedQuantity}</span>
<button
onClick={() => handleQuantityChange(item.id, 1, 'weekly')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
>
<Plus className="h-3 w-3" />
</button>
<span className="text-sm text-gray-600 ml-2">{item.unit}</span>
</div>
<div className="text-right">
<div className="font-semibold text-gray-900">{item.estimatedCost.toFixed(2)}</div>
<div className="text-xs text-gray-500">{item.confidence}% confianza</div>
</div>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<span className="font-medium">Stock actual:</span> {item.currentStock} {item.unit}
</div>
<div>
<span className="font-medium">Próximo pedido:</span> {new Date(item.nextOrderDate).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-2 flex items-center text-xs text-gray-500">
<CheckCircle className="h-3 w-3 mr-1 text-green-500" />
Frecuencia: {item.frequency === 'weekly' ? 'Semanal' : 'Quincenal'}
</div>
</div>
))}
</div>
{selectedItems.size > 0 && (
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-primary-900">
{selectedItems.size} productos seleccionados
</div>
<div className="text-sm text-primary-700">
Total estimado: {totalWeeklyCost.toFixed(2)}
</div>
</div>
<button
onClick={handleCreateSelectedOrders}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
>
Crear Pedido Semanal
</button>
</div>
</div>
)}
</div>
)}
{/* Empty States */}
{activeTab === 'daily' && dailyOrders.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Calendar className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p>No hay pedidos diarios sugeridos</p>
<p className="text-sm">El stock actual es suficiente para mañana</p>
</div>
)}
{activeTab === 'weekly' && weeklyOrders.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Clock className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p>No hay pedidos semanales pendientes</p>
<p className="text-sm">Todos los suministros están en stock</p>
</div>
)}
</div>
);
};
export default OrderSuggestions;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import {
Phone, FileText, AlertTriangle, Package,
Euro, BarChart3, Settings, Calendar,
Plus, Clock
} from 'lucide-react';
export interface QuickAction {
id: string;
label: string;
icon: any;
description?: string;
variant: 'primary' | 'secondary' | 'urgent';
badge?: string;
}
interface QuickActionsProps {
actions?: QuickAction[];
onActionClick?: (actionId: string) => void;
className?: string;
}
const QuickActions: React.FC<QuickActionsProps> = ({
actions = [
{
id: 'call_supplier',
label: 'Llamar Proveedor',
icon: Phone,
description: 'Contactar proveedor principal',
variant: 'urgent',
badge: 'Urgente'
},
{
id: 'record_sale',
label: 'Anotar Venta',
icon: FileText,
description: 'Registrar venta manual',
variant: 'primary'
},
{
id: 'stock_issue',
label: 'Problema Stock',
icon: AlertTriangle,
description: 'Reportar falta de producto',
variant: 'urgent'
},
{
id: 'view_orders',
label: 'Ver Pedidos',
icon: Package,
description: 'Revisar pedidos especiales',
variant: 'secondary',
badge: '3'
},
{
id: 'close_register',
label: 'Cerrar Caja',
icon: Euro,
description: 'Arqueo de caja diario',
variant: 'primary'
},
{
id: 'view_sales',
label: 'Ver Ventas',
icon: BarChart3,
description: 'Resumen de ventas del día',
variant: 'secondary'
}
],
onActionClick,
className = ''
}) => {
const getActionStyles = (variant: QuickAction['variant']) => {
switch (variant) {
case 'primary':
return {
button: 'bg-blue-600 hover:bg-blue-700 text-white border-blue-600',
icon: 'text-white'
};
case 'urgent':
return {
button: 'bg-red-600 hover:bg-red-700 text-white border-red-600',
icon: 'text-white'
};
case 'secondary':
return {
button: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-300',
icon: 'text-gray-600'
};
}
};
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Clock className="h-5 w-5 mr-2 text-purple-600" />
Acciones Rápidas
</h3>
<span className="text-xs text-gray-500">
Tareas comunes
</span>
</div>
{/* Actions Grid */}
<div className="grid grid-cols-2 gap-3">
{actions.map((action) => {
const IconComponent = action.icon;
const styles = getActionStyles(action.variant);
return (
<button
key={action.id}
onClick={() => onActionClick?.(action.id)}
className={`
relative p-4 border rounded-xl transition-all duration-200
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
${styles.button}
`}
>
{/* Badge */}
{action.badge && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
{action.badge}
</span>
)}
{/* Icon */}
<div className="flex items-center justify-center mb-2">
<IconComponent className={`h-6 w-6 ${styles.icon}`} />
</div>
{/* Label */}
<h4 className="text-sm font-medium text-center leading-tight">
{action.label}
</h4>
{/* Description */}
{action.description && (
<p className={`text-xs mt-1 text-center leading-tight ${
action.variant === 'secondary' ? 'text-gray-500' : 'text-white/80'
}`}>
{action.description}
</p>
)}
</button>
);
})}
</div>
{/* Quick Stats */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
{actions.filter(a => a.variant === 'urgent').length} Urgentes
</span>
<span className="flex items-center">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-1"></div>
{actions.filter(a => a.variant === 'primary').length} Principales
</span>
</div>
{/* Add Action */}
<button className="flex items-center text-xs text-gray-600 hover:text-gray-900 font-medium">
<Plus className="h-3 w-3 mr-1" />
Personalizar
</button>
</div>
{/* Keyboard Shortcuts Hint */}
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
<p className="text-xs text-gray-600 text-center">
💡 Usa Ctrl + K para acceso rápido por teclado
</p>
</div>
</div>
);
};
export default QuickActions;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { Package, BarChart3, Cloud, ChevronRight, Calendar, TrendingUp } from 'lucide-react';
interface QuickOverviewProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
className?: string;
}
const QuickOverview: React.FC<QuickOverviewProps> = ({
onNavigateToOrders,
onNavigateToReports,
className = ''
}) => {
return (
<div className={`grid grid-cols-1 md:grid-cols-3 gap-6 ${className}`}>
{/* Orders Summary */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<Package className="h-4 w-4 mr-2 text-green-600" />
Pedidos
</h4>
<button
onClick={onNavigateToOrders}
className="text-xs text-blue-600 hover:text-blue-700 font-medium flex items-center"
>
Ver todos <ChevronRight className="h-3 w-3 ml-1" />
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="flex items-center space-x-2">
<Calendar className="h-3 w-3 text-yellow-600" />
<span className="text-sm font-medium text-yellow-900">Tarta Ana</span>
</div>
<span className="text-xs text-yellow-700 font-medium">Viernes</span>
</div>
<div className="flex items-center justify-between p-2 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center space-x-2">
<Calendar className="h-3 w-3 text-blue-600" />
<span className="text-sm font-medium text-blue-900">Cumple Sara</span>
</div>
<span className="text-xs text-blue-700 font-medium">Sábado</span>
</div>
<div className="text-center py-2">
<span className="text-xs text-gray-500">2 pedidos especiales esta semana</span>
</div>
</div>
</div>
{/* Weekly Summary */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<BarChart3 className="h-4 w-4 mr-2 text-blue-600" />
Esta Semana
</h4>
<button
onClick={onNavigateToReports}
className="text-xs text-blue-600 hover:text-blue-700 font-medium flex items-center"
>
Ver informes <ChevronRight className="h-3 w-3 ml-1" />
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Ventas totales</span>
<span className="text-sm font-semibold text-gray-900">1,940</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Mejor día</span>
<span className="text-sm font-semibold text-green-600">Martes (+18%)</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Producto top</span>
<span className="text-sm font-semibold text-gray-900">🥐 Croissants</span>
</div>
<div className="pt-2 border-t border-gray-100">
<div className="flex items-center justify-center text-xs text-green-600">
<TrendingUp className="h-3 w-3 mr-1" />
+8% vs semana anterior
</div>
</div>
</div>
</div>
{/* Weather & Context */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<Cloud className="h-4 w-4 mr-2 text-gray-600" />
Clima & Contexto
</h4>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-200">
<div>
<div className="text-sm font-medium text-blue-900">Lluvia esperada</div>
<div className="text-xs text-blue-700">14:00 - 17:00</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-blue-900">🌧</div>
<div className="text-xs text-blue-700">12°C</div>
</div>
</div>
<div className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200">
<span className="text-sm font-medium text-yellow-900">Impacto estimado</span>
<span className="text-sm font-bold text-yellow-900">-15% ventas</span>
</div>
<div className="text-center py-1">
<span className="text-xs text-gray-500">Predicciones ya ajustadas</span>
</div>
</div>
</div>
</div>
);
};
export default QuickOverview;

View File

@@ -0,0 +1,231 @@
import React from 'react';
import { CheckCircle2, Clock, Loader2, Calendar, Plus, Minus } from 'lucide-react';
export interface ProductionItem {
id: string;
product: string;
emoji: string;
quantity: number;
status: 'completed' | 'in_progress' | 'pending';
scheduledTime: string;
completedTime?: string;
confidence?: number;
}
interface TodayProductionProps {
items?: ProductionItem[];
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
className?: string;
}
const TodayProduction: React.FC<TodayProductionProps> = ({
items = [
{
id: '1',
product: 'Croissants',
emoji: '🥐',
quantity: 45,
status: 'completed',
scheduledTime: '06:00',
completedTime: '06:30',
confidence: 92
},
{
id: '2',
product: 'Pan integral',
emoji: '🍞',
quantity: 30,
status: 'in_progress',
scheduledTime: '07:30',
confidence: 87
},
{
id: '3',
product: 'Magdalenas',
emoji: '🧁',
quantity: 25,
status: 'pending',
scheduledTime: '09:00',
confidence: 78
},
{
id: '4',
product: 'Empanadas',
emoji: '🥟',
quantity: 20,
status: 'pending',
scheduledTime: '10:30',
confidence: 85
}
],
onUpdateQuantity,
onUpdateStatus,
className = ''
}) => {
const getStatusIcon = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return <CheckCircle2 className="h-5 w-5 text-green-600" />;
case 'in_progress': return <Loader2 className="h-5 w-5 text-blue-600 animate-spin" />;
case 'pending': return <Clock className="h-5 w-5 text-gray-400" />;
}
};
const getStatusText = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return 'Listo';
case 'in_progress': return 'Haciendo';
case 'pending': return 'Pendiente';
}
};
const getStatusColors = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return 'bg-green-50 border-green-200';
case 'in_progress': return 'bg-blue-50 border-blue-200';
case 'pending': return 'bg-gray-50 border-gray-200';
}
};
const completedCount = items.filter(item => item.status === 'completed').length;
const inProgressCount = items.filter(item => item.status === 'in_progress').length;
const pendingCount = items.filter(item => item.status === 'pending').length;
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Calendar className="h-5 w-5 mr-2 text-blue-600" />
Producir Hoy
</h3>
<div className="text-xs text-gray-500">
{new Date().toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' })}
</div>
</div>
{/* Progress Summary */}
<div className="flex items-center space-x-4 mb-4 p-3 bg-gray-50 rounded-lg">
<div className="text-center">
<div className="text-lg font-bold text-green-600">{completedCount}</div>
<div className="text-xs text-gray-600">Listos</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-blue-600">{inProgressCount}</div>
<div className="text-xs text-gray-600">En curso</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-gray-600">{pendingCount}</div>
<div className="text-xs text-gray-600">Pendientes</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-gray-900">{items.reduce((sum, item) => sum + item.quantity, 0)}</div>
<div className="text-xs text-gray-600">Total uds</div>
</div>
</div>
{/* Production Items */}
<div className="space-y-3">
{items.map((item) => (
<div
key={item.id}
className={`border rounded-lg p-4 transition-all duration-200 ${getStatusColors(item.status)}`}
>
<div className="flex items-center space-x-3">
{/* Status Icon */}
<div className="flex-shrink-0">
{getStatusIcon(item.status)}
</div>
{/* Product Info */}
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className="text-lg">{item.emoji}</span>
<h4 className="font-medium text-gray-900">{item.product}</h4>
{item.confidence && (
<span className="text-xs text-gray-500">
({item.confidence}% confianza)
</span>
)}
</div>
<div className="flex items-center space-x-4 text-sm text-gray-600">
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
{item.status === 'completed' && item.completedTime
? `Listo a las ${item.completedTime}`
: `Programado ${item.scheduledTime}`}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${
item.status === 'completed' ? 'bg-green-100 text-green-800' :
item.status === 'in_progress' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{getStatusText(item.status)}
</span>
</div>
</div>
{/* Quantity Controls */}
<div className="flex-shrink-0">
<div className="flex items-center space-x-2">
<button
onClick={() => onUpdateQuantity?.(item.id, Math.max(1, item.quantity - 1))}
disabled={item.status === 'completed'}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Minus className="h-3 w-3" />
</button>
<span className="text-lg font-bold text-gray-900 min-w-[40px] text-center">
{item.quantity}
</span>
<button
onClick={() => onUpdateQuantity?.(item.id, item.quantity + 1)}
disabled={item.status === 'completed'}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="h-3 w-3" />
</button>
</div>
<div className="text-xs text-gray-500 text-center mt-1">uds</div>
</div>
{/* Action Button */}
{item.status !== 'completed' && (
<div className="flex-shrink-0">
<button
onClick={() => {
const nextStatus = item.status === 'pending' ? 'in_progress' : 'completed';
onUpdateStatus?.(item.id, nextStatus);
}}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
item.status === 'pending'
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
{item.status === 'pending' ? 'Iniciar' : 'Completar'}
</button>
</div>
)}
</div>
</div>
))}
</div>
{/* Quick Actions */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<button className="text-sm text-gray-600 hover:text-gray-900 font-medium">
+ Agregar producto
</button>
<button className="text-sm text-blue-600 hover:text-blue-700 font-medium">
Ver horario completo
</button>
</div>
</div>
);
};
export default TodayProduction;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { TrendingUp, TrendingDown, Euro } from 'lucide-react';
interface TodayRevenueProps {
currentRevenue: number;
previousRevenue: number;
dailyTarget: number;
className?: string;
}
const TodayRevenue: React.FC<TodayRevenueProps> = ({
currentRevenue = 287.50,
previousRevenue = 256.25,
dailyTarget = 350,
className = ''
}) => {
const changeAmount = currentRevenue - previousRevenue;
const changePercentage = ((changeAmount / previousRevenue) * 100);
const remainingToTarget = Math.max(0, dailyTarget - currentRevenue);
const targetProgress = (currentRevenue / dailyTarget) * 100;
const isPositive = changeAmount >= 0;
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Euro className="h-5 w-5 mr-2 text-green-600" />
Ingresos de Hoy
</h3>
<span className="text-xs text-gray-500">
{new Date().toLocaleDateString('es-ES')}
</span>
</div>
{/* Main Revenue */}
<div className="mb-4">
<div className="flex items-baseline space-x-2">
<span className="text-3xl font-bold text-gray-900">
{currentRevenue.toFixed(2)}
</span>
<div className={`flex items-center text-sm font-medium ${
isPositive ? 'text-green-600' : 'text-red-600'
}`}>
{isPositive ? (
<TrendingUp className="h-4 w-4 mr-1" />
) : (
<TrendingDown className="h-4 w-4 mr-1" />
)}
{isPositive ? '+' : ''}{changePercentage.toFixed(1)}%
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
{isPositive ? '+' : ''} {changeAmount.toFixed(2)} vs ayer
</p>
</div>
{/* Target Progress */}
<div className="mb-4">
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
<span>Meta del día: {dailyTarget}</span>
<span>{targetProgress.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
targetProgress >= 100 ? 'bg-green-500' :
targetProgress >= 80 ? 'bg-yellow-500' : 'bg-blue-500'
}`}
style={{ width: `${Math.min(targetProgress, 100)}%` }}
></div>
</div>
{remainingToTarget > 0 && (
<p className="text-sm text-gray-600 mt-2">
Faltan {remainingToTarget.toFixed(2)} para la meta
</p>
)}
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-3 pt-4 border-t border-gray-100">
<div className="text-center">
<p className="text-xs text-gray-500">Esta Semana</p>
<p className="text-lg font-semibold text-gray-900">1,940</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500">Promedio/Día</p>
<p className="text-lg font-semibold text-gray-900">{(1940/7).toFixed(0)}</p>
</div>
</div>
</div>
);
};
export default TodayRevenue;

View File

@@ -0,0 +1,438 @@
import { useState, useEffect } from 'react';
import { Brain, TrendingUp, TrendingDown, Star, Clock, Eye, EyeOff, MoreHorizontal } from 'lucide-react';
interface AIInsight {
id: string;
type: 'trend' | 'opportunity' | 'warning' | 'recommendation' | 'achievement';
title: string;
description: string;
confidence: number;
impact: 'high' | 'medium' | 'low';
category: 'demand' | 'revenue' | 'efficiency' | 'quality' | 'customer';
timestamp: string;
data?: {
trend?: {
direction: 'up' | 'down';
percentage: number;
period: string;
};
revenue?: {
amount: number;
comparison: string;
};
actionable?: {
action: string;
expectedImpact: string;
};
};
isRead?: boolean;
isStarred?: boolean;
}
interface AIInsightsFeedProps {
insights?: AIInsight[];
onInsightAction?: (insightId: string, action: 'read' | 'star' | 'dismiss') => void;
maxItems?: number;
showFilters?: boolean;
className?: string;
}
const AIInsightsFeed: React.FC<AIInsightsFeedProps> = ({
insights: propInsights,
onInsightAction,
maxItems = 10,
showFilters = true,
className = ''
}) => {
const [insights, setInsights] = useState<AIInsight[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [selectedImpact, setSelectedImpact] = useState<string>('all');
const [showOnlyUnread, setShowOnlyUnread] = useState(false);
// Generate realistic AI insights if none provided
useEffect(() => {
if (propInsights) {
setInsights(propInsights);
} else {
setInsights(generateSampleInsights());
}
}, [propInsights]);
const generateSampleInsights = (): AIInsight[] => {
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
return [
{
id: '1',
type: 'opportunity',
title: 'Oportunidad: Incremento en demanda de tartas',
description: 'Los datos muestran un aumento del 23% en la demanda de tartas los viernes. Considera aumentar la producción para maximizar ingresos.',
confidence: 87,
impact: 'high',
category: 'revenue',
timestamp: now.toISOString(),
data: {
trend: { direction: 'up', percentage: 23, period: 'últimas 3 semanas' },
actionable: { action: 'Aumentar producción de tartas los viernes', expectedImpact: '+€180/semana' }
},
isRead: false,
isStarred: false
},
{
id: '2',
type: 'warning',
title: 'Alerta: Posible desperdicio de magdalenas',
description: 'Las magdalenas tienen una tasa de venta del 67% los martes. Reducir la producción podría ahorrar €45 semanales.',
confidence: 78,
impact: 'medium',
category: 'efficiency',
timestamp: yesterday.toISOString(),
data: {
revenue: { amount: 45, comparison: 'ahorro semanal estimado' },
actionable: { action: 'Reducir producción de magdalenas los martes', expectedImpact: '-€45 desperdicio' }
},
isRead: true,
isStarred: false
},
{
id: '3',
type: 'trend',
title: 'Tendencia: Croissants más populares por las mañanas',
description: 'El 78% de las ventas de croissants ocurren antes de las 11 AM. Considera reorganizar la producción matutina.',
confidence: 92,
impact: 'medium',
category: 'efficiency',
timestamp: yesterday.toISOString(),
data: {
trend: { direction: 'up', percentage: 78, period: 'horario matutino' },
actionable: { action: 'Priorizar croissants en producción matutina', expectedImpact: 'Mejor disponibilidad' }
},
isRead: false,
isStarred: true
},
{
id: '4',
type: 'achievement',
title: '¡Éxito! Predicciones de ayer fueron 94% precisas',
description: 'Las predicciones de demanda de ayer tuvieron una precisión del 94%, resultando en ventas óptimas sin desperdicios.',
confidence: 94,
impact: 'high',
category: 'quality',
timestamp: yesterday.toISOString(),
data: {
revenue: { amount: 127, comparison: 'ingresos adicionales por precisión' }
},
isRead: false,
isStarred: false
},
{
id: '5',
type: 'recommendation',
title: 'Recomendación: Nuevo producto para fin de semana',
description: 'Los datos de fin de semana sugieren que los clientes buscan productos especiales. Una tarta de temporada podría generar €200 adicionales.',
confidence: 72,
impact: 'high',
category: 'revenue',
timestamp: twoDaysAgo.toISOString(),
data: {
revenue: { amount: 200, comparison: 'ingresos potenciales fin de semana' },
actionable: { action: 'Introducir tarta especial de fin de semana', expectedImpact: '+€200/semana' }
},
isRead: true,
isStarred: false
},
{
id: '6',
type: 'trend',
title: 'Patrón meteorológico: Lluvia afecta ventas -15%',
description: 'Los días lluviosos reducen las ventas en promedio 15%. El algoritmo ahora ajusta automáticamente las predicciones.',
confidence: 88,
impact: 'medium',
category: 'demand',
timestamp: twoDaysAgo.toISOString(),
data: {
trend: { direction: 'down', percentage: 15, period: 'días lluviosos' },
actionable: { action: 'Producción automáticamente ajustada', expectedImpact: 'Menos desperdicio' }
},
isRead: true,
isStarred: false
}
];
};
const handleInsightAction = (insightId: string, action: 'read' | 'star' | 'dismiss') => {
setInsights(prev => prev.map(insight => {
if (insight.id === insightId) {
switch (action) {
case 'read':
return { ...insight, isRead: true };
case 'star':
return { ...insight, isStarred: !insight.isStarred };
case 'dismiss':
return insight; // In a real app, this might remove the insight
}
}
return insight;
}));
onInsightAction?.(insightId, action);
};
const filteredInsights = insights.filter(insight => {
if (selectedCategory !== 'all' && insight.category !== selectedCategory) return false;
if (selectedImpact !== 'all' && insight.impact !== selectedImpact) return false;
if (showOnlyUnread && insight.isRead) return false;
return true;
}).slice(0, maxItems);
const getInsightIcon = (type: AIInsight['type']) => {
switch (type) {
case 'trend': return TrendingUp;
case 'opportunity': return Star;
case 'warning': return TrendingDown;
case 'recommendation': return Brain;
case 'achievement': return Star;
default: return Brain;
}
};
const getInsightColors = (type: AIInsight['type'], impact: AIInsight['impact']) => {
const baseColors = {
trend: 'blue',
opportunity: 'green',
warning: 'yellow',
recommendation: 'purple',
achievement: 'emerald'
};
const color = baseColors[type] || 'gray';
const intensity = impact === 'high' ? '600' : impact === 'medium' ? '500' : '400';
return {
background: `bg-${color}-50`,
border: `border-${color}-200`,
icon: `text-${color}-${intensity}`,
badge: impact === 'high' ? `bg-${color}-100 text-${color}-800` :
impact === 'medium' ? `bg-${color}-50 text-${color}-700` :
`bg-gray-100 text-gray-600`
};
};
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffHours < 1) return 'Hace unos minutos';
if (diffHours < 24) return `Hace ${diffHours}h`;
if (diffHours < 48) return 'Ayer';
return date.toLocaleDateString('es-ES', { day: 'numeric', month: 'short' });
};
const unreadCount = insights.filter(i => !i.isRead).length;
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Brain className="h-6 w-6 text-purple-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Insights de IA
</h3>
<p className="text-sm text-gray-600">
Recomendaciones inteligentes para tu negocio
{unreadCount > 0 && (
<span className="ml-2 px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded-full">
{unreadCount} nuevos
</span>
)}
</p>
</div>
</div>
</div>
{/* Filters */}
{showFilters && (
<div className="mt-4 flex flex-wrap gap-3">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="all">Todas las categorías</option>
<option value="demand">Demanda</option>
<option value="revenue">Ingresos</option>
<option value="efficiency">Eficiencia</option>
<option value="quality">Calidad</option>
<option value="customer">Cliente</option>
</select>
<select
value={selectedImpact}
onChange={(e) => setSelectedImpact(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="all">Todos los impactos</option>
<option value="high">Alto impacto</option>
<option value="medium">Impacto medio</option>
<option value="low">Bajo impacto</option>
</select>
<label className="flex items-center text-sm">
<input
type="checkbox"
checked={showOnlyUnread}
onChange={(e) => setShowOnlyUnread(e.target.checked)}
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded mr-2"
/>
Solo no leídos
</label>
</div>
)}
</div>
{/* Insights List */}
<div className="divide-y divide-gray-100 max-h-96 overflow-y-auto">
{filteredInsights.map((insight) => {
const IconComponent = getInsightIcon(insight.type);
const colors = getInsightColors(insight.type, insight.impact);
return (
<div
key={insight.id}
className={`p-4 hover:bg-gray-50 transition-colors ${!insight.isRead ? 'bg-purple-25' : ''}`}
>
<div className="flex items-start space-x-3">
{/* Icon */}
<div className={`flex-shrink-0 p-2 rounded-lg ${colors.background}`}>
<IconComponent className={`h-4 w-4 ${colors.icon}`} />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`text-sm font-medium ${!insight.isRead ? 'text-gray-900' : 'text-gray-700'}`}>
{insight.title}
</h4>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{insight.description}
</p>
{/* Metadata */}
<div className="flex items-center mt-2 space-x-3">
<span className="text-xs text-gray-500 flex items-center">
<Clock className="h-3 w-3 mr-1" />
{formatTimestamp(insight.timestamp)}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${colors.badge}`}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} impacto
</span>
<span className="text-xs text-gray-500">
{insight.confidence}% confianza
</span>
</div>
{/* Data Insights */}
{insight.data && (
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
{insight.data.trend && (
<div className="flex items-center text-sm">
{insight.data.trend.direction === 'up' ? (
<TrendingUp className="h-4 w-4 text-green-600 mr-2" />
) : (
<TrendingDown className="h-4 w-4 text-red-600 mr-2" />
)}
<span className="font-medium">
{insight.data.trend.percentage}% {insight.data.trend.direction === 'up' ? 'aumento' : 'reducción'}
</span>
<span className="text-gray-600 ml-1">
en {insight.data.trend.period}
</span>
</div>
)}
{insight.data.revenue && (
<div className="flex items-center text-sm">
<span className="font-medium text-green-600">{insight.data.revenue.amount}</span>
<span className="text-gray-600 ml-1">{insight.data.revenue.comparison}</span>
</div>
)}
{insight.data.actionable && (
<div className="mt-2 text-sm">
<div className="font-medium text-gray-900">{insight.data.actionable.action}</div>
<div className="text-green-600">{insight.data.actionable.expectedImpact}</div>
</div>
)}
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center space-x-1 ml-4">
<button
onClick={() => handleInsightAction(insight.id, 'star')}
className={`p-1.5 rounded-lg hover:bg-gray-100 transition-colors ${
insight.isStarred ? 'text-yellow-600' : 'text-gray-400 hover:text-gray-600'
}`}
>
<Star className={`h-4 w-4 ${insight.isStarred ? 'fill-current' : ''}`} />
</button>
<button
onClick={() => handleInsightAction(insight.id, 'read')}
className={`p-1.5 rounded-lg hover:bg-gray-100 transition-colors ${
insight.isRead ? 'text-gray-400' : 'text-blue-600 hover:text-blue-700'
}`}
>
{insight.isRead ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
<button className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors">
<MoreHorizontal className="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
</div>
);
})}
{filteredInsights.length === 0 && (
<div className="p-8 text-center">
<Brain className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay insights disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Los insights aparecerán aquí conforme el sistema analice tus datos
</p>
</div>
)}
</div>
{/* Footer */}
{filteredInsights.length > 0 && (
<div className="p-4 border-t border-gray-200 bg-gray-50">
<div className="text-center">
<p className="text-xs text-gray-500">
Mostrando {filteredInsights.length} de {insights.length} insights
</p>
<button className="text-xs text-purple-600 hover:text-purple-700 mt-1 font-medium">
Ver historial completo
</button>
</div>
</div>
)}
</div>
);
};
export default AIInsightsFeed;

View File

@@ -0,0 +1,155 @@
import React from 'react';
import { AlertTriangle, TrendingDown, Package, Clock, Euro } from 'lucide-react';
export interface BusinessAlert {
id: string;
type: 'stockout_risk' | 'overstock' | 'revenue_loss' | 'quality_risk' | 'weather_impact';
severity: 'low' | 'medium' | 'high' | 'critical';
product: string;
message: string;
action: string;
impact?: {
type: 'revenue' | 'units' | 'percentage';
value: number;
currency?: string;
};
urgency?: 'immediate' | 'today' | 'this_week';
}
interface AlertCardProps {
alert: BusinessAlert;
onAction?: (alertId: string, actionType: string) => void;
}
const getSeverityConfig = (severity: BusinessAlert['severity']) => {
switch (severity) {
case 'critical':
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
iconColor: 'text-red-600',
textColor: 'text-red-900',
actionColor: 'bg-red-100 hover:bg-red-200 text-red-800'
};
case 'high':
return {
bgColor: 'bg-orange-50',
borderColor: 'border-orange-200',
iconColor: 'text-orange-600',
textColor: 'text-orange-900',
actionColor: 'bg-orange-100 hover:bg-orange-200 text-orange-800'
};
case 'medium':
return {
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
iconColor: 'text-yellow-600',
textColor: 'text-yellow-900',
actionColor: 'bg-yellow-100 hover:bg-yellow-200 text-yellow-800'
};
default:
return {
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
iconColor: 'text-blue-600',
textColor: 'text-blue-900',
actionColor: 'bg-blue-100 hover:bg-blue-200 text-blue-800'
};
}
};
const getAlertIcon = (type: BusinessAlert['type']) => {
switch (type) {
case 'stockout_risk':
return Package;
case 'overstock':
return TrendingDown;
case 'revenue_loss':
return Euro;
case 'quality_risk':
return Clock;
case 'weather_impact':
return AlertTriangle;
default:
return AlertTriangle;
}
};
const getUrgencyLabel = (urgency?: BusinessAlert['urgency']) => {
switch (urgency) {
case 'immediate':
return { label: 'URGENTE', color: 'bg-red-100 text-red-800' };
case 'today':
return { label: 'HOY', color: 'bg-orange-100 text-orange-800' };
case 'this_week':
return { label: 'ESTA SEMANA', color: 'bg-blue-100 text-blue-800' };
default:
return null;
}
};
const AlertCard: React.FC<AlertCardProps> = ({ alert, onAction }) => {
const config = getSeverityConfig(alert.severity);
const Icon = getAlertIcon(alert.type);
const urgencyInfo = getUrgencyLabel(alert.urgency);
const handleAction = () => {
onAction?.(alert.id, 'primary_action');
};
return (
<div className={`${config.bgColor} ${config.borderColor} border rounded-lg p-4 shadow-sm`}>
<div className="flex items-start space-x-3">
<div className={`${config.iconColor} mt-0.5`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className={`font-medium ${config.textColor}`}>
{alert.product}
</h4>
{urgencyInfo && (
<span className={`px-2 py-1 text-xs font-bold rounded ${urgencyInfo.color}`}>
{urgencyInfo.label}
</span>
)}
</div>
<p className={`text-sm mt-1 ${config.textColor.replace('900', '700')}`}>
{alert.message}
</p>
{alert.impact && (
<div className={`text-sm font-medium mt-2 ${config.textColor}`}>
{alert.impact.type === 'revenue' && (
<>Impacto: -{alert.impact.value}{alert.impact.currency || '€'}</>
)}
{alert.impact.type === 'units' && (
<>Unidades afectadas: {alert.impact.value}</>
)}
{alert.impact.type === 'percentage' && (
<>Reducción estimada: {alert.impact.value}%</>
)}
</div>
)}
</div>
</div>
<div className="mt-3">
<button
onClick={handleAction}
className={`px-3 py-2 text-sm font-medium rounded transition-colors ${config.actionColor}`}
>
{alert.action}
</button>
</div>
</div>
</div>
</div>
);
};
export default AlertCard;

View File

@@ -0,0 +1,408 @@
import { useState } from 'react';
import { BarChart3, TrendingUp, TrendingDown, Award, Target, Eye, EyeOff } from 'lucide-react';
interface BenchmarkMetric {
id: string;
name: string;
yourValue: number;
industryAverage: number;
topPerformers: number;
unit: string;
description: string;
category: 'efficiency' | 'revenue' | 'waste' | 'customer' | 'quality';
trend: 'improving' | 'declining' | 'stable';
percentile: number; // Your position (0-100)
insights: string[];
}
interface CompetitiveBenchmarksProps {
metrics?: BenchmarkMetric[];
location?: string; // e.g., "Madrid Centro"
showSensitiveData?: boolean;
className?: string;
}
const CompetitiveBenchmarks: React.FC<CompetitiveBenchmarksProps> = ({
metrics: propMetrics,
location = "Madrid Centro",
showSensitiveData = true,
className = ''
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [showDetails, setShowDetails] = useState<boolean>(showSensitiveData);
// Sample benchmark data (anonymized)
const defaultMetrics: BenchmarkMetric[] = [
{
id: 'forecast_accuracy',
name: 'Precisión de Predicciones',
yourValue: 87.2,
industryAverage: 72.5,
topPerformers: 94.1,
unit: '%',
description: 'Qué tan precisas son las predicciones vs. ventas reales',
category: 'quality',
trend: 'improving',
percentile: 85,
insights: [
'Superioridad del 15% vs. promedio de la industria',
'Solo 7 puntos por debajo de los mejores del sector',
'Mejora consistente en los últimos 3 meses'
]
},
{
id: 'waste_percentage',
name: 'Porcentaje de Desperdicio',
yourValue: 8.3,
industryAverage: 12.7,
topPerformers: 4.2,
unit: '%',
description: 'Productos no vendidos como % del total producido',
category: 'waste',
trend: 'improving',
percentile: 78,
insights: [
'35% menos desperdicio que el promedio',
'Oportunidad: reducir 4 puntos más para llegar al top',
'Ahorro de ~€230/mes vs. promedio de la industria'
]
},
{
id: 'revenue_per_sqm',
name: 'Ingresos por m²',
yourValue: 2847,
industryAverage: 2134,
topPerformers: 4521,
unit: '€/mes',
description: 'Ingresos mensuales por metro cuadrado de local',
category: 'revenue',
trend: 'stable',
percentile: 73,
insights: [
'33% más eficiente en generación de ingresos',
'Potencial de crecimiento: +59% para alcanzar el top',
'Excelente aprovechamiento del espacio'
]
},
{
id: 'customer_retention',
name: 'Retención de Clientes',
yourValue: 68,
industryAverage: 61,
topPerformers: 84,
unit: '%',
description: 'Clientes que regresan al menos una vez por semana',
category: 'customer',
trend: 'improving',
percentile: 67,
insights: [
'11% mejor retención que la competencia',
'Oportunidad: programas de fidelización podrían sumar 16 puntos',
'Base de clientes sólida y leal'
]
},
{
id: 'production_efficiency',
name: 'Eficiencia de Producción',
yourValue: 1.8,
industryAverage: 2.3,
topPerformers: 1.2,
unit: 'h/100 unidades',
description: 'Tiempo promedio para producir 100 unidades',
category: 'efficiency',
trend: 'improving',
percentile: 71,
insights: [
'22% más rápido que el promedio',
'Excelente optimización de procesos',
'Margen para mejora: -33% para ser top performer'
]
},
{
id: 'profit_margin',
name: 'Margen de Ganancia',
yourValue: 32.5,
industryAverage: 28.1,
topPerformers: 41.7,
unit: '%',
description: 'Ganancia neta como % de los ingresos totales',
category: 'revenue',
trend: 'stable',
percentile: 69,
insights: [
'16% más rentable que la competencia',
'Sólida gestión de costos',
'Oportunidad: optimizar ingredientes premium'
]
}
];
const metrics = propMetrics || defaultMetrics;
const categories = [
{ id: 'all', name: 'Todas', count: metrics.length },
{ id: 'revenue', name: 'Ingresos', count: metrics.filter(m => m.category === 'revenue').length },
{ id: 'efficiency', name: 'Eficiencia', count: metrics.filter(m => m.category === 'efficiency').length },
{ id: 'waste', name: 'Desperdicio', count: metrics.filter(m => m.category === 'waste').length },
{ id: 'customer', name: 'Clientes', count: metrics.filter(m => m.category === 'customer').length },
{ id: 'quality', name: 'Calidad', count: metrics.filter(m => m.category === 'quality').length }
];
const filteredMetrics = metrics.filter(metric =>
selectedCategory === 'all' || metric.category === selectedCategory
);
const getPerformanceLevel = (percentile: number) => {
if (percentile >= 90) return { label: 'Excelente', color: 'text-green-600', bg: 'bg-green-50' };
if (percentile >= 75) return { label: 'Bueno', color: 'text-blue-600', bg: 'bg-blue-50' };
if (percentile >= 50) return { label: 'Promedio', color: 'text-yellow-600', bg: 'bg-yellow-50' };
return { label: 'Mejora Necesaria', color: 'text-red-600', bg: 'bg-red-50' };
};
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'improving': return <TrendingUp className="h-4 w-4 text-green-600" />;
case 'declining': return <TrendingDown className="h-4 w-4 text-red-600" />;
default: return <div className="w-4 h-4 bg-gray-400 rounded-full"></div>;
}
};
const getComparisonPercentage = (yourValue: number, compareValue: number, isLowerBetter = false) => {
const diff = isLowerBetter
? ((compareValue - yourValue) / compareValue) * 100
: ((yourValue - compareValue) / compareValue) * 100;
return {
value: Math.abs(diff),
isPositive: diff > 0
};
};
const isLowerBetter = (metricId: string) => {
return ['waste_percentage', 'production_efficiency'].includes(metricId);
};
const averagePercentile = Math.round(metrics.reduce((sum, m) => sum + m.percentile, 0) / metrics.length);
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<BarChart3 className="h-6 w-6 text-indigo-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Benchmarks Competitivos
</h3>
<p className="text-sm text-gray-600">
Comparación anónima con panaderías similares en {location}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
{/* Overall Score */}
<div className="text-right">
<div className="text-2xl font-bold text-indigo-600">
{averagePercentile}
</div>
<div className="text-xs text-gray-600">Percentil General</div>
</div>
{/* Toggle Details */}
<button
onClick={() => setShowDetails(!showDetails)}
className="p-2 rounded-lg hover:bg-gray-100 text-gray-600"
title={showDetails ? "Ocultar detalles" : "Mostrar detalles"}
>
{showDetails ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
{/* Performance Summary */}
<div className="mt-4 grid grid-cols-3 gap-4">
<div className="bg-green-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-green-600">
{metrics.filter(m => m.percentile >= 75).length}
</div>
<div className="text-xs text-green-700">Métricas Top 25%</div>
</div>
<div className="bg-blue-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-blue-600">
{metrics.filter(m => m.trend === 'improving').length}
</div>
<div className="text-xs text-blue-700">En Mejora</div>
</div>
<div className="bg-yellow-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-yellow-600">
{metrics.filter(m => m.percentile < 50).length}
</div>
<div className="text-xs text-yellow-700">Áreas de Oportunidad</div>
</div>
</div>
{/* Category Filters */}
<div className="mt-4 flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === category.id
? 'bg-indigo-100 text-indigo-800 border border-indigo-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{category.name}
<span className="ml-1.5 text-xs bg-white rounded-full px-1.5 py-0.5">
{category.count}
</span>
</button>
))}
</div>
</div>
{/* Metrics List */}
<div className="divide-y divide-gray-100">
{filteredMetrics.map(metric => {
const performance = getPerformanceLevel(metric.percentile);
const vsAverage = getComparisonPercentage(
metric.yourValue,
metric.industryAverage,
isLowerBetter(metric.id)
);
const vsTop = getComparisonPercentage(
metric.yourValue,
metric.topPerformers,
isLowerBetter(metric.id)
);
return (
<div key={metric.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-3">
<h4 className="text-lg font-medium text-gray-900">
{metric.name}
</h4>
{getTrendIcon(metric.trend)}
<span className={`px-2 py-1 text-xs rounded-full ${performance.bg} ${performance.color}`}>
{performance.label}
</span>
</div>
<p className="text-sm text-gray-600 mt-1">{metric.description}</p>
</div>
<div className="text-right ml-4">
<div className="text-2xl font-bold text-gray-900">
{metric.yourValue.toLocaleString('es-ES')}<span className="text-sm text-gray-500">{metric.unit}</span>
</div>
<div className="text-sm text-gray-600">Tu Resultado</div>
</div>
</div>
{/* Comparison Bars */}
<div className="space-y-3">
{/* Your Performance */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium text-gray-900">Tu Rendimiento</span>
<span className="text-sm text-indigo-600 font-medium">Percentil {metric.percentile}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-indigo-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${metric.percentile}%` }}
></div>
</div>
</div>
{/* Industry Average */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600">Promedio Industria</span>
<span className="text-sm text-gray-600">
{metric.industryAverage.toLocaleString('es-ES')}{metric.unit}
<span className={`ml-2 ${vsAverage.isPositive ? 'text-green-600' : 'text-red-600'}`}>
({vsAverage.isPositive ? '+' : '-'}{vsAverage.value.toFixed(1)}%)
</span>
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-gray-400 h-1.5 rounded-full"
style={{ width: '50%' }}
></div>
</div>
</div>
{/* Top Performers */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600 flex items-center">
<Award className="h-3 w-3 mr-1 text-yellow-500" />
Top Performers
</span>
<span className="text-sm text-gray-600">
{metric.topPerformers.toLocaleString('es-ES')}{metric.unit}
<span className={`ml-2 ${vsTop.isPositive ? 'text-green-600' : 'text-orange-600'}`}>
({vsTop.isPositive ? '+' : '-'}{vsTop.value.toFixed(1)}%)
</span>
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-yellow-400 h-1.5 rounded-full"
style={{ width: '90%' }}
></div>
</div>
</div>
</div>
{/* Insights */}
{showDetails && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<h5 className="text-sm font-medium text-gray-700 mb-2 flex items-center">
<Target className="h-4 w-4 mr-2 text-indigo-600" />
Insights Clave:
</h5>
<ul className="space-y-1">
{metric.insights.map((insight, index) => (
<li key={index} className="text-sm text-gray-600 flex items-start">
<div className="w-1.5 h-1.5 bg-indigo-400 rounded-full mt-2 mr-2 flex-shrink-0"></div>
{insight}
</li>
))}
</ul>
</div>
)}
</div>
);
})}
</div>
{filteredMetrics.length === 0 && (
<div className="p-8 text-center">
<BarChart3 className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay métricas disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Los benchmarks aparecerán cuando haya suficientes datos
</p>
</div>
)}
{/* Disclaimer */}
<div className="px-6 pb-4">
<div className="bg-gray-50 rounded-lg p-3">
<div className="text-xs text-gray-600">
<strong>🔒 Privacidad:</strong> Todos los datos están anonimizados.
Solo se comparten métricas agregadas de panaderías similares en tamaño y ubicación.
</div>
</div>
</div>
</div>
);
};
export default CompetitiveBenchmarks;

View File

@@ -0,0 +1,287 @@
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Calendar, TrendingUp, Eye } from 'lucide-react';
export interface DayDemand {
date: string;
demand: number;
isToday?: boolean;
isForecast?: boolean;
products?: Array<{
name: string;
demand: number;
confidence: 'high' | 'medium' | 'low';
}>;
}
export interface WeekData {
weekStart: string;
days: DayDemand[];
}
interface DemandHeatmapProps {
data: WeekData[];
selectedProduct?: string;
onDateClick?: (date: string) => void;
className?: string;
}
const DemandHeatmap: React.FC<DemandHeatmapProps> = ({
data,
selectedProduct,
onDateClick,
className = ''
}) => {
const [currentWeekIndex, setCurrentWeekIndex] = useState(0);
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const maxDemand = Math.max(
...data.flatMap(week => week.days.map(day => day.demand))
);
const getDemandIntensity = (demand: number) => {
const intensity = demand / maxDemand;
if (intensity > 0.8) return 'bg-red-500';
if (intensity > 0.6) return 'bg-orange-500';
if (intensity > 0.4) return 'bg-yellow-500';
if (intensity > 0.2) return 'bg-green-500';
return 'bg-gray-200';
};
const getDemandLabel = (demand: number) => {
const intensity = demand / maxDemand;
if (intensity > 0.8) return 'Muy Alta';
if (intensity > 0.6) return 'Alta';
if (intensity > 0.4) return 'Media';
if (intensity > 0.2) return 'Baja';
return 'Muy Baja';
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.getDate().toString();
};
const formatWeekRange = (weekStart: string) => {
const start = new Date(weekStart);
const end = new Date(start);
end.setDate(end.getDate() + 6);
return `${start.getDate()}-${end.getDate()} ${start.toLocaleDateString('es-ES', { month: 'short' })}`;
};
const handleDateClick = (date: string) => {
setSelectedDate(date);
onDateClick?.(date);
};
const currentWeek = data[currentWeekIndex];
const selectedDay = selectedDate ?
data.flatMap(w => w.days).find(d => d.date === selectedDate) : null;
return (
<div className={`bg-white rounded-xl shadow-soft p-6 ${className}`}>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Calendar className="h-5 w-5 mr-2 text-primary-600" />
Mapa de Calor de Demanda
</h3>
<p className="text-sm text-gray-600 mt-1">
Patrones de demanda visual por día
{selectedProduct && ` - ${selectedProduct}`}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setCurrentWeekIndex(Math.max(0, currentWeekIndex - 1))}
disabled={currentWeekIndex === 0}
className="p-2 rounded-lg bg-gray-100 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-sm font-medium text-gray-700 min-w-[100px] text-center">
{currentWeek ? formatWeekRange(currentWeek.weekStart) : 'Esta Semana'}
</span>
<button
onClick={() => setCurrentWeekIndex(Math.min(data.length - 1, currentWeekIndex + 1))}
disabled={currentWeekIndex === data.length - 1}
className="p-2 rounded-lg bg-gray-100 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
{/* Heatmap Grid */}
<div className="grid grid-cols-7 gap-2 mb-6">
{['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'].map(day => (
<div key={day} className="text-center text-xs font-medium text-gray-600 p-2">
{day}
</div>
))}
{currentWeek?.days.map(day => (
<button
key={day.date}
onClick={() => handleDateClick(day.date)}
className={`
relative p-3 rounded-lg text-white text-sm font-medium
transition-all duration-200 hover:scale-105 hover:shadow-md
${getDemandIntensity(day.demand)}
${selectedDate === day.date ? 'ring-2 ring-primary-600 ring-offset-2' : ''}
${day.isToday ? 'ring-2 ring-blue-400' : ''}
`}
>
<div className="text-center">
<div className="text-lg font-bold">{formatDate(day.date)}</div>
<div className="text-xs opacity-90">{day.demand}</div>
{day.isForecast && (
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-400 rounded-full"></div>
)}
</div>
</button>
))}
</div>
{/* Legend */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">Demanda:</span>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-gray-200 rounded"></div>
<span className="text-xs">Muy Baja</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-green-500 rounded"></div>
<span className="text-xs">Baja</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-yellow-500 rounded"></div>
<span className="text-xs">Media</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-orange-500 rounded"></div>
<span className="text-xs">Alta</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-red-500 rounded"></div>
<span className="text-xs">Muy Alta</span>
</div>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-400 rounded-full mr-1"></div>
Predicción
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-600 rounded-full mr-1"></div>
Hoy
</div>
</div>
</div>
{/* Selected Day Details */}
{selectedDay && (
<div className="border-t border-gray-200 pt-4">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-gray-900 flex items-center">
<Eye className="h-4 w-4 mr-2 text-primary-600" />
{new Date(selectedDay.date).toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long'
})}
{selectedDay.isToday && (
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
Hoy
</span>
)}
</h4>
<div className="mt-2 flex items-center space-x-4">
<div className="flex items-center">
<TrendingUp className="h-4 w-4 text-gray-600 mr-1" />
<span className="text-sm text-gray-600">
Demanda Total: <span className="font-medium">{selectedDay.demand}</span>
</span>
</div>
<span className={`px-2 py-1 text-xs rounded-full ${
getDemandIntensity(selectedDay.demand)
} text-white`}>
{getDemandLabel(selectedDay.demand)}
</span>
</div>
</div>
</div>
{/* Product Breakdown */}
{selectedDay.products && (
<div className="mt-4">
<h5 className="text-sm font-medium text-gray-700 mb-3">Desglose por Producto:</h5>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{selectedDay.products.map((product, index) => (
<div key={index} className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{product.name}</span>
<span className={`px-2 py-1 text-xs rounded ${
product.confidence === 'high' ? 'bg-green-100 text-green-800' :
product.confidence === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{product.confidence === 'high' ? 'Alta' :
product.confidence === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
<div className="text-lg font-bold text-gray-900 mt-1">
{product.demand}
</div>
<div className="text-xs text-gray-600">unidades</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Weekly Summary */}
{currentWeek && (
<div className="mt-6 bg-gray-50 rounded-lg p-4">
<h5 className="text-sm font-medium text-gray-700 mb-2">Resumen Semanal:</h5>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div>
<div className="text-lg font-bold text-gray-900">
{currentWeek.days.reduce((sum, day) => sum + day.demand, 0)}
</div>
<div className="text-xs text-gray-600">Total</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{Math.round(currentWeek.days.reduce((sum, day) => sum + day.demand, 0) / 7)}
</div>
<div className="text-xs text-gray-600">Promedio/día</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{Math.max(...currentWeek.days.map(d => d.demand))}
</div>
<div className="text-xs text-gray-600">Pico</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{currentWeek.days.filter(d => d.isForecast).length}
</div>
<div className="text-xs text-gray-600">Predicciones</div>
</div>
</div>
</div>
)}
</div>
);
};
export default DemandHeatmap;

View File

@@ -0,0 +1,207 @@
import React from 'react';
import { Clock, CheckCircle, AlertTriangle, TrendingUp } from 'lucide-react';
export interface ProductionItem {
id: string;
product: string;
quantity: number;
priority: 'high' | 'medium' | 'low';
estimatedTime: number; // minutes
status: 'pending' | 'in_progress' | 'completed';
confidence: number; // 0-1
notes?: string;
}
export interface ProductionTimeSlot {
time: string;
items: ProductionItem[];
totalTime: number;
}
interface ProductionScheduleProps {
schedule: ProductionTimeSlot[];
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
className?: string;
}
const getPriorityConfig = (priority: ProductionItem['priority']) => {
switch (priority) {
case 'high':
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
textColor: 'text-red-800',
label: 'ALTA'
};
case 'medium':
return {
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
textColor: 'text-yellow-800',
label: 'MEDIA'
};
default:
return {
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
textColor: 'text-green-800',
label: 'BAJA'
};
}
};
const getStatusIcon = (status: ProductionItem['status']) => {
switch (status) {
case 'completed':
return <CheckCircle className="h-4 w-4 text-success-600" />;
case 'in_progress':
return <Clock className="h-4 w-4 text-primary-600" />;
default:
return <AlertTriangle className="h-4 w-4 text-gray-400" />;
}
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'text-success-600';
if (confidence >= 0.6) return 'text-warning-600';
return 'text-danger-600';
};
const ProductionItem: React.FC<{
item: ProductionItem;
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
}> = ({ item, onUpdateQuantity, onUpdateStatus }) => {
const priorityConfig = getPriorityConfig(item.priority);
const handleQuantityChange = (delta: number) => {
const newQuantity = Math.max(0, item.quantity + delta);
onUpdateQuantity?.(item.id, newQuantity);
};
return (
<div className="bg-white border border-gray-200 rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{getStatusIcon(item.status)}
<span className="font-medium text-gray-900">{item.product}</span>
<span className={`px-2 py-1 text-xs font-medium rounded ${priorityConfig.bgColor} ${priorityConfig.borderColor} ${priorityConfig.textColor}`}>
{priorityConfig.label}
</span>
</div>
<div className={`text-sm font-medium ${getConfidenceColor(item.confidence)}`}>
{Math.round(item.confidence * 100)}% confianza
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<button
onClick={() => handleQuantityChange(-5)}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm font-medium"
disabled={item.quantity <= 5}
>
-
</button>
<span className="w-12 text-center font-bold text-lg">{item.quantity}</span>
<button
onClick={() => handleQuantityChange(5)}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm font-medium"
>
+
</button>
</div>
<span className="text-sm text-gray-600">unidades</span>
</div>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<Clock className="h-4 w-4" />
<span>{item.estimatedTime} min</span>
</div>
</div>
{item.notes && (
<div className="text-sm text-gray-600 bg-gray-50 rounded p-2">
{item.notes}
</div>
)}
<div className="flex space-x-2">
{item.status === 'pending' && (
<button
onClick={() => onUpdateStatus?.(item.id, 'in_progress')}
className="flex-1 px-3 py-2 text-sm font-medium text-primary-700 bg-primary-100 hover:bg-primary-200 rounded transition-colors"
>
Iniciar Producción
</button>
)}
{item.status === 'in_progress' && (
<button
onClick={() => onUpdateStatus?.(item.id, 'completed')}
className="flex-1 px-3 py-2 text-sm font-medium text-success-700 bg-success-100 hover:bg-success-200 rounded transition-colors"
>
Marcar Completado
</button>
)}
{item.status === 'completed' && (
<div className="flex-1 px-3 py-2 text-sm font-medium text-success-700 bg-success-100 rounded text-center">
Completado
</div>
)}
</div>
</div>
);
};
const ProductionSchedule: React.FC<ProductionScheduleProps> = ({
schedule,
onUpdateQuantity,
onUpdateStatus,
className = ''
}) => {
const getTotalItems = (items: ProductionItem[]) => {
return items.reduce((sum, item) => sum + item.quantity, 0);
};
return (
<div className={`space-y-6 ${className}`}>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
Plan de Producción de Hoy
</h3>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<TrendingUp className="h-4 w-4" />
<span>Optimizado por IA</span>
</div>
</div>
{schedule.map((timeSlot, index) => (
<div key={index} className="space-y-4">
<div className="flex items-center space-x-3">
<div className="bg-primary-100 text-primary-800 px-3 py-1 rounded-lg font-medium text-sm">
{timeSlot.time}
</div>
<div className="text-sm text-gray-600">
{getTotalItems(timeSlot.items)} unidades {timeSlot.totalTime} min total
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{timeSlot.items.map((item) => (
<ProductionItem
key={item.id}
item={item}
onUpdateQuantity={onUpdateQuantity}
onUpdateStatus={onUpdateStatus}
/>
))}
</div>
</div>
))}
</div>
);
};
export default ProductionSchedule;

View File

@@ -0,0 +1,373 @@
import { useState } from 'react';
import {
Zap, Plus, Minus, RotateCcw, TrendingUp, ShoppingCart,
Clock, AlertTriangle, Phone, MessageSquare, Calculator,
RefreshCw, Package, Users, Settings, ChevronRight
} from 'lucide-react';
interface QuickAction {
id: string;
title: string;
description: string;
icon: any;
category: 'production' | 'inventory' | 'sales' | 'customer' | 'system';
shortcut?: string;
requiresConfirmation?: boolean;
estimatedTime?: string;
badge?: {
text: string;
color: 'red' | 'yellow' | 'green' | 'blue' | 'purple';
};
}
interface QuickActionsPanelProps {
onActionClick?: (actionId: string) => void;
availableActions?: QuickAction[];
compactMode?: boolean;
showCategories?: boolean;
className?: string;
}
const QuickActionsPanel: React.FC<QuickActionsPanelProps> = ({
onActionClick,
availableActions,
compactMode = false,
showCategories = true,
className = ''
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
const defaultActions: QuickAction[] = [
// Production Actions
{
id: 'increase_production',
title: 'Aumentar Producción',
description: 'Incrementa rápidamente la producción del producto más demandado',
icon: Plus,
category: 'production',
shortcut: 'P + A',
estimatedTime: '2 min',
badge: { text: 'Croissants', color: 'green' }
},
{
id: 'emergency_batch',
title: 'Lote de Emergencia',
description: 'Inicia producción urgente para productos con stock bajo',
icon: AlertTriangle,
category: 'production',
requiresConfirmation: true,
estimatedTime: '45 min',
badge: { text: '3 productos', color: 'red' }
},
{
id: 'adjust_schedule',
title: 'Ajustar Horario',
description: 'Modifica el horario de producción basado en predicciones',
icon: Clock,
category: 'production',
estimatedTime: '1 min'
},
// Inventory Actions
{
id: 'check_stock',
title: 'Revisar Stock',
description: 'Verifica niveles de inventario y productos próximos a agotarse',
icon: Package,
category: 'inventory',
shortcut: 'I + S',
badge: { text: '2 bajos', color: 'yellow' }
},
{
id: 'order_supplies',
title: 'Pedir Suministros',
description: 'Genera orden automática de ingredientes basada en predicciones',
icon: ShoppingCart,
category: 'inventory',
estimatedTime: '3 min',
badge: { text: 'Harina, Huevos', color: 'blue' }
},
{
id: 'waste_report',
title: 'Reportar Desperdicio',
description: 'Registra productos no vendidos para mejorar predicciones',
icon: Minus,
category: 'inventory',
estimatedTime: '2 min'
},
// Sales Actions
{
id: 'price_adjustment',
title: 'Ajustar Precios',
description: 'Modifica precios para productos con baja rotación',
icon: Calculator,
category: 'sales',
requiresConfirmation: true,
badge: { text: 'Magdalenas -10%', color: 'yellow' }
},
{
id: 'promotion_activate',
title: 'Activar Promoción',
description: 'Inicia promoción instantánea para productos específicos',
icon: TrendingUp,
category: 'sales',
estimatedTime: '1 min',
badge: { text: '2x1 Tartas', color: 'green' }
},
// Customer Actions
{
id: 'notify_customers',
title: 'Avisar Clientes',
description: 'Notifica a clientes regulares sobre disponibilidad especial',
icon: MessageSquare,
category: 'customer',
badge: { text: '15 clientes', color: 'blue' }
},
{
id: 'call_supplier',
title: 'Llamar Proveedor',
description: 'Contacto rápido con proveedor principal',
icon: Phone,
category: 'customer',
estimatedTime: '5 min'
},
// System Actions
{
id: 'refresh_predictions',
title: 'Actualizar Predicciones',
description: 'Recalcula predicciones con datos más recientes',
icon: RefreshCw,
category: 'system',
estimatedTime: '30 seg'
},
{
id: 'backup_data',
title: 'Respaldar Datos',
description: 'Crea respaldo de la información del día',
icon: RotateCcw,
category: 'system',
estimatedTime: '1 min'
}
];
const actions = availableActions || defaultActions;
const categories = [
{ id: 'all', name: 'Todas', icon: Zap },
{ id: 'production', name: 'Producción', icon: Package },
{ id: 'inventory', name: 'Inventario', icon: ShoppingCart },
{ id: 'sales', name: 'Ventas', icon: TrendingUp },
{ id: 'customer', name: 'Clientes', icon: Users },
{ id: 'system', name: 'Sistema', icon: Settings }
];
const filteredActions = actions.filter(action =>
selectedCategory === 'all' || action.category === selectedCategory
);
const handleActionClick = async (action: QuickAction) => {
if (action.requiresConfirmation) {
const confirmed = window.confirm(`¿Estás seguro de que quieres ejecutar "${action.title}"?`);
if (!confirmed) return;
}
setActionInProgress(action.id);
// Simulate action execution
await new Promise(resolve => setTimeout(resolve, 1000));
setActionInProgress(null);
onActionClick?.(action.id);
};
const getBadgeColors = (color: string) => {
const colors = {
red: 'bg-red-100 text-red-800',
yellow: 'bg-yellow-100 text-yellow-800',
green: 'bg-green-100 text-green-800',
blue: 'bg-blue-100 text-blue-800',
purple: 'bg-purple-100 text-purple-800'
};
return colors[color as keyof typeof colors] || colors.blue;
};
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Zap className="h-6 w-6 text-yellow-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Acciones Rápidas
</h3>
<p className="text-sm text-gray-600">
Tareas comunes del día a día
</p>
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-500">
{filteredActions.length} acciones disponibles
</div>
<div className="text-xs text-gray-400">
Usa atajos de teclado para mayor velocidad
</div>
</div>
</div>
{/* Category Filters */}
{showCategories && !compactMode && (
<div className="mt-4 flex flex-wrap gap-2">
{categories.map(category => {
const IconComponent = category.icon;
const isSelected = selectedCategory === category.id;
return (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
isSelected
? 'bg-yellow-100 text-yellow-800 border border-yellow-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border border-transparent'
}`}
>
<IconComponent className="h-4 w-4 mr-1.5" />
{category.name}
{category.id !== 'all' && (
<span className="ml-1.5 text-xs bg-white rounded-full px-1.5 py-0.5">
{actions.filter(a => a.category === category.id).length}
</span>
)}
</button>
);
})}
</div>
)}
</div>
{/* Actions Grid */}
<div className="p-6">
<div className={`grid gap-4 ${
compactMode
? 'grid-cols-1'
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
}`}>
{filteredActions.map(action => {
const IconComponent = action.icon;
const isInProgress = actionInProgress === action.id;
return (
<button
key={action.id}
onClick={() => handleActionClick(action)}
disabled={isInProgress}
className={`relative group text-left p-4 border border-gray-200 rounded-lg hover:border-yellow-300 hover:shadow-md transition-all duration-200 ${
isInProgress ? 'opacity-50 cursor-not-allowed' : 'hover:scale-[1.02]'
} ${compactMode ? 'flex items-center' : 'block'}`}
>
{/* Loading overlay */}
{isInProgress && (
<div className="absolute inset-0 bg-white bg-opacity-90 rounded-lg flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-600"></div>
</div>
)}
<div className={`flex ${compactMode ? 'items-center space-x-3' : 'items-start justify-between'}`}>
<div className={`flex ${compactMode ? 'items-center space-x-3' : 'items-start space-x-3'}`}>
{/* Icon */}
<div className="flex-shrink-0 p-2 bg-yellow-50 rounded-lg group-hover:bg-yellow-100 transition-colors">
<IconComponent className="h-5 w-5 text-yellow-600" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900 group-hover:text-yellow-700">
{action.title}
</h4>
{/* Badges */}
<div className="flex items-center space-x-2 ml-2">
{action.badge && (
<span className={`px-2 py-1 text-xs rounded-full ${getBadgeColors(action.badge.color)}`}>
{action.badge.text}
</span>
)}
{action.shortcut && !compactMode && (
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded border font-mono">
{action.shortcut}
</span>
)}
</div>
</div>
{!compactMode && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{action.description}
</p>
)}
{/* Metadata */}
<div className="flex items-center mt-2 space-x-3">
{action.estimatedTime && (
<span className="flex items-center text-xs text-gray-500">
<Clock className="h-3 w-3 mr-1" />
{action.estimatedTime}
</span>
)}
{action.requiresConfirmation && (
<span className="flex items-center text-xs text-orange-600">
<AlertTriangle className="h-3 w-3 mr-1" />
Requiere confirmación
</span>
)}
</div>
</div>
</div>
{/* Arrow indicator */}
{!compactMode && (
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-yellow-600 transition-colors flex-shrink-0" />
)}
</div>
</button>
);
})}
</div>
{filteredActions.length === 0 && (
<div className="text-center py-8">
<Zap className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay acciones disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Las acciones aparecerán basadas en el estado actual de tu panadería
</p>
</div>
)}
</div>
{/* Keyboard Shortcuts Help */}
{!compactMode && (
<div className="px-6 pb-4">
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between text-xs text-gray-600">
<span className="font-medium">💡 Tip:</span>
<span>Usa Ctrl + K para búsqueda rápida de acciones</span>
</div>
</div>
</div>
)}
</div>
);
};
export default QuickActionsPanel;

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { Euro, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
export interface RevenueData {
projectedDailyRevenue: number;
lostRevenueFromStockouts: number;
wasteCost: number;
revenueTrend: 'up' | 'down' | 'stable';
trendPercentage: number;
currency: string;
}
interface RevenueMetricsProps {
revenueData: RevenueData;
className?: string;
}
const formatCurrency = (amount: number, currency: string = '€') => {
return `${amount.toFixed(0)}${currency}`;
};
const RevenueMetrics: React.FC<RevenueMetricsProps> = ({ revenueData, className = '' }) => {
const getTrendIcon = () => {
switch (revenueData.revenueTrend) {
case 'up':
return <TrendingUp className="h-4 w-4 text-success-600" />;
case 'down':
return <TrendingDown className="h-4 w-4 text-danger-600" />;
default:
return <TrendingUp className="h-4 w-4 text-gray-600" />;
}
};
const getTrendColor = () => {
switch (revenueData.revenueTrend) {
case 'up':
return 'text-success-600';
case 'down':
return 'text-danger-600';
default:
return 'text-gray-600';
}
};
return (
<div className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}>
{/* Projected Daily Revenue */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-2">
<div className="p-2 bg-success-100 rounded-lg">
<Euro className="h-6 w-6 text-success-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Ingresos Previstos Hoy</p>
<p className="text-2xl font-bold text-gray-900">
{formatCurrency(revenueData.projectedDailyRevenue, revenueData.currency)}
</p>
</div>
</div>
<div className={`flex items-center mt-2 text-sm ${getTrendColor()}`}>
{getTrendIcon()}
<span className="ml-1">
{revenueData.trendPercentage > 0 ? '+' : ''}{revenueData.trendPercentage}% vs ayer
</span>
</div>
</div>
</div>
</div>
{/* Lost Revenue from Stockouts */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center space-x-2">
<div className="p-2 bg-danger-100 rounded-lg">
<AlertCircle className="h-6 w-6 text-danger-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Ventas Perdidas</p>
<p className="text-2xl font-bold text-danger-700">
-{formatCurrency(revenueData.lostRevenueFromStockouts, revenueData.currency)}
</p>
<p className="text-xs text-gray-500 mt-1">Por falta de stock (últimos 7 días)</p>
</div>
</div>
</div>
{/* Waste Cost Tracker */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center space-x-2">
<div className="p-2 bg-warning-100 rounded-lg">
<TrendingDown className="h-6 w-6 text-warning-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Coste Desperdicio</p>
<p className="text-2xl font-bold text-warning-700">
-{formatCurrency(revenueData.wasteCost, revenueData.currency)}
</p>
<p className="text-xs text-gray-500 mt-1">Productos no vendidos (esta semana)</p>
</div>
</div>
</div>
</div>
);
};
export default RevenueMetrics;

View File

@@ -0,0 +1,557 @@
import { useState } from 'react';
import { Play, RotateCcw, TrendingUp, TrendingDown, AlertTriangle, Euro, Calendar } from 'lucide-react';
interface Scenario {
id: string;
name: string;
description: string;
type: 'weather' | 'promotion' | 'event' | 'supply' | 'custom';
icon: any;
parameters: {
[key: string]: {
label: string;
type: 'number' | 'select' | 'boolean';
value: any;
options?: string[];
min?: number;
max?: number;
step?: number;
unit?: string;
};
};
}
interface ScenarioResult {
scenarioId: string;
demandChange: number;
revenueImpact: number;
productImpacts: Array<{
name: string;
demandChange: number;
newDemand: number;
revenueImpact: number;
}>;
recommendations: string[];
confidence: 'high' | 'medium' | 'low';
}
interface WhatIfPlannerProps {
baselineData?: {
totalDemand: number;
totalRevenue: number;
products: Array<{
name: string;
demand: number;
price: number;
}>;
};
onScenarioRun?: (scenario: Scenario, result: ScenarioResult) => void;
className?: string;
}
const WhatIfPlanner: React.FC<WhatIfPlannerProps> = ({
baselineData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Croissants', demand: 45, price: 2.5 },
{ name: 'Pan', demand: 30, price: 1.8 },
{ name: 'Magdalenas', demand: 25, price: 1.2 },
{ name: 'Empanadas', demand: 20, price: 3.2 },
{ name: 'Tartas', demand: 15, price: 12.0 },
]
},
onScenarioRun,
className = ''
}) => {
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
const [scenarioResult, setScenarioResult] = useState<ScenarioResult | null>(null);
const [isRunning, setIsRunning] = useState(false);
const scenarios: Scenario[] = [
{
id: 'rain',
name: 'Día Lluvioso',
description: 'Simula el impacto de un día de lluvia en Madrid',
type: 'weather',
icon: AlertTriangle,
parameters: {
rainIntensity: {
label: 'Intensidad de lluvia',
type: 'select',
value: 'moderate',
options: ['light', 'moderate', 'heavy']
},
temperature: {
label: 'Temperatura (°C)',
type: 'number',
value: 15,
min: 5,
max: 25,
step: 1,
unit: '°C'
}
}
},
{
id: 'promotion',
name: 'Promoción Especial',
description: 'Aplica un descuento y ve el impacto en la demanda',
type: 'promotion',
icon: TrendingUp,
parameters: {
discount: {
label: 'Descuento',
type: 'number',
value: 20,
min: 5,
max: 50,
step: 5,
unit: '%'
},
targetProduct: {
label: 'Producto objetivo',
type: 'select',
value: 'Croissants',
options: baselineData.products.map(p => p.name)
},
duration: {
label: 'Duración (días)',
type: 'number',
value: 3,
min: 1,
max: 7,
step: 1,
unit: 'días'
}
}
},
{
id: 'weekend',
name: 'Fin de Semana',
description: 'Simula la demanda típica de fin de semana',
type: 'event',
icon: Calendar,
parameters: {
dayType: {
label: 'Tipo de día',
type: 'select',
value: 'saturday',
options: ['saturday', 'sunday', 'holiday']
},
weatherGood: {
label: 'Buen tiempo',
type: 'boolean',
value: true
}
}
},
{
id: 'supply_shortage',
name: 'Escasez de Ingredientes',
description: 'Simula falta de ingredientes clave',
type: 'supply',
icon: AlertTriangle,
parameters: {
ingredient: {
label: 'Ingrediente afectado',
type: 'select',
value: 'flour',
options: ['flour', 'butter', 'eggs', 'sugar', 'chocolate']
},
shortage: {
label: 'Nivel de escasez',
type: 'select',
value: 'moderate',
options: ['mild', 'moderate', 'severe']
}
}
}
];
const runScenario = async (scenario: Scenario) => {
setIsRunning(true);
setScenarioResult(null);
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1500));
// Generate realistic scenario results based on parameters
const result = generateScenarioResult(scenario, baselineData);
setScenarioResult(result);
setIsRunning(false);
onScenarioRun?.(scenario, result);
};
const generateScenarioResult = (scenario: Scenario, baseline: typeof baselineData): ScenarioResult => {
let demandChange = 0;
let productImpacts: ScenarioResult['productImpacts'] = [];
let recommendations: string[] = [];
let confidence: 'high' | 'medium' | 'low' = 'medium';
switch (scenario.id) {
case 'rain':
const intensity = scenario.parameters.rainIntensity.value;
demandChange = intensity === 'light' ? -5 : intensity === 'moderate' ? -15 : -25;
confidence = 'high';
recommendations = [
'Reduce la producción de productos para llevar',
'Aumenta café caliente y productos de temporada',
'Prepara promociones para el día siguiente'
];
productImpacts = baseline.products.map(product => {
const change = product.name === 'Croissants' ? demandChange * 1.2 : demandChange;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: Math.max(0, product.demand + Math.round(change * product.demand / 100)),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
case 'promotion':
const discount = scenario.parameters.discount.value;
const targetProduct = scenario.parameters.targetProduct.value;
demandChange = discount * 1.5; // 20% discount = ~30% demand increase
confidence = 'high';
recommendations = [
`Aumenta la producción de ${targetProduct} en un ${Math.round(demandChange)}%`,
'Asegúrate de tener suficientes ingredientes',
'Promociona productos complementarios'
];
productImpacts = baseline.products.map(product => {
const change = product.name === targetProduct ? demandChange : demandChange * 0.3;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: product.demand + Math.round(change * product.demand / 100),
revenueImpact: Math.round((change * product.demand / 100) * product.price * (product.name === targetProduct ? (1 - discount/100) : 1))
};
});
break;
case 'weekend':
const isWeekend = scenario.parameters.dayType.value;
const goodWeather = scenario.parameters.weatherGood.value;
demandChange = (isWeekend === 'saturday' ? 25 : isWeekend === 'sunday' ? 15 : 35) + (goodWeather ? 10 : -5);
confidence = 'high';
recommendations = [
'Aumenta la producción de productos especiales',
'Prepara más variedad para familias',
'Considera abrir más temprano'
];
productImpacts = baseline.products.map(product => {
const multiplier = product.name === 'Tartas' ? 1.5 : product.name === 'Croissants' ? 1.3 : 1.0;
const change = demandChange * multiplier;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: product.demand + Math.round(change * product.demand / 100),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
case 'supply_shortage':
const ingredient = scenario.parameters.ingredient.value;
const shortage = scenario.parameters.shortage.value;
demandChange = shortage === 'mild' ? -10 : shortage === 'moderate' ? -25 : -40;
confidence = 'medium';
recommendations = [
'Busca proveedores alternativos inmediatamente',
'Promociona productos que no requieren este ingrediente',
'Informa a los clientes sobre productos no disponibles'
];
const affectedProducts = getAffectedProducts(ingredient);
productImpacts = baseline.products.map(product => {
const change = affectedProducts.includes(product.name) ? demandChange : 0;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: Math.max(0, product.demand + Math.round(change * product.demand / 100)),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
}
const totalRevenueImpact = productImpacts.reduce((sum, impact) => sum + impact.revenueImpact, 0);
return {
scenarioId: scenario.id,
demandChange,
revenueImpact: totalRevenueImpact,
productImpacts,
recommendations,
confidence
};
};
const getAffectedProducts = (ingredient: string): string[] => {
const ingredientMap: Record<string, string[]> = {
flour: ['Pan', 'Croissants', 'Magdalenas', 'Tartas'],
butter: ['Croissants', 'Tartas', 'Magdalenas'],
eggs: ['Magdalenas', 'Tartas'],
sugar: ['Magdalenas', 'Tartas'],
chocolate: ['Tartas']
};
return ingredientMap[ingredient] || [];
};
const updateParameter = (scenarioId: string, paramKey: string, value: any) => {
// This would update the scenario parameters in a real implementation
console.log('Updating parameter:', scenarioId, paramKey, value);
};
const resetScenario = () => {
setSelectedScenario(null);
setScenarioResult(null);
};
const selectedScenarioData = scenarios.find(s => s.id === selectedScenario);
return (
<div className={`bg-white rounded-xl shadow-soft p-6 ${className}`}>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Play className="h-5 w-5 mr-2 text-primary-600" />
Simulador de Escenarios
</h3>
<p className="text-sm text-gray-600 mt-1">
Simula diferentes situaciones y ve el impacto en tu negocio
</p>
</div>
{selectedScenario && (
<button
onClick={resetScenario}
className="flex items-center px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<RotateCcw className="h-4 w-4 mr-1" />
Reiniciar
</button>
)}
</div>
{!selectedScenario ? (
// Scenario Selection
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{scenarios.map(scenario => {
const IconComponent = scenario.icon;
return (
<button
key={scenario.id}
onClick={() => setSelectedScenario(scenario.id)}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-primary-300 hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-start">
<div className="flex-shrink-0 p-2 bg-primary-50 rounded-lg group-hover:bg-primary-100 transition-colors">
<IconComponent className="h-5 w-5 text-primary-600" />
</div>
<div className="ml-3 flex-1">
<h4 className="font-medium text-gray-900 group-hover:text-primary-700">
{scenario.name}
</h4>
<p className="text-sm text-gray-600 mt-1">
{scenario.description}
</p>
</div>
</div>
</button>
);
})}
</div>
) : (
// Selected Scenario Configuration
<div className="space-y-6">
{selectedScenarioData && (
<>
{/* Scenario Header */}
<div className="flex items-center p-4 bg-primary-50 rounded-lg">
<selectedScenarioData.icon className="h-6 w-6 text-primary-600 mr-3" />
<div>
<h4 className="font-medium text-primary-900">{selectedScenarioData.name}</h4>
<p className="text-sm text-primary-700 mt-1">{selectedScenarioData.description}</p>
</div>
</div>
{/* Parameters */}
<div>
<h5 className="text-sm font-medium text-gray-700 mb-3">Parámetros del Escenario:</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(selectedScenarioData.parameters).map(([key, param]) => (
<div key={key} className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
{param.label}
</label>
{param.type === 'select' ? (
<select
value={param.value}
onChange={(e) => updateParameter(selectedScenarioData.id, key, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
{param.options?.map(option => (
<option key={option} value={option}>
{option === 'light' ? 'Ligera' :
option === 'moderate' ? 'Moderada' :
option === 'heavy' ? 'Intensa' :
option === 'saturday' ? 'Sábado' :
option === 'sunday' ? 'Domingo' :
option === 'holiday' ? 'Festivo' :
option === 'mild' ? 'Leve' :
option === 'severe' ? 'Severa' :
option === 'flour' ? 'Harina' :
option === 'butter' ? 'Mantequilla' :
option === 'eggs' ? 'Huevos' :
option === 'sugar' ? 'Azúcar' :
option === 'chocolate' ? 'Chocolate' :
option}
</option>
))}
</select>
) : param.type === 'number' ? (
<div className="flex items-center">
<input
type="number"
value={param.value}
min={param.min}
max={param.max}
step={param.step}
onChange={(e) => updateParameter(selectedScenarioData.id, key, Number(e.target.value))}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
{param.unit && (
<span className="ml-2 text-sm text-gray-600">{param.unit}</span>
)}
</div>
) : param.type === 'boolean' ? (
<label className="flex items-center">
<input
type="checkbox"
checked={param.value}
onChange={(e) => updateParameter(selectedScenarioData.id, key, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-600">
{param.value ? 'Sí' : 'No'}
</span>
</label>
) : null}
</div>
))}
</div>
</div>
{/* Run Scenario Button */}
<button
onClick={() => runScenario(selectedScenarioData)}
disabled={isRunning}
className="w-full flex items-center justify-center px-4 py-3 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRunning ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Simulando...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Ejecutar Simulación
</>
)}
</button>
{/* Results */}
{scenarioResult && (
<div className="border-t border-gray-200 pt-6 space-y-6">
<div>
<h5 className="text-lg font-medium text-gray-900 mb-4">Resultados de la Simulación</h5>
{/* Overall Impact */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Cambio en Demanda</span>
<span className={`text-lg font-bold ${scenarioResult.demandChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{scenarioResult.demandChange >= 0 ? '+' : ''}{scenarioResult.demandChange}%
</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Impacto en Ingresos</span>
<span className={`text-lg font-bold flex items-center ${scenarioResult.revenueImpact >= 0 ? 'text-green-600' : 'text-red-600'}`}>
<Euro className="h-4 w-4 mr-1" />
{scenarioResult.revenueImpact >= 0 ? '+' : ''}{scenarioResult.revenueImpact}
</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Confianza</span>
<span className={`px-2 py-1 text-xs rounded-full ${
scenarioResult.confidence === 'high' ? 'bg-green-100 text-green-800' :
scenarioResult.confidence === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{scenarioResult.confidence === 'high' ? 'Alta' :
scenarioResult.confidence === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
</div>
</div>
{/* Product Impact */}
<div className="mb-6">
<h6 className="text-sm font-medium text-gray-700 mb-3">Impacto por Producto:</h6>
<div className="space-y-2">
{scenarioResult.productImpacts.map((impact, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center">
<span className="font-medium text-gray-900">{impact.name}</span>
<span className="ml-2 text-sm text-gray-600">
{baselineData.products.find(p => p.name === impact.name)?.demand} {impact.newDemand}
</span>
</div>
<div className="flex items-center space-x-3">
<span className={`text-sm font-medium ${impact.demandChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{impact.demandChange >= 0 ? '+' : ''}{impact.demandChange}
</span>
<span className={`text-sm font-medium flex items-center ${impact.revenueImpact >= 0 ? 'text-green-600' : 'text-red-600'}`}>
<Euro className="h-3 w-3 mr-1" />
{impact.revenueImpact >= 0 ? '+' : ''}{impact.revenueImpact}
</span>
</div>
</div>
))}
</div>
</div>
{/* Recommendations */}
<div>
<h6 className="text-sm font-medium text-gray-700 mb-3">Recomendaciones:</h6>
<ul className="space-y-2">
{scenarioResult.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<div className="flex-shrink-0 w-2 h-2 bg-primary-600 rounded-full mt-2 mr-3"></div>
<span className="text-sm text-gray-700">{rec}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)}
</>
)}
</div>
)}
</div>
);
};
export default WhatIfPlanner;

View File

@@ -229,14 +229,16 @@ export const useDashboard = () => {
// Load data on mount and when tenant changes
useEffect(() => {
loadDashboardData();
}, [loadDashboardData]);
if (tenantId) {
loadDashboardData(tenantId);
}
}, [loadDashboardData, tenantId]);
return {
...dashboardData,
isLoading: isLoading || dataLoading || forecastLoading,
error: error || dataError || forecastError,
reload: loadDashboardData,
reload: () => tenantId ? loadDashboardData(tenantId) : Promise.resolve(),
clearError: () => setError(null)
};
};

View File

@@ -0,0 +1,286 @@
// Real API hook for Order Suggestions using backend data
import { useState, useCallback, useEffect } from 'react';
import { useData, useForecast } from '../api';
import { useTenantId } from './useTenantId';
import type { DailyOrderItem, WeeklyOrderItem } from '../components/simple/OrderSuggestions';
// Product price mapping that could come from backend
const PRODUCT_PRICES: Record<string, number> = {
'Pan de Molde': 1.80,
'Baguettes': 2.80,
'Croissants': 2.50,
'Magdalenas': 2.40,
'Café en Grano': 17.50, // per kg
'Leche Entera': 0.95, // per liter
'Mantequilla': 4.20, // per kg
'Vasos de Café': 0.08, // per unit
'Servilletas': 0.125, // per pack
'Bolsas papel': 0.12, // per unit
};
// Suppliers mapping
const SUPPLIERS: Record<string, string> = {
'Pan de Molde': 'Panadería Central Madrid',
'Baguettes': 'Panadería Central Madrid',
'Croissants': 'Panadería Central Madrid',
'Magdalenas': 'Panadería Central Madrid',
'Café en Grano': 'Cafés Premium',
'Leche Entera': 'Lácteos Frescos SA',
'Mantequilla': 'Lácteos Frescos SA',
'Vasos de Café': 'Suministros Hostelería',
'Servilletas': 'Suministros Hostelería',
'Bolsas papel': 'Distribuciones Madrid',
};
export const useOrderSuggestions = () => {
const [dailyOrders, setDailyOrders] = useState<DailyOrderItem[]>([]);
const [weeklyOrders, setWeeklyOrders] = useState<WeeklyOrderItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { tenantId } = useTenantId();
const {
getProductsList,
getSalesAnalytics,
getDashboardStats,
getCurrentWeather
} = useData();
const {
createSingleForecast,
getQuickForecasts,
getForecastAlerts
} = useForecast();
// Generate daily order suggestions based on real forecast data
const generateDailyOrderSuggestions = useCallback(async (): Promise<DailyOrderItem[]> => {
if (!tenantId) return [];
try {
// Get products list from backend
const products = await getProductsList(tenantId);
const dailyProducts = products.filter(p =>
['Pan de Molde', 'Baguettes', 'Croissants', 'Magdalenas'].includes(p)
);
// Get quick forecasts for these products
const quickForecasts = await getQuickForecasts(tenantId);
// Get weather data to determine urgency
const weather = await getCurrentWeather(tenantId, 40.4168, -3.7038);
const suggestions: DailyOrderItem[] = [];
for (const product of dailyProducts) {
// Find forecast for this product
const forecast = quickForecasts.find(f => f.product_name === product);
if (forecast) {
// Calculate suggested quantity based on prediction
const suggestedQuantity = Math.max(forecast.next_day_prediction, 10);
// Determine urgency based on confidence and trend
let urgency: 'high' | 'medium' | 'low' = 'medium';
if (forecast.confidence_score > 0.9 && forecast.trend_direction === 'up') {
urgency = 'high';
} else if (forecast.confidence_score < 0.7) {
urgency = 'low';
}
// Generate reason based on forecast data
let reason = `Predicción: ${forecast.next_day_prediction} unidades`;
if (forecast.trend_direction === 'up') {
reason += ' (tendencia al alza)';
}
if (weather && weather.precipitation > 0) {
reason += ', lluvia prevista';
urgency = urgency === 'low' ? 'medium' : 'high';
}
const orderItem: DailyOrderItem = {
id: `daily-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity: Math.round(suggestedQuantity),
currentQuantity: Math.round(suggestedQuantity * 0.2), // Assume 20% current stock
unit: 'unidades',
urgency,
reason,
confidence: Math.round(forecast.confidence_score * 100),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 2.5) * 100) / 100,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
};
suggestions.push(orderItem);
}
}
return suggestions;
} catch (error) {
console.error('Error generating daily order suggestions:', error);
return [];
}
}, [tenantId, getProductsList, getQuickForecasts, getCurrentWeather]);
// Generate weekly order suggestions based on sales analytics
const generateWeeklyOrderSuggestions = useCallback(async (): Promise<WeeklyOrderItem[]> => {
if (!tenantId) return [];
try {
// Get sales analytics for the past month
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const analytics = await getSalesAnalytics(tenantId, startDate, endDate);
// Weekly products (ingredients and supplies)
const weeklyProducts = [
'Café en Grano',
'Leche Entera',
'Mantequilla',
'Vasos de Café',
'Servilletas',
'Bolsas papel'
];
const suggestions: WeeklyOrderItem[] = [];
for (const product of weeklyProducts) {
// Calculate weekly consumption based on analytics
const weeklyConsumption = calculateWeeklyConsumption(product, analytics);
const currentStock = Math.round(weeklyConsumption * (0.3 + Math.random() * 0.4)); // Random stock between 30-70% of weekly need
const stockDays = Math.max(1, Math.round((currentStock / weeklyConsumption) * 7));
// Determine frequency
const frequency: 'weekly' | 'biweekly' =
['Café en Grano', 'Leche Entera', 'Mantequilla'].includes(product) ? 'weekly' : 'biweekly';
const suggestedQuantity = frequency === 'weekly' ?
Math.round(weeklyConsumption * 1.1) : // 10% buffer for weekly
Math.round(weeklyConsumption * 2.2); // 2 weeks + 10% buffer
const orderItem: WeeklyOrderItem = {
id: `weekly-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity,
currentStock,
unit: getProductUnit(product),
frequency,
nextOrderDate: getNextOrderDate(frequency, stockDays),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 1.0) * 100) / 100,
stockDays,
confidence: stockDays <= 2 ? 95 : stockDays <= 5 ? 85 : 75
};
suggestions.push(orderItem);
}
return suggestions.sort((a, b) => a.stockDays - b.stockDays); // Sort by urgency
} catch (error) {
console.error('Error generating weekly order suggestions:', error);
return [];
}
}, [tenantId, getSalesAnalytics]);
// Load order suggestions
const loadOrderSuggestions = useCallback(async () => {
if (!tenantId) return;
setIsLoading(true);
setError(null);
try {
const [daily, weekly] = await Promise.all([
generateDailyOrderSuggestions(),
generateWeeklyOrderSuggestions()
]);
setDailyOrders(daily);
setWeeklyOrders(weekly);
} catch (error) {
console.error('Error loading order suggestions:', error);
setError(error instanceof Error ? error.message : 'Failed to load order suggestions');
} finally {
setIsLoading(false);
}
}, [tenantId, generateDailyOrderSuggestions, generateWeeklyOrderSuggestions]);
// Load on mount and when tenant changes
useEffect(() => {
loadOrderSuggestions();
}, [loadOrderSuggestions]);
return {
dailyOrders,
weeklyOrders,
isLoading,
error,
reload: loadOrderSuggestions,
clearError: () => setError(null),
};
};
// Helper functions
function getProductEmoji(product: string): string {
const emojiMap: Record<string, string> = {
'Pan de Molde': '🍞',
'Baguettes': '🥖',
'Croissants': '🥐',
'Magdalenas': '🧁',
'Café en Grano': '☕',
'Leche Entera': '🥛',
'Mantequilla': '🧈',
'Vasos de Café': '🥤',
'Servilletas': '🧻',
'Bolsas papel': '🛍️'
};
return emojiMap[product] || '📦';
}
function getProductUnit(product: string): string {
const unitMap: Record<string, string> = {
'Pan de Molde': 'unidades',
'Baguettes': 'unidades',
'Croissants': 'unidades',
'Magdalenas': 'unidades',
'Café en Grano': 'kg',
'Leche Entera': 'litros',
'Mantequilla': 'kg',
'Vasos de Café': 'unidades',
'Servilletas': 'paquetes',
'Bolsas papel': 'unidades'
};
return unitMap[product] || 'unidades';
}
function calculateWeeklyConsumption(product: string, analytics: any): number {
// This would ideally come from sales analytics
// For now, use realistic estimates based on product type
const weeklyEstimates: Record<string, number> = {
'Café en Grano': 5, // 5kg per week
'Leche Entera': 25, // 25L per week
'Mantequilla': 3, // 3kg per week
'Vasos de Café': 500, // 500 cups per week
'Servilletas': 10, // 10 packs per week
'Bolsas papel': 200 // 200 bags per week
};
return weeklyEstimates[product] || 10;
}
function getNextOrderDate(frequency: 'weekly' | 'biweekly', stockDays: number): string {
const today = new Date();
let daysToAdd = frequency === 'weekly' ? 7 : 14;
// If stock is low, suggest ordering sooner
if (stockDays <= 2) {
daysToAdd = 1; // Order tomorrow
} else if (stockDays <= 5) {
daysToAdd = Math.min(daysToAdd, 3); // Order within 3 days
}
const nextDate = new Date(today.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
return nextDate.toISOString().split('T')[0];
}

View File

@@ -0,0 +1,162 @@
// Real API hook for Critical Alerts using backend forecast alerts
import { useState, useCallback, useEffect } from 'react';
import { useForecast } from '../api';
import { useTenantId } from './useTenantId';
export interface RealAlert {
id: string;
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
action?: string;
time: string;
}
export const useRealAlerts = () => {
const [alerts, setAlerts] = useState<RealAlert[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { tenantId } = useTenantId();
const { getForecastAlerts, acknowledgeForecastAlert } = useForecast();
// Transform backend forecast alerts to frontend alert format
const transformForecastAlert = (alert: any): RealAlert => {
// Map alert types
let type: RealAlert['type'] = 'system';
if (alert.alert_type?.includes('stock') || alert.alert_type?.includes('demand')) {
type = 'stock';
} else if (alert.alert_type?.includes('weather')) {
type = 'weather';
} else if (alert.alert_type?.includes('production')) {
type = 'production';
}
// Map severity
let severity: RealAlert['severity'] = 'medium';
if (alert.severity === 'critical' || alert.severity === 'high') {
severity = 'high';
} else if (alert.severity === 'low') {
severity = 'low';
}
// Generate user-friendly title and description
let title = alert.message;
let description = alert.message;
if (alert.alert_type?.includes('high_demand')) {
title = 'Alta Demanda Prevista';
description = `Se prevé alta demanda. ${alert.message}`;
} else if (alert.alert_type?.includes('low_confidence')) {
title = 'Predicción Incierta';
description = `Baja confianza en predicción. ${alert.message}`;
} else if (alert.alert_type?.includes('stock_risk')) {
title = 'Riesgo de Desabastecimiento';
description = `Posible falta de stock. ${alert.message}`;
}
return {
id: alert.id,
type,
severity,
title,
description,
action: 'Ver detalles',
time: new Date(alert.created_at).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})
};
};
// Load real alerts from backend
const loadAlerts = useCallback(async () => {
if (!tenantId) return;
setIsLoading(true);
setError(null);
try {
// Get forecast alerts from backend
const forecastAlerts = await getForecastAlerts(tenantId);
// Filter only active alerts
const activeAlerts = forecastAlerts.filter(alert => alert.is_active);
// Transform to frontend format
const transformedAlerts = activeAlerts.map(transformForecastAlert);
// Sort by severity and time (most recent first)
transformedAlerts.sort((a, b) => {
// First by severity (high > medium > low)
const severityOrder = { high: 3, medium: 2, low: 1 };
const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
if (severityDiff !== 0) return severityDiff;
// Then by time (most recent first)
return b.time.localeCompare(a.time);
});
setAlerts(transformedAlerts.slice(0, 3)); // Show max 3 alerts in dashboard
} catch (error) {
console.error('Error loading alerts:', error);
setError(error instanceof Error ? error.message : 'Failed to load alerts');
// Fallback to sample alerts based on common scenarios
setAlerts(generateFallbackAlerts());
} finally {
setIsLoading(false);
}
}, [tenantId, getForecastAlerts]);
// Handle alert acknowledgment
const handleAlertAction = useCallback(async (alertId: string) => {
if (!tenantId) return;
try {
await acknowledgeForecastAlert(tenantId, alertId);
// Remove acknowledged alert from local state
setAlerts(prev => prev.filter(alert => alert.id !== alertId));
} catch (error) {
console.error('Error acknowledging alert:', error);
}
}, [tenantId, acknowledgeForecastAlert]);
// Generate fallback alerts when API fails
const generateFallbackAlerts = (): RealAlert[] => {
const now = new Date();
const timeString = now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
return [
{
id: 'fallback-1',
type: 'stock',
severity: 'high',
title: 'Stock Bajo de Croissants',
description: 'Se prevé alta demanda este fin de semana',
action: 'Aumentar producción',
time: timeString
}
];
};
// Load alerts on mount and when tenant changes
useEffect(() => {
loadAlerts();
// Refresh alerts every 5 minutes
const interval = setInterval(loadAlerts, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [loadAlerts]);
return {
alerts,
isLoading,
error,
onAlertAction: handleAlertAction,
reload: loadAlerts,
clearError: () => setError(null),
};
};

View File

@@ -1,360 +1,242 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, Package, AlertTriangle, Cloud, Users } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import React from 'react';
import { useDashboard } from '../../hooks/useDashboard';
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
import { useRealAlerts } from '../../hooks/useRealAlerts';
// Import simplified components
import TodayRevenue from '../../components/simple/TodayRevenue';
import CriticalAlerts from '../../components/simple/CriticalAlerts';
import TodayProduction from '../../components/simple/TodayProduction';
import QuickActions from '../../components/simple/QuickActions';
import QuickOverview from '../../components/simple/QuickOverview';
import OrderSuggestions from '../../components/simple/OrderSuggestions';
// Helper functions
const getConfidenceColor = (confidence: 'high' | 'medium' | 'low') => {
switch (confidence) {
case 'high':
return 'bg-success-100 text-success-800';
case 'medium':
return 'bg-warning-100 text-warning-800';
case 'low':
return 'bg-danger-100 text-danger-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
interface DashboardPageProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
onNavigateToProduction?: () => void;
}
const getConfidenceLabel = (confidence: 'high' | 'medium' | 'low') => {
switch (confidence) {
case 'high':
return 'Alta';
case 'medium':
return 'Media';
case 'low':
return 'Baja';
default:
return 'Media';
}
};
const DashboardPage = () => {
const DashboardPage: React.FC<DashboardPageProps> = ({
onNavigateToOrders,
onNavigateToReports,
onNavigateToProduction
}) => {
const {
weather,
todayForecasts,
metrics,
products,
isLoading,
error,
reload
reload,
todayForecasts,
metrics
} = useDashboard();
if (isLoading) {
return <div>Loading dashboard...</div>;
}
// Use real API data for order suggestions
const {
dailyOrders: realDailyOrders,
weeklyOrders: realWeeklyOrders,
isLoading: ordersLoading
} = useOrderSuggestions();
if (error) {
// Use real API data for alerts
const {
alerts: realAlerts,
onAlertAction
} = useRealAlerts();
// Transform forecast data for production component
const mockProduction = todayForecasts.map((forecast, index) => ({
id: `prod-${index}`,
product: forecast.product,
emoji: forecast.product.toLowerCase().includes('croissant') ? '🥐' :
forecast.product.toLowerCase().includes('pan') ? '🍞' :
forecast.product.toLowerCase().includes('magdalena') ? '🧁' : '🥖',
quantity: forecast.predicted,
status: 'pending' as const,
scheduledTime: index < 3 ? '06:00' : '14:00',
confidence: forecast.confidence === 'high' ? 0.9 :
forecast.confidence === 'medium' ? 0.7 : 0.5
}));
// 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';
};
if (isLoading) {
return (
<div>
<p>Error: {error}</p>
<button onClick={reload}>Retry</button>
<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 datos de tu panadería...</p>
</div>
</div>
);
}
// Sample historical data for charts (you can move this to the hook later)
const salesHistory = [
{ date: '2024-10-28', ventas: 145, prediccion: 140 },
{ date: '2024-10-29', ventas: 128, prediccion: 135 },
{ date: '2024-10-30', ventas: 167, prediccion: 160 },
{ date: '2024-10-31', ventas: 143, prediccion: 145 },
{ date: '2024-11-01', ventas: 156, prediccion: 150 },
{ date: '2024-11-02', ventas: 189, prediccion: 185 },
{ date: '2024-11-03', ventas: 134, prediccion: 130 },
];
const topProducts = [
{ name: 'Croissants', quantity: 45, trend: 'up' },
{ name: 'Pan de molde', quantity: 32, trend: 'up' },
{ name: 'Baguettes', quantity: 28, trend: 'down' },
{ name: 'Napolitanas', quantity: 23, trend: 'up' },
{ name: 'Café', quantity: 67, trend: 'up' },
];
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h3 className="text-red-800 font-medium">Error al cargar datos</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>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
{/* ¡Hola, {user.fullName?.split(' ')[0] || 'Usuario'}! 👋 */}
Hola
</h1>
<p className="text-gray-600 mt-1">
Aquí tienes un resumen de tu panadería para hoy
</p>
<div className="p-4 md:p-6 space-y-6 bg-gray-50 min-h-screen">
{/* Welcome Header */}
<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>
<h1 className="text-2xl font-bold text-gray-900">
{getGreeting()}! 👋
</h1>
<p className="text-gray-600 mt-1">
{new Date().toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</p>
</div>
<div className="mt-4 sm:mt-0 flex items-center space-x-4">
{weather && (
<div className="flex items-center text-sm text-gray-600 bg-gray-50 rounded-lg px-4 py-2">
<span className="text-lg mr-2">
{weather.precipitation > 0 ? '🌧️' : weather.temperature > 20 ? '☀️' : '⛅'}
</span>
<span>{weather.temperature}°C</span>
</div>
)}
<div className="text-right">
<div className="text-sm font-medium text-gray-900">Estado del sistema</div>
<div className="text-xs text-green-600 flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
Operativo
</div>
</div>
</div>
</div>
</div>
{/* Critical Section - Always Visible */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Revenue - Most Important */}
<TodayRevenue
currentRevenue={metrics?.totalSales || 287.50}
previousRevenue={256.25}
dailyTarget={350}
/>
{weather && (
<div className="mt-4 sm:mt-0 flex items-center text-sm text-gray-600 bg-white rounded-lg px-4 py-2 shadow-soft">
<Cloud className="h-4 w-4 mr-2" />
<span>{weather.temperature}°C - {weather.description}</span>
</div>
)}
{/* Alerts - Real API Data */}
<CriticalAlerts
alerts={realAlerts}
onAlertClick={onAlertAction}
/>
{/* Quick Actions - Easy Access */}
<QuickActions
onActionClick={(actionId) => {
console.log('Action clicked:', actionId);
// Handle quick actions
switch (actionId) {
case 'view_orders':
onNavigateToOrders?.();
break;
case 'view_sales':
onNavigateToReports?.();
break;
default:
// Handle other actions
break;
}
}}
/>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Ventas de Hoy</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.totalSales ?? 0}</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
+12% vs ayer
</p>
</div>
</div>
</div>
{/* Order Suggestions - Real AI-Powered Recommendations */}
<OrderSuggestions
dailyOrders={realDailyOrders}
weeklyOrders={realWeeklyOrders}
onUpdateQuantity={(orderId, quantity, type) => {
console.log('Update order quantity:', orderId, quantity, type);
// In real implementation, this would update the backend
}}
onCreateOrder={(items, type) => {
console.log('Create order:', type, items);
// Navigate to orders page to complete the order
onNavigateToOrders?.();
}}
onViewDetails={() => {
onNavigateToOrders?.();
}}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-success-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Reducción Desperdicio</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.wasteReduction ?? 0}%</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
Mejorando
</p>
</div>
</div>
</div>
{/* Production Section - Core Operations */}
<TodayProduction
items={mockProduction}
onUpdateQuantity={(itemId: string, quantity: number) => {
console.log('Update quantity:', itemId, quantity);
}}
onUpdateStatus={(itemId: string, status: any) => {
console.log('Update status:', itemId, status);
}}
onViewDetails={() => {
onNavigateToProduction?.();
}}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Users className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Precisión IA</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.accuracy ?? 0}%</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
Excelente
</p>
</div>
</div>
</div>
{/* Quick Overview - Supporting Information */}
<QuickOverview
onNavigateToOrders={onNavigateToOrders}
onNavigateToReports={onNavigateToReports}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-warning-100 rounded-lg">
<AlertTriangle className="h-6 w-6 text-warning-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Roturas Stock</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.stockouts ?? 0}</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingDown className="h-3 w-3 mr-1" />
Reduciendo
</p>
</div>
</div>
</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sales Chart */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Ventas vs Predicciones (Última Semana)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={salesHistory}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
stroke="#666"
fontSize={12}
tickFormatter={(value) => {
const date = new Date(value);
return `${date.getDate()}/${date.getMonth() + 1}`;
}}
/>
<YAxis stroke="#666" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
labelFormatter={(value) => {
const date = new Date(value);
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
}}
/>
<Line
type="monotone"
dataKey="ventas"
stroke="#f97316"
strokeWidth={3}
name="Ventas Reales"
dot={{ fill: '#f97316', strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="prediccion"
stroke="#64748b"
strokeWidth={2}
strokeDasharray="5 5"
name="Predicción IA"
dot={{ fill: '#64748b', strokeWidth: 2, r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Today's Forecasts */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Predicciones para Hoy
</h3>
<div className="space-y-4">
{todayForecasts.map((forecast, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-900">{forecast.product}</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${getConfidenceColor(forecast.confidence)}`}>
{getConfidenceLabel(forecast.confidence)}
</span>
</div>
<div className="flex items-center mt-1">
<span className="text-xl font-bold text-gray-900 mr-2">
{forecast.predicted}
</span>
<span className={`text-sm flex items-center ${
forecast.change >= 0 ? 'text-success-600' : 'text-danger-600'
}`}>
{forecast.change >= 0 ? (
<TrendingUp className="h-3 w-3 mr-1" />
) : (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{Math.abs(forecast.change)} vs ayer
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bottom Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Productos Más Vendidos (Esta Semana)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={topProducts}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="name"
stroke="#666"
fontSize={12}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis stroke="#666" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
/>
<Bar
dataKey="quantity"
fill="#f97316"
radius={[4, 4, 0, 0]}
name="Cantidad Vendida"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Acciones Rápidas
</h3>
<div className="space-y-3">
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg mr-3">
<TrendingUp className="h-5 w-5 text-primary-600" />
</div>
<div>
<div className="font-medium text-gray-900">Ver Predicciones Detalladas</div>
<div className="text-sm text-gray-500">Analiza las predicciones completas</div>
</div>
</div>
</button>
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg mr-3">
<Package className="h-5 w-5 text-success-600" />
</div>
<div>
<div className="font-medium text-gray-900">Gestionar Pedidos</div>
<div className="text-sm text-gray-500">Revisa y ajusta tus pedidos</div>
</div>
</div>
</button>
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg mr-3">
<Users className="h-5 w-5 text-blue-600" />
</div>
<div>
<div className="font-medium text-gray-900">Configurar Alertas</div>
<div className="text-sm text-gray-500">Personaliza tus notificaciones</div>
</div>
</div>
</button>
</div>
</div>
</div>
{/* Weather Impact Alert */}
{/* Weather Impact Alert - Context Aware */}
{weather && weather.precipitation > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start">
<Cloud className="h-5 w-5 text-blue-600 mt-0.5 mr-3" />
<span className="text-2xl mr-3">🌧</span>
<div>
<h4 className="font-medium text-blue-900">Impacto del Clima</h4>
<h4 className="font-medium text-blue-900">Impacto del Clima Detectado</h4>
<p className="text-blue-800 text-sm mt-1">
Se esperan precipitaciones hoy. Esto puede reducir el tráfico peatonal en un 20-30%.
Considera ajustar la producción de productos frescos.
Se esperan precipitaciones ({weather.precipitation}mm). Las predicciones se han ajustado
automáticamente considerando una reducción del 15% en el tráfico.
</p>
<div className="mt-2 flex items-center text-xs text-blue-700">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-2"></div>
Producción y pedidos ya optimizados
</div>
</div>
</div>
</div>
)}
{/* Success Message - When Everything is Good */}
{realAlerts.length === 0 && (
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
<div className="text-4xl mb-2">🎉</div>
<h4 className="font-medium text-green-900">¡Todo bajo control!</h4>
<p className="text-green-700 text-sm mt-1">
No hay alertas activas. Tu panadería está funcionando perfectamente.
</p>
</div>
)}
</div>
);
};

View File

@@ -1,5 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock } from 'lucide-react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock, BarChart3, TrendingUp, Euro, Settings } from 'lucide-react';
// Import complex components
import WhatIfPlanner from '../../components/ui/WhatIfPlanner';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
interface Order {
id: string;
@@ -24,7 +28,7 @@ const OrdersPage: React.FC = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showNewOrder, setShowNewOrder] = useState(false);
const [activeTab, setActiveTab] = useState<'all' | 'pending' | 'delivered'>('all');
const [activeTab, setActiveTab] = useState<'orders' | 'analytics' | 'forecasting' | 'suppliers'>('orders');
// Sample orders data
const sampleOrders: Order[] = [
@@ -135,11 +139,47 @@ const OrdersPage: React.FC = () => {
}
};
// Sample data for complex components
const orderDemandHeatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Harina de trigo', demand: 50, confidence: 'high' as const },
{ name: 'Levadura fresca', demand: 2, confidence: 'high' as const },
{ name: 'Mantequilla', demand: 5, confidence: 'medium' as const },
{ name: 'Vasos café', demand: 1000, confidence: 'medium' as const },
]
},
{ date: '2024-11-05', demand: 165, isForecast: true },
{ date: '2024-11-06', demand: 195, isForecast: true },
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const baselineSupplyData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Harina de trigo', demand: 50, price: 0.85 },
{ name: 'Levadura fresca', demand: 2, price: 3.20 },
{ name: 'Mantequilla', demand: 5, price: 4.20 },
{ name: 'Leche entera', demand: 20, price: 0.95 },
{ name: 'Vasos café', demand: 1000, price: 0.08 },
]
};
const filteredOrders = orders.filter(order => {
if (activeTab === 'all') return true;
if (activeTab === 'pending') return order.status === 'pending' || order.status === 'confirmed';
if (activeTab === 'delivered') return order.status === 'delivered';
return true;
if (activeTab === 'orders') return true;
return false;
});
const handleDeleteOrder = (orderId: string) => {
@@ -181,24 +221,31 @@ const OrdersPage: React.FC = () => {
</button>
</div>
{/* Tabs */}
{/* Enhanced Tabs */}
<div className="bg-white rounded-xl shadow-soft p-1">
<div className="flex space-x-1">
{[
{ id: 'all', label: 'Todos', count: orders.length },
{ id: 'pending', label: 'Pendientes', count: orders.filter(o => o.status === 'pending' || o.status === 'confirmed').length },
{ id: 'delivered', label: 'Entregados', count: orders.filter(o => o.status === 'delivered').length }
{ id: 'orders', label: 'Gestión de Pedidos', icon: Package, count: orders.length },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'forecasting', label: 'Simulaciones', icon: TrendingUp },
{ id: 'suppliers', label: 'Proveedores', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-lg transition-all ${
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
{tab.label} ({tab.count})
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
{tab.count && (
<span className="ml-2 px-2 py-1 bg-gray-200 text-gray-700 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
@@ -224,8 +271,11 @@ const OrdersPage: React.FC = () => {
</div>
</div>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Tab Content */}
{activeTab === 'orders' && (
<>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredOrders.map((order) => (
<div key={order.id} className="bg-white rounded-xl shadow-soft hover:shadow-medium transition-shadow">
{/* Order Header */}
@@ -390,6 +440,148 @@ const OrdersPage: React.FC = () => {
</button>
</div>
</div>
</>
)}
{/* Analytics Tab */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={orderDemandHeatmapData}
selectedProduct="Ingredientes"
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Cost Analysis Chart */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Euro className="h-5 w-5 mr-2 text-primary-600" />
Análisis de Costos
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800 font-semibold">Ahorro Mensual</div>
<div className="text-2xl font-bold text-green-900">124.50</div>
<div className="text-sm text-green-700">vs mes anterior</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-blue-800 font-semibold">Gasto Promedio</div>
<div className="text-2xl font-bold text-blue-900">289.95</div>
<div className="text-sm text-blue-700">por pedido</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-purple-800 font-semibold">Eficiencia</div>
<div className="text-2xl font-bold text-purple-900">94.2%</div>
<div className="text-sm text-purple-700">predicción IA</div>
</div>
</div>
<div className="mt-6 h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de costos</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{/* Forecasting/Simulations Tab */}
{activeTab === 'forecasting' && (
<div className="space-y-6">
<WhatIfPlanner
baselineData={baselineSupplyData}
onScenarioRun={(scenario, result) => {
console.log('Scenario run:', scenario, result);
}}
/>
</div>
)}
{/* Suppliers Tab */}
{activeTab === 'suppliers' && (
<div className="space-y-6">
{/* Suppliers Management */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Settings className="h-5 w-5 mr-2 text-primary-600" />
Gestión de Proveedores
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
name: 'Harinas Castellana',
category: 'Ingredientes',
rating: 4.8,
reliability: 98,
nextDelivery: '2024-11-05',
status: 'active'
},
{
name: 'Distribuciones Madrid',
category: 'Consumibles',
rating: 4.5,
reliability: 95,
nextDelivery: '2024-11-04',
status: 'active'
},
{
name: 'Lácteos Frescos SA',
category: 'Ingredientes',
rating: 4.9,
reliability: 99,
nextDelivery: '2024-11-03',
status: 'active'
}
].map((supplier, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{supplier.name}</h4>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
</span>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Categoría:</span> {supplier.category}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Calificación:</span> {supplier.rating}/5
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Confiabilidad:</span> {supplier.reliability}%
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Próxima entrega:</span> {new Date(supplier.nextDelivery).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-4 flex space-x-2">
<button className="flex-1 px-3 py-2 text-sm bg-primary-100 text-primary-700 rounded-lg hover:bg-primary-200 transition-colors">
Editar
</button>
<button className="px-3 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
Contactar
</button>
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors">
<Plus className="h-4 w-4 mr-2" />
Añadir Proveedor
</button>
</div>
</div>
</div>
)}
{/* New Order Modal Placeholder */}
{showNewOrder && (

View File

@@ -0,0 +1,671 @@
import React, { useState, useEffect } from 'react';
import {
Clock, Calendar, ChefHat, TrendingUp, AlertTriangle,
CheckCircle, Settings, Plus, BarChart3, Users,
Timer, Target, Activity, Zap
} from 'lucide-react';
// Import existing complex components
import ProductionSchedule from '../../components/ui/ProductionSchedule';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
import { useDashboard } from '../../hooks/useDashboard';
// Types for production management
interface ProductionMetrics {
efficiency: number;
onTimeCompletion: number;
wastePercentage: number;
energyUsage: number;
staffUtilization: number;
}
interface ProductionBatch {
id: string;
product: string;
batchSize: number;
startTime: string;
endTime: string;
status: 'planned' | 'in_progress' | 'completed' | 'delayed';
assignedStaff: string[];
actualYield: number;
expectedYield: number;
notes?: string;
temperature?: number;
humidity?: number;
}
interface StaffMember {
id: string;
name: string;
role: 'baker' | 'assistant' | 'decorator';
currentTask?: string;
status: 'available' | 'busy' | 'break';
efficiency: number;
}
interface Equipment {
id: string;
name: string;
type: 'oven' | 'mixer' | 'proofer' | 'cooling_rack';
status: 'idle' | 'in_use' | 'maintenance' | 'error';
currentBatch?: string;
temperature?: number;
maintenanceDue?: string;
}
const ProductionPage: React.FC = () => {
const { todayForecasts, metrics, weather, isLoading } = useDashboard();
const [activeTab, setActiveTab] = useState<'schedule' | 'batches' | 'analytics' | 'staff' | 'equipment'>('schedule');
const [productionMetrics, setProductionMetrics] = useState<ProductionMetrics>({
efficiency: 87.5,
onTimeCompletion: 94.2,
wastePercentage: 3.8,
energyUsage: 156.7,
staffUtilization: 78.3
});
// Sample production schedule data
const [productionSchedule, setProductionSchedule] = useState([
{
time: '05:00 AM',
items: [
{
id: 'prod-1',
product: 'Croissants',
quantity: 48,
priority: 'high' as const,
estimatedTime: 180,
status: 'in_progress' as const,
confidence: 0.92,
notes: 'Alta demanda prevista - lote doble'
},
{
id: 'prod-2',
product: 'Pan de molde',
quantity: 35,
priority: 'high' as const,
estimatedTime: 240,
status: 'pending' as const,
confidence: 0.88
}
],
totalTime: 420
},
{
time: '08:00 AM',
items: [
{
id: 'prod-3',
product: 'Baguettes',
quantity: 25,
priority: 'medium' as const,
estimatedTime: 200,
status: 'pending' as const,
confidence: 0.75
},
{
id: 'prod-4',
product: 'Magdalenas',
quantity: 60,
priority: 'medium' as const,
estimatedTime: 120,
status: 'pending' as const,
confidence: 0.82
}
],
totalTime: 320
}
]);
const [productionBatches, setProductionBatches] = useState<ProductionBatch[]>([
{
id: 'batch-1',
product: 'Croissants',
batchSize: 48,
startTime: '05:00',
endTime: '08:00',
status: 'in_progress',
assignedStaff: ['maria-lopez', 'carlos-ruiz'],
actualYield: 45,
expectedYield: 48,
temperature: 180,
humidity: 65,
notes: 'Masa fermentando correctamente'
},
{
id: 'batch-2',
product: 'Pan de molde',
batchSize: 35,
startTime: '06:30',
endTime: '10:30',
status: 'planned',
assignedStaff: ['ana-garcia'],
actualYield: 0,
expectedYield: 35,
notes: 'Esperando finalización de croissants'
}
]);
const [staff, setStaff] = useState<StaffMember[]>([
{
id: 'maria-lopez',
name: 'María López',
role: 'baker',
currentTask: 'Preparando croissants',
status: 'busy',
efficiency: 94.2
},
{
id: 'carlos-ruiz',
name: 'Carlos Ruiz',
role: 'assistant',
currentTask: 'Horneando croissants',
status: 'busy',
efficiency: 87.8
},
{
id: 'ana-garcia',
name: 'Ana García',
role: 'baker',
status: 'available',
efficiency: 91.5
}
]);
const [equipment, setEquipment] = useState<Equipment[]>([
{
id: 'oven-1',
name: 'Horno Principal',
type: 'oven',
status: 'in_use',
currentBatch: 'batch-1',
temperature: 180,
maintenanceDue: '2024-11-15'
},
{
id: 'mixer-1',
name: 'Amasadora Industrial',
type: 'mixer',
status: 'idle',
maintenanceDue: '2024-11-20'
},
{
id: 'proofer-1',
name: 'Fermentadora',
type: 'proofer',
status: 'in_use',
currentBatch: 'batch-2',
temperature: 28,
maintenanceDue: '2024-12-01'
}
]);
// Demand heatmap sample data
const heatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Croissants', demand: 48, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 35, confidence: 'high' as const },
{ name: 'Baguettes', demand: 25, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 32, confidence: 'medium' as const },
]
},
{
date: '2024-11-05',
demand: 165,
isForecast: true,
products: [
{ name: 'Croissants', demand: 42, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 38, confidence: 'medium' as const },
{ name: 'Baguettes', demand: 28, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 28, confidence: 'low' as const },
]
},
{
date: '2024-11-06',
demand: 195,
isForecast: true,
products: [
{ name: 'Croissants', demand: 55, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 40, confidence: 'high' as const },
{ name: 'Baguettes', demand: 32, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 35, confidence: 'medium' as const },
]
},
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const getStatusColor = (status: string) => {
switch (status) {
case 'planned':
return 'bg-blue-100 text-blue-800';
case 'in_progress':
return 'bg-yellow-100 text-yellow-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'delayed':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getEquipmentStatusColor = (status: Equipment['status']) => {
switch (status) {
case 'idle':
return 'bg-gray-100 text-gray-800';
case 'in_use':
return 'bg-green-100 text-green-800';
case 'maintenance':
return 'bg-yellow-100 text-yellow-800';
case 'error':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6 bg-gray-50 min-h-screen">
{/* 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>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<ChefHat className="h-8 w-8 mr-3 text-primary-600" />
Centro de Producción
</h1>
<p className="text-gray-600 mt-1">
Gestión completa de la producción diaria y planificación inteligente
</p>
</div>
<div className="mt-4 lg:mt-0 flex items-center space-x-4">
<div className="bg-gray-50 rounded-lg px-4 py-2">
<div className="text-sm font-medium text-gray-900">Eficiencia Hoy</div>
<div className="text-2xl font-bold text-primary-600">{productionMetrics.efficiency}%</div>
</div>
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors">
<Plus className="h-5 w-5 mr-2" />
Nuevo Lote
</button>
</div>
</div>
</div>
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Eficiencia</p>
<p className="text-2xl font-bold text-green-600">{productionMetrics.efficiency}%</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<Target className="h-6 w-6 text-green-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-green-600">
<TrendingUp className="h-3 w-3 mr-1" />
+2.3% vs ayer
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">A Tiempo</p>
<p className="text-2xl font-bold text-blue-600">{productionMetrics.onTimeCompletion}%</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<Clock className="h-6 w-6 text-blue-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-blue-600">
<CheckCircle className="h-3 w-3 mr-1" />
Muy bueno
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Desperdicio</p>
<p className="text-2xl font-bold text-orange-600">{productionMetrics.wastePercentage}%</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<AlertTriangle className="h-6 w-6 text-orange-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-orange-600">
<TrendingUp className="h-3 w-3 mr-1" />
-0.5% vs ayer
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Energía</p>
<p className="text-2xl font-bold text-purple-600">{productionMetrics.energyUsage} kW</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<Zap className="h-6 w-6 text-purple-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-purple-600">
<Activity className="h-3 w-3 mr-1" />
Normal
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Personal</p>
<p className="text-2xl font-bold text-indigo-600">{productionMetrics.staffUtilization}%</p>
</div>
<div className="p-3 bg-indigo-100 rounded-lg">
<Users className="h-6 w-6 text-indigo-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-indigo-600">
<Users className="h-3 w-3 mr-1" />
3/4 activos
</div>
</div>
</div>
{/* Tabs Navigation */}
<div className="bg-white rounded-xl shadow-sm p-1">
<div className="flex space-x-1">
{[
{ id: 'schedule', label: 'Programa', icon: Calendar },
{ id: 'batches', label: 'Lotes Activos', icon: Timer },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'staff', label: 'Personal', icon: Users },
{ id: 'equipment', label: 'Equipos', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
</button>
))}
</div>
</div>
{/* Tab Content */}
<div className="space-y-6">
{activeTab === 'schedule' && (
<>
<ProductionSchedule
schedule={productionSchedule}
onUpdateQuantity={(itemId, quantity) => {
setProductionSchedule(prev =>
prev.map(slot => ({
...slot,
items: slot.items.map(item =>
item.id === itemId ? { ...item, quantity } : item
)
}))
);
}}
onUpdateStatus={(itemId, status) => {
setProductionSchedule(prev =>
prev.map(slot => ({
...slot,
items: slot.items.map(item =>
item.id === itemId ? { ...item, status } : item
)
}))
);
}}
/>
</>
)}
{activeTab === 'batches' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{productionBatches.map((batch) => (
<div key={batch.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{batch.product}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(batch.status)}`}>
{batch.status === 'planned' ? 'Planificado' :
batch.status === 'in_progress' ? 'En Progreso' :
batch.status === 'completed' ? 'Completado' : 'Retrasado'}
</span>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Tamaño del Lote</p>
<p className="font-semibold text-gray-900">{batch.batchSize} unidades</p>
</div>
<div>
<p className="text-sm text-gray-600">Rendimiento</p>
<p className="font-semibold text-gray-900">
{batch.actualYield || 0}/{batch.expectedYield}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Inicio</p>
<p className="font-semibold text-gray-900">{batch.startTime}</p>
</div>
<div>
<p className="text-sm text-gray-600">Fin Estimado</p>
<p className="font-semibold text-gray-900">{batch.endTime}</p>
</div>
</div>
{(batch.temperature || batch.humidity) && (
<div className="grid grid-cols-2 gap-4">
{batch.temperature && (
<div>
<p className="text-sm text-gray-600">Temperatura</p>
<p className="font-semibold text-gray-900">{batch.temperature}°C</p>
</div>
)}
{batch.humidity && (
<div>
<p className="text-sm text-gray-600">Humedad</p>
<p className="font-semibold text-gray-900">{batch.humidity}%</p>
</div>
)}
</div>
)}
<div>
<p className="text-sm text-gray-600 mb-2">Personal Asignado</p>
<div className="flex space-x-2">
{batch.assignedStaff.map((staffId) => {
const staffMember = staff.find(s => s.id === staffId);
return (
<span
key={staffId}
className="px-2 py-1 bg-gray-100 text-gray-700 text-sm rounded"
>
{staffMember?.name || staffId}
</span>
);
})}
</div>
</div>
{batch.notes && (
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-sm text-gray-700">{batch.notes}</p>
</div>
)}
</div>
</div>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={heatmapData}
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Production Trends Chart Placeholder */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<BarChart3 className="h-5 w-5 mr-2 text-primary-600" />
Tendencias de Producción
</h3>
<div className="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de producción</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{activeTab === 'staff' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{staff.map((member) => (
<div key={member.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{member.name}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
member.status === 'available' ? 'bg-green-100 text-green-800' :
member.status === 'busy' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{member.status === 'available' ? 'Disponible' :
member.status === 'busy' ? 'Ocupado' : 'Descanso'}
</span>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Rol</p>
<p className="font-medium text-gray-900 capitalize">{member.role}</p>
</div>
{member.currentTask && (
<div>
<p className="text-sm text-gray-600">Tarea Actual</p>
<p className="font-medium text-gray-900">{member.currentTask}</p>
</div>
)}
<div>
<p className="text-sm text-gray-600">Eficiencia</p>
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full"
style={{ width: `${member.efficiency}%` }}
></div>
</div>
<span className="text-sm font-medium text-gray-900">{member.efficiency}%</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
{activeTab === 'equipment' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{equipment.map((item) => (
<div key={item.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{item.name}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getEquipmentStatusColor(item.status)}`}>
{item.status === 'idle' ? 'Inactivo' :
item.status === 'in_use' ? 'En Uso' :
item.status === 'maintenance' ? 'Mantenimiento' : 'Error'}
</span>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Tipo</p>
<p className="font-medium text-gray-900 capitalize">{item.type}</p>
</div>
{item.currentBatch && (
<div>
<p className="text-sm text-gray-600">Lote Actual</p>
<p className="font-medium text-gray-900">
{productionBatches.find(b => b.id === item.currentBatch)?.product || item.currentBatch}
</p>
</div>
)}
{item.temperature && (
<div>
<p className="text-sm text-gray-600">Temperatura</p>
<p className="font-medium text-gray-900">{item.temperature}°C</p>
</div>
)}
{item.maintenanceDue && (
<div>
<p className="text-sm text-gray-600">Próximo Mantenimiento</p>
<p className="font-medium text-orange-600">
{new Date(item.maintenanceDue).toLocaleDateString('es-ES')}
</p>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default ProductionPage;

31
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.js"]
}