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,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;