ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -1,17 +0,0 @@
import React from 'react';
const AIInsightsPage: React.FC = () => {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Insights de IA</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<p className="text-gray-500">Insights de IA en desarrollo</p>
</div>
</div>
);
};
export default AIInsightsPage;

View File

@@ -1,17 +0,0 @@
import React from 'react';
const FinancialReportsPage: React.FC = () => {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Reportes Financieros</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<p className="text-gray-500">Reportes financieros en desarrollo</p>
</div>
</div>
);
};
export default FinancialReportsPage;

View File

@@ -1,17 +0,0 @@
import React from 'react';
const PerformanceKPIsPage: React.FC = () => {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">KPIs de Rendimiento</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<p className="text-gray-500">KPIs de rendimiento en desarrollo</p>
</div>
</div>
);
};
export default PerformanceKPIsPage;

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { useBakeryType } from '../../hooks/useBakeryType';
const ProductionReportsPage: React.FC = () => {
const { getProductionLabel } = useBakeryType();
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Reportes de {getProductionLabel()}</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<p className="text-gray-500">Reportes de {getProductionLabel().toLowerCase()} en desarrollo</p>
</div>
</div>
);
};
export default ProductionReportsPage;

View File

@@ -1,120 +0,0 @@
import React, { useState } from 'react';
import { TrendingUp, DollarSign, ShoppingCart, Calendar } from 'lucide-react';
import { useBakeryType } from '../../hooks/useBakeryType';
const SalesAnalyticsPage: React.FC = () => {
const { isIndividual, isCentral } = useBakeryType();
const [timeRange, setTimeRange] = useState('week');
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Análisis de Ventas</h1>
<p className="text-gray-600 mt-1">
{isIndividual
? 'Analiza el rendimiento de ventas de tu panadería'
: 'Analiza el rendimiento de ventas de todos tus puntos de venta'
}
</p>
</div>
{/* Time Range Selector */}
<div className="mb-6">
<div className="flex space-x-2">
{['day', 'week', 'month', 'quarter'].map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
timeRange === range
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
{range === 'day' && 'Hoy'}
{range === 'week' && 'Esta Semana'}
{range === 'month' && 'Este Mes'}
{range === 'quarter' && 'Este Trimestre'}
</button>
))}
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="h-8 w-8 bg-green-100 rounded-lg flex items-center justify-center">
<DollarSign className="h-4 w-4 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Ingresos Totales</p>
<p className="text-2xl font-bold text-gray-900">2,847</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="h-8 w-8 bg-blue-100 rounded-lg flex items-center justify-center">
<ShoppingCart className="h-4 w-4 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">
{isIndividual ? 'Productos Vendidos' : 'Productos Distribuidos'}
</p>
<p className="text-2xl font-bold text-gray-900">1,429</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="h-8 w-8 bg-purple-100 rounded-lg flex items-center justify-center">
<TrendingUp className="h-4 w-4 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Crecimiento</p>
<p className="text-2xl font-bold text-gray-900">+12.5%</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="h-8 w-8 bg-yellow-100 rounded-lg flex items-center justify-center">
<Calendar className="h-4 w-4 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Días Activos</p>
<p className="text-2xl font-bold text-gray-900">6/7</p>
</div>
</div>
</div>
</div>
{/* Charts placeholder */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Tendencia de Ventas
</h3>
<div className="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<p className="text-gray-500">Gráfico de tendencias aquí</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{isIndividual ? 'Productos Más Vendidos' : 'Productos Más Distribuidos'}
</h3>
<div className="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<p className="text-gray-500">Gráfico de productos aquí</p>
</div>
</div>
</div>
</div>
);
};
export default SalesAnalyticsPage;

View File

@@ -0,0 +1,352 @@
import React from 'react';
import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Clock, DollarSign } from 'lucide-react';
import { Card, Badge } from '../../components/ui';
import { PageHeader } from '../../components/layout';
import { DashboardCard, KPIWidget, QuickActions, RecentActivity, ActivityType, ActivityStatus } from '../../components/domain/dashboard';
const DashboardPage: React.FC = () => {
const kpiData = [
{
title: 'Ventas Hoy',
value: {
current: 1247,
previous: 1112,
format: 'currency' as const,
prefix: '€'
},
trend: {
direction: 'up' as const,
value: 12,
isPositive: true,
comparisonPeriod: 'vs ayer'
},
icon: <DollarSign className="w-5 h-5" />,
},
{
title: 'Órdenes Pendientes',
value: {
current: 23,
previous: 24,
format: 'number' as const
},
trend: {
direction: 'down' as const,
value: 4.2,
isPositive: false,
comparisonPeriod: 'vs ayer'
},
icon: <Clock className="w-5 h-5" />,
},
{
title: 'Productos Vendidos',
value: {
current: 156,
previous: 144,
format: 'number' as const
},
trend: {
direction: 'up' as const,
value: 8.3,
isPositive: true,
comparisonPeriod: 'vs ayer'
},
icon: <CheckCircle className="w-5 h-5" />,
},
{
title: 'Stock Crítico',
value: {
current: 4,
previous: 2,
format: 'number' as const
},
trend: {
direction: 'up' as const,
value: 100,
isPositive: false,
comparisonPeriod: 'vs ayer'
},
status: 'warning' as const,
icon: <AlertTriangle className="w-5 h-5" />,
},
];
const quickActions = [
{
id: 'production',
title: 'Nueva Orden de Producción',
description: 'Crear nueva orden de producción',
icon: <TrendingUp className="w-6 h-6" />,
onClick: () => window.location.href = '/app/operations/production',
href: '/app/operations/production'
},
{
id: 'inventory',
title: 'Gestionar Inventario',
description: 'Administrar stock de productos',
icon: <CheckCircle className="w-6 h-6" />,
onClick: () => window.location.href = '/app/operations/inventory',
href: '/app/operations/inventory'
},
{
id: 'sales',
title: 'Ver Ventas',
description: 'Analizar ventas y reportes',
icon: <DollarSign className="w-6 h-6" />,
onClick: () => window.location.href = '/app/analytics/sales',
href: '/app/analytics/sales'
},
{
id: 'settings',
title: 'Configuración',
description: 'Ajustar configuración del sistema',
icon: <AlertTriangle className="w-6 h-6" />,
onClick: () => window.location.href = '/app/settings',
href: '/app/settings'
},
];
const recentActivities = [
{
id: '1',
type: ActivityType.PRODUCTION,
title: 'Orden de producción completada',
description: 'Pan de Molde Integral - 20 unidades',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
status: ActivityStatus.SUCCESS,
},
{
id: '2',
type: ActivityType.INVENTORY,
title: 'Stock bajo detectado',
description: 'Levadura fresca necesita reposición',
timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
status: ActivityStatus.WARNING,
},
{
id: '3',
type: ActivityType.SALES,
title: 'Venta registrada',
description: '€45.50 - Croissants y café',
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
status: ActivityStatus.INFO,
},
];
const productionStatus = {
today: {
target: 150,
completed: 95,
inProgress: 18,
pending: 37,
},
efficiency: 85,
};
const salesData = {
today: 1247,
yesterday: 1112,
thisWeek: 8934,
thisMonth: 35678,
};
const inventoryAlerts = [
{ item: 'Levadura Fresca', current: 2, min: 5, status: 'critical' },
{ item: 'Harina Integral', current: 8, min: 10, status: 'low' },
{ item: 'Mantequilla', current: 15, min: 20, status: 'low' },
];
const topProducts = [
{ name: 'Pan de Molde', sold: 45, revenue: 202.50 },
{ name: 'Croissants', sold: 32, revenue: 192.00 },
{ name: 'Baguettes', sold: 28, revenue: 84.00 },
{ name: 'Magdalenas', sold: 24, revenue: 72.00 },
];
return (
<div className="space-y-6">
<PageHeader
title="Panel de Control"
description="Vista general de tu panadería"
/>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpiData.map((kpi, index) => (
<KPIWidget
key={index}
title={kpi.title}
value={kpi.value}
trend={kpi.trend}
icon={kpi.icon}
status={kpi.status}
/>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Production Status */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado de Producción</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Progreso del Día</span>
<span className="text-sm font-medium">
{productionStatus.today.completed} / {productionStatus.today.target}
</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-[var(--color-info)] h-2 rounded-full"
style={{
width: `${(productionStatus.today.completed / productionStatus.today.target) * 100}%`
}}
></div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<div className="text-center">
<p className="text-2xl font-bold text-[var(--color-success)]">{productionStatus.today.completed}</p>
<p className="text-xs text-[var(--text-secondary)]">Completado</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{productionStatus.today.inProgress}</p>
<p className="text-xs text-[var(--text-secondary)]">En Proceso</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-[var(--color-primary)]">{productionStatus.today.pending}</p>
<p className="text-xs text-[var(--text-secondary)]">Pendiente</p>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Eficiencia</span>
<span className="text-sm font-medium text-purple-600">{productionStatus.efficiency}%</span>
</div>
</div>
</div>
</Card>
{/* Sales Summary */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen de Ventas</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Hoy</span>
<span className="text-lg font-semibold text-[var(--color-success)]">{salesData.today.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Ayer</span>
<div className="flex items-center">
<span className="text-sm font-medium mr-2">{salesData.yesterday.toLocaleString()}</span>
{salesData.today > salesData.yesterday ? (
<TrendingUp className="h-4 w-4 text-[var(--color-success)]" />
) : (
<TrendingDown className="h-4 w-4 text-[var(--color-error)]" />
)}
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Esta Semana</span>
<span className="text-sm font-medium">{salesData.thisWeek.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Este Mes</span>
<span className="text-sm font-medium">{salesData.thisMonth.toLocaleString()}</span>
</div>
<div className="mt-4 pt-4 border-t">
<div className="text-center">
<p className="text-xs text-[var(--text-secondary)]">Crecimiento vs ayer</p>
<p className="text-lg font-semibold text-[var(--color-success)]">
+{(((salesData.today - salesData.yesterday) / salesData.yesterday) * 100).toFixed(1)}%
</p>
</div>
</div>
</div>
</Card>
{/* Inventory Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas de Inventario</h3>
<div className="space-y-3">
{inventoryAlerts.map((alert, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--color-error)]/10 rounded-lg">
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{alert.item}</p>
<p className="text-xs text-[var(--text-secondary)]">Stock: {alert.current} / Mín: {alert.min}</p>
</div>
<Badge variant={alert.status === 'critical' ? 'red' : 'yellow'}>
{alert.status === 'critical' ? 'Crítico' : 'Bajo'}
</Badge>
</div>
))}
</div>
<div className="mt-4 pt-4 border-t">
<button className="w-full text-sm text-[var(--color-info)] hover:text-[var(--color-info)] font-medium">
Ver Todo el Inventario
</button>
</div>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Productos Más Vendidos</h3>
<div className="space-y-3">
{topProducts.map((product, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-sm font-medium text-[var(--text-secondary)] w-6">{index + 1}.</span>
<span className="text-sm text-[var(--text-primary)]">{product.name}</span>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">{product.sold} unidades</p>
<p className="text-xs text-[var(--color-success)]">{product.revenue.toFixed(2)}</p>
</div>
</div>
))}
</div>
<div className="mt-4 pt-4 border-t">
<button className="w-full text-sm text-[var(--color-info)] hover:text-[var(--color-info)] font-medium">
Ver Análisis Completo
</button>
</div>
</Card>
{/* Recent Activity */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Actividad Reciente</h3>
<RecentActivity activities={recentActivities} />
<div className="mt-4 pt-4 border-t">
<button className="w-full text-sm text-[var(--color-info)] hover:text-[var(--color-info)] font-medium">
Ver Toda la Actividad
</button>
</div>
</Card>
</div>
{/* Quick Actions */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Acciones Rápidas</h3>
<QuickActions actions={quickActions} />
</Card>
</div>
);
};
export default DashboardPage;

View File

@@ -0,0 +1,313 @@
import React, { useState } from 'react';
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [isRefreshing, setIsRefreshing] = useState(false);
const insights = [
{
id: '1',
type: 'optimization',
priority: 'high',
title: 'Optimización de Producción de Croissants',
description: 'La demanda de croissants aumenta un 23% los viernes. Recomendamos incrementar la producción en 15 unidades.',
impact: 'Aumento estimado de ingresos: €180/semana',
confidence: 87,
category: 'production',
timestamp: '2024-01-26 09:30',
actionable: true,
metrics: {
currentProduction: 45,
recommendedProduction: 60,
expectedIncrease: '+23%'
}
},
{
id: '2',
type: 'alert',
priority: 'medium',
title: 'Patrón de Compra en Tardes',
description: 'Los clientes compran más productos salados después de las 16:00. Considera promocionar empanadas durante estas horas.',
impact: 'Potencial aumento de ventas: 12%',
confidence: 92,
category: 'sales',
timestamp: '2024-01-26 08:45',
actionable: true,
metrics: {
afternoonSales: '+15%',
savoryProducts: '68%',
conversionRate: '12.3%'
}
},
{
id: '3',
type: 'prediction',
priority: 'high',
title: 'Predicción de Demanda de San Valentín',
description: 'Se espera un incremento del 40% en la demanda de productos de repostería especiales entre el 10-14 de febrero.',
impact: 'Preparar stock adicional de ingredientes premium',
confidence: 94,
category: 'forecasting',
timestamp: '2024-01-26 07:15',
actionable: true,
metrics: {
expectedIncrease: '+40%',
daysAhead: 18,
recommendedPrep: '3 días'
}
},
{
id: '4',
type: 'recommendation',
priority: 'low',
title: 'Optimización de Inventario de Harina',
description: 'El consumo de harina integral ha disminuido 8% este mes. Considera ajustar las órdenes de compra.',
impact: 'Reducción de desperdicios: €45/mes',
confidence: 78,
category: 'inventory',
timestamp: '2024-01-25 16:20',
actionable: false,
metrics: {
consumption: '-8%',
currentStock: '45kg',
recommendedOrder: '25kg'
}
},
{
id: '5',
type: 'insight',
priority: 'medium',
title: 'Análisis de Satisfacción del Cliente',
description: 'Los clientes valoran más la frescura (95%) que el precio (67%). Enfoque en destacar la calidad artesanal.',
impact: 'Mejorar estrategia de marketing',
confidence: 89,
category: 'customer',
timestamp: '2024-01-25 14:30',
actionable: true,
metrics: {
freshnessScore: '95%',
priceScore: '67%',
qualityScore: '91%'
}
}
];
const categories = [
{ value: 'all', label: 'Todas las Categorías', count: insights.length },
{ value: 'production', label: 'Producción', count: insights.filter(i => i.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: insights.filter(i => i.category === 'sales').length },
{ value: 'forecasting', label: 'Pronósticos', count: insights.filter(i => i.category === 'forecasting').length },
{ value: 'inventory', label: 'Inventario', count: insights.filter(i => i.category === 'inventory').length },
{ value: 'customer', label: 'Clientes', count: insights.filter(i => i.category === 'customer').length },
];
const aiMetrics = {
totalInsights: insights.length,
actionableInsights: insights.filter(i => i.actionable).length,
averageConfidence: Math.round(insights.reduce((sum, i) => sum + i.confidence, 0) / insights.length),
highPriorityInsights: insights.filter(i => i.priority === 'high').length,
};
const getTypeIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'optimization': return <Target {...iconProps} />;
case 'alert': return <AlertTriangle {...iconProps} />;
case 'prediction': return <TrendingUp {...iconProps} />;
case 'recommendation': return <Lightbulb {...iconProps} />;
case 'insight': return <Brain {...iconProps} />;
default: return <Brain {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'optimization': return 'bg-[var(--color-info)]/10 text-[var(--color-info)]';
case 'alert': return 'bg-[var(--color-error)]/10 text-[var(--color-error)]';
case 'prediction': return 'bg-purple-100 text-purple-800';
case 'recommendation': return 'bg-[var(--color-success)]/10 text-[var(--color-success)]';
case 'insight': return 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]';
default: return 'bg-[var(--bg-tertiary)] text-[var(--text-primary)]';
}
};
const filteredInsights = selectedCategory === 'all'
? insights
: insights.filter(insight => insight.category === selectedCategory);
const handleRefresh = async () => {
setIsRefreshing(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsRefreshing(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Inteligencia Artificial"
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
Actualizar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* AI Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Insights</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{aiMetrics.totalInsights}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Brain className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Accionables</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{aiMetrics.actionableInsights}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Zap className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Confianza Promedio</p>
<p className="text-3xl font-bold text-purple-600">{aiMetrics.averageConfidence}%</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Target className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Alta Prioridad</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{aiMetrics.highPriorityInsights}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
</div>
{/* Category Filter */}
<Card className="p-6">
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</Card>
{/* Insights List */}
<div className="space-y-4">
{filteredInsights.map((insight) => (
<Card key={insight.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg ${getTypeColor(insight.type)}`}>
{getTypeIcon(insight.type)}
</div>
<div className="flex items-center space-x-2">
<Badge variant={getPriorityColor(insight.priority)}>
{insight.priority === 'high' ? 'Alta' : insight.priority === 'medium' ? 'Media' : 'Baja'} Prioridad
</Badge>
<Badge variant="gray">{insight.confidence}% confianza</Badge>
{insight.actionable && (
<Badge variant="blue">Accionable</Badge>
)}
</div>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{insight.title}</h3>
<p className="text-[var(--text-secondary)] mb-3">{insight.description}</p>
<p className="text-sm font-medium text-[var(--color-success)] mb-4">{insight.impact}</p>
{/* Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{Object.entries(insight.metrics).map(([key, value]) => (
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="text-sm font-semibold text-[var(--text-primary)]">{value}</p>
</div>
))}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-[var(--text-tertiary)]">{insight.timestamp}</p>
{insight.actionable && (
<Button size="sm">
Aplicar Recomendación
</Button>
)}
</div>
</div>
</div>
</Card>
))}
</div>
{filteredInsights.length === 0 && (
<Card className="p-12 text-center">
<Brain className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay insights disponibles</h3>
<p className="text-[var(--text-secondary)] mb-4">
No se encontraron insights para la categoría seleccionada.
</p>
<Button onClick={handleRefresh}>
<RefreshCw className="w-4 h-4 mr-2" />
Generar Nuevos Insights
</Button>
</Card>
)}
</div>
);
};
export default AIInsightsPage;

View File

@@ -0,0 +1,313 @@
import React, { useState } from 'react';
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [isRefreshing, setIsRefreshing] = useState(false);
const insights = [
{
id: '1',
type: 'optimization',
priority: 'high',
title: 'Optimización de Producción de Croissants',
description: 'La demanda de croissants aumenta un 23% los viernes. Recomendamos incrementar la producción en 15 unidades.',
impact: 'Aumento estimado de ingresos: €180/semana',
confidence: 87,
category: 'production',
timestamp: '2024-01-26 09:30',
actionable: true,
metrics: {
currentProduction: 45,
recommendedProduction: 60,
expectedIncrease: '+23%'
}
},
{
id: '2',
type: 'alert',
priority: 'medium',
title: 'Patrón de Compra en Tardes',
description: 'Los clientes compran más productos salados después de las 16:00. Considera promocionar empanadas durante estas horas.',
impact: 'Potencial aumento de ventas: 12%',
confidence: 92,
category: 'sales',
timestamp: '2024-01-26 08:45',
actionable: true,
metrics: {
afternoonSales: '+15%',
savoryProducts: '68%',
conversionRate: '12.3%'
}
},
{
id: '3',
type: 'prediction',
priority: 'high',
title: 'Predicción de Demanda de San Valentín',
description: 'Se espera un incremento del 40% en la demanda de productos de repostería especiales entre el 10-14 de febrero.',
impact: 'Preparar stock adicional de ingredientes premium',
confidence: 94,
category: 'forecasting',
timestamp: '2024-01-26 07:15',
actionable: true,
metrics: {
expectedIncrease: '+40%',
daysAhead: 18,
recommendedPrep: '3 días'
}
},
{
id: '4',
type: 'recommendation',
priority: 'low',
title: 'Optimización de Inventario de Harina',
description: 'El consumo de harina integral ha disminuido 8% este mes. Considera ajustar las órdenes de compra.',
impact: 'Reducción de desperdicios: €45/mes',
confidence: 78,
category: 'inventory',
timestamp: '2024-01-25 16:20',
actionable: false,
metrics: {
consumption: '-8%',
currentStock: '45kg',
recommendedOrder: '25kg'
}
},
{
id: '5',
type: 'insight',
priority: 'medium',
title: 'Análisis de Satisfacción del Cliente',
description: 'Los clientes valoran más la frescura (95%) que el precio (67%). Enfoque en destacar la calidad artesanal.',
impact: 'Mejorar estrategia de marketing',
confidence: 89,
category: 'customer',
timestamp: '2024-01-25 14:30',
actionable: true,
metrics: {
freshnessScore: '95%',
priceScore: '67%',
qualityScore: '91%'
}
}
];
const categories = [
{ value: 'all', label: 'Todas las Categorías', count: insights.length },
{ value: 'production', label: 'Producción', count: insights.filter(i => i.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: insights.filter(i => i.category === 'sales').length },
{ value: 'forecasting', label: 'Pronósticos', count: insights.filter(i => i.category === 'forecasting').length },
{ value: 'inventory', label: 'Inventario', count: insights.filter(i => i.category === 'inventory').length },
{ value: 'customer', label: 'Clientes', count: insights.filter(i => i.category === 'customer').length },
];
const aiMetrics = {
totalInsights: insights.length,
actionableInsights: insights.filter(i => i.actionable).length,
averageConfidence: Math.round(insights.reduce((sum, i) => sum + i.confidence, 0) / insights.length),
highPriorityInsights: insights.filter(i => i.priority === 'high').length,
};
const getTypeIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'optimization': return <Target {...iconProps} />;
case 'alert': return <AlertTriangle {...iconProps} />;
case 'prediction': return <TrendingUp {...iconProps} />;
case 'recommendation': return <Lightbulb {...iconProps} />;
case 'insight': return <Brain {...iconProps} />;
default: return <Brain {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'optimization': return 'bg-blue-100 text-blue-800';
case 'alert': return 'bg-red-100 text-red-800';
case 'prediction': return 'bg-purple-100 text-purple-800';
case 'recommendation': return 'bg-green-100 text-green-800';
case 'insight': return 'bg-orange-100 text-orange-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const filteredInsights = selectedCategory === 'all'
? insights
: insights.filter(insight => insight.category === selectedCategory);
const handleRefresh = async () => {
setIsRefreshing(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsRefreshing(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Inteligencia Artificial"
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
Actualizar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* AI Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Insights</p>
<p className="text-3xl font-bold text-blue-600">{aiMetrics.totalInsights}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Brain className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Accionables</p>
<p className="text-3xl font-bold text-green-600">{aiMetrics.actionableInsights}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Zap className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Confianza Promedio</p>
<p className="text-3xl font-bold text-purple-600">{aiMetrics.averageConfidence}%</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Target className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Alta Prioridad</p>
<p className="text-3xl font-bold text-red-600">{aiMetrics.highPriorityInsights}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
</div>
{/* Category Filter */}
<Card className="p-6">
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</Card>
{/* Insights List */}
<div className="space-y-4">
{filteredInsights.map((insight) => (
<Card key={insight.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg ${getTypeColor(insight.type)}`}>
{getTypeIcon(insight.type)}
</div>
<div className="flex items-center space-x-2">
<Badge variant={getPriorityColor(insight.priority)}>
{insight.priority === 'high' ? 'Alta' : insight.priority === 'medium' ? 'Media' : 'Baja'} Prioridad
</Badge>
<Badge variant="gray">{insight.confidence}% confianza</Badge>
{insight.actionable && (
<Badge variant="blue">Accionable</Badge>
)}
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{insight.title}</h3>
<p className="text-gray-700 mb-3">{insight.description}</p>
<p className="text-sm font-medium text-green-600 mb-4">{insight.impact}</p>
{/* Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{Object.entries(insight.metrics).map(([key, value]) => (
<div key={key} className="bg-gray-50 p-3 rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wider">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="text-sm font-semibold text-gray-900">{value}</p>
</div>
))}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">{insight.timestamp}</p>
{insight.actionable && (
<Button size="sm">
Aplicar Recomendación
</Button>
)}
</div>
</div>
</div>
</Card>
))}
</div>
{filteredInsights.length === 0 && (
<Card className="p-12 text-center">
<Brain className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay insights disponibles</h3>
<p className="text-gray-600 mb-4">
No se encontraron insights para la categoría seleccionada.
</p>
<Button onClick={handleRefresh}>
<RefreshCw className="w-4 h-4 mr-2" />
Generar Nuevos Insights
</Button>
</Card>
)}
</div>
);
};
export default AIInsightsPage;

View File

@@ -0,0 +1 @@
export { default as AIInsightsPage } from './AIInsightsPage';

View File

@@ -0,0 +1,385 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings } from 'lucide-react';
import { Button, Card, Badge, Select } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { DemandChart, ForecastTable, SeasonalityIndicator, AlertsPanel } from '../../../../components/domain/forecasting';
const ForecastingPage: React.FC = () => {
const [selectedProduct, setSelectedProduct] = useState('all');
const [forecastPeriod, setForecastPeriod] = useState('7');
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
const forecastData = {
accuracy: 92,
totalDemand: 1247,
growthTrend: 8.5,
seasonalityFactor: 1.15,
};
const products = [
{ id: 'all', name: 'Todos los productos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
];
const periods = [
{ value: '7', label: '7 días' },
{ value: '14', label: '14 días' },
{ value: '30', label: '30 días' },
{ value: '90', label: '3 meses' },
];
const mockForecasts = [
{
id: '1',
product: 'Pan de Molde Integral',
currentStock: 25,
forecastDemand: 45,
recommendedProduction: 50,
confidence: 95,
trend: 'up',
stockoutRisk: 'low',
},
{
id: '2',
product: 'Croissants de Mantequilla',
currentStock: 18,
forecastDemand: 32,
recommendedProduction: 35,
confidence: 88,
trend: 'stable',
stockoutRisk: 'medium',
},
{
id: '3',
product: 'Baguettes Francesas',
currentStock: 12,
forecastDemand: 28,
recommendedProduction: 30,
confidence: 91,
trend: 'down',
stockoutRisk: 'high',
},
];
const alerts = [
{
id: '1',
type: 'stockout',
product: 'Baguettes Francesas',
message: 'Alto riesgo de agotamiento en las próximas 24h',
severity: 'high',
recommendation: 'Incrementar producción en 15 unidades',
},
{
id: '2',
type: 'overstock',
product: 'Magdalenas',
message: 'Probable exceso de stock para mañana',
severity: 'medium',
recommendation: 'Reducir producción en 20%',
},
{
id: '3',
type: 'weather',
product: 'Todos',
message: 'Lluvia prevista - incremento esperado en demanda de bollería',
severity: 'info',
recommendation: 'Aumentar producción de productos de interior en 10%',
},
];
const weatherImpact = {
today: 'sunny',
temperature: 22,
demandFactor: 0.95,
affectedCategories: ['helados', 'bebidas frías'],
};
const seasonalInsights = [
{ period: 'Mañana', factor: 1.2, products: ['Pan', 'Bollería'] },
{ period: 'Tarde', factor: 0.8, products: ['Tartas', 'Dulces'] },
{ period: 'Fin de semana', factor: 1.4, products: ['Tartas especiales'] },
];
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'up':
return <TrendingUp className="h-4 w-4 text-[var(--color-success)]" />;
case 'down':
return <TrendingUp className="h-4 w-4 text-[var(--color-error)] rotate-180" />;
default:
return <div className="h-4 w-4 bg-gray-400 rounded-full" />;
}
};
const getRiskBadge = (risk: string) => {
const riskConfig = {
low: { color: 'green', text: 'Bajo' },
medium: { color: 'yellow', text: 'Medio' },
high: { color: 'red', text: 'Alto' },
};
const config = riskConfig[risk as keyof typeof riskConfig];
return <Badge variant={config?.color as any}>{config?.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Predicción de Demanda"
description="Predicciones inteligentes basadas en IA para optimizar tu producción"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Configurar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Precisión del Modelo</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{forecastData.accuracy}%</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Demanda Prevista</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{forecastData.totalDemand}</p>
<p className="text-xs text-[var(--text-tertiary)]">próximos {forecastPeriod} días</p>
</div>
<Calendar className="h-12 w-12 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tendencia</p>
<p className="text-3xl font-bold text-purple-600">+{forecastData.growthTrend}%</p>
<p className="text-xs text-[var(--text-tertiary)]">vs período anterior</p>
</div>
<TrendingUp className="h-12 w-12 text-purple-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Factor Estacional</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{forecastData.seasonalityFactor}x</p>
<p className="text-xs text-[var(--text-tertiary)]">multiplicador actual</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707" />
</svg>
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Producto</label>
<select
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{products.map(product => (
<option key={product.id} value={product.id}>{product.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={forecastPeriod}
onChange={(e) => setForecastPeriod(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Vista</label>
<div className="flex rounded-md border border-[var(--border-secondary)]">
<button
onClick={() => setViewMode('chart')}
className={`px-3 py-2 text-sm ${viewMode === 'chart' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-l-md`}
>
Gráfico
</button>
<button
onClick={() => setViewMode('table')}
className={`px-3 py-2 text-sm ${viewMode === 'table' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-r-md border-l`}
>
Tabla
</button>
</div>
</div>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Forecast Display */}
<div className="lg:col-span-2">
{viewMode === 'chart' ? (
<DemandChart
product={selectedProduct}
period={forecastPeriod}
/>
) : (
<ForecastTable forecasts={mockForecasts} />
)}
</div>
{/* Alerts Panel */}
<div className="space-y-6">
<AlertsPanel alerts={alerts} />
{/* Weather Impact */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Impacto Meteorológico</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Hoy:</span>
<div className="flex items-center">
<span className="text-sm font-medium">{weatherImpact.temperature}°C</span>
<div className="ml-2 w-6 h-6 bg-yellow-400 rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Factor de demanda:</span>
<span className="text-sm font-medium text-[var(--color-info)]">{weatherImpact.demandFactor}x</span>
</div>
<div className="mt-4">
<p className="text-xs text-[var(--text-tertiary)] mb-2">Categorías afectadas:</p>
<div className="flex flex-wrap gap-1">
{weatherImpact.affectedCategories.map((category, index) => (
<Badge key={index} variant="blue">{category}</Badge>
))}
</div>
</div>
</div>
</Card>
{/* Seasonal Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Patrones Estacionales</h3>
<div className="space-y-3">
{seasonalInsights.map((insight, index) => (
<div key={index} className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{insight.period}</span>
<span className="text-sm text-purple-600 font-medium">{insight.factor}x</span>
</div>
<div className="flex flex-wrap gap-1">
{insight.products.map((product, idx) => (
<Badge key={idx} variant="purple">{product}</Badge>
))}
</div>
</div>
))}
</div>
</Card>
</div>
</div>
{/* Detailed Forecasts Table */}
<Card>
<div className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Predicciones Detalladas</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Producto
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Stock Actual
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Demanda Prevista
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Producción Recomendada
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Confianza
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Tendencia
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Riesgo Agotamiento
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockForecasts.map((forecast) => (
<tr key={forecast.id} className="hover:bg-[var(--bg-secondary)]">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-[var(--text-primary)]">{forecast.product}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{forecast.currentStock}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--color-info)]">
{forecast.forecastDemand}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--color-success)]">
{forecast.recommendedProduction}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{forecast.confidence}%
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getTrendIcon(forecast.trend)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getRiskBadge(forecast.stockoutRisk)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</div>
);
};
export default ForecastingPage;

View File

@@ -0,0 +1,385 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings } from 'lucide-react';
import { Button, Card, Badge, Select } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { DemandChart, ForecastTable, SeasonalityIndicator, AlertsPanel } from '../../../../components/domain/forecasting';
const ForecastingPage: React.FC = () => {
const [selectedProduct, setSelectedProduct] = useState('all');
const [forecastPeriod, setForecastPeriod] = useState('7');
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
const forecastData = {
accuracy: 92,
totalDemand: 1247,
growthTrend: 8.5,
seasonalityFactor: 1.15,
};
const products = [
{ id: 'all', name: 'Todos los productos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
];
const periods = [
{ value: '7', label: '7 días' },
{ value: '14', label: '14 días' },
{ value: '30', label: '30 días' },
{ value: '90', label: '3 meses' },
];
const mockForecasts = [
{
id: '1',
product: 'Pan de Molde Integral',
currentStock: 25,
forecastDemand: 45,
recommendedProduction: 50,
confidence: 95,
trend: 'up',
stockoutRisk: 'low',
},
{
id: '2',
product: 'Croissants de Mantequilla',
currentStock: 18,
forecastDemand: 32,
recommendedProduction: 35,
confidence: 88,
trend: 'stable',
stockoutRisk: 'medium',
},
{
id: '3',
product: 'Baguettes Francesas',
currentStock: 12,
forecastDemand: 28,
recommendedProduction: 30,
confidence: 91,
trend: 'down',
stockoutRisk: 'high',
},
];
const alerts = [
{
id: '1',
type: 'stockout',
product: 'Baguettes Francesas',
message: 'Alto riesgo de agotamiento en las próximas 24h',
severity: 'high',
recommendation: 'Incrementar producción en 15 unidades',
},
{
id: '2',
type: 'overstock',
product: 'Magdalenas',
message: 'Probable exceso de stock para mañana',
severity: 'medium',
recommendation: 'Reducir producción en 20%',
},
{
id: '3',
type: 'weather',
product: 'Todos',
message: 'Lluvia prevista - incremento esperado en demanda de bollería',
severity: 'info',
recommendation: 'Aumentar producción de productos de interior en 10%',
},
];
const weatherImpact = {
today: 'sunny',
temperature: 22,
demandFactor: 0.95,
affectedCategories: ['helados', 'bebidas frías'],
};
const seasonalInsights = [
{ period: 'Mañana', factor: 1.2, products: ['Pan', 'Bollería'] },
{ period: 'Tarde', factor: 0.8, products: ['Tartas', 'Dulces'] },
{ period: 'Fin de semana', factor: 1.4, products: ['Tartas especiales'] },
];
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'up':
return <TrendingUp className="h-4 w-4 text-green-600" />;
case 'down':
return <TrendingUp className="h-4 w-4 text-red-600 rotate-180" />;
default:
return <div className="h-4 w-4 bg-gray-400 rounded-full" />;
}
};
const getRiskBadge = (risk: string) => {
const riskConfig = {
low: { color: 'green', text: 'Bajo' },
medium: { color: 'yellow', text: 'Medio' },
high: { color: 'red', text: 'Alto' },
};
const config = riskConfig[risk as keyof typeof riskConfig];
return <Badge variant={config?.color as any}>{config?.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Predicción de Demanda"
description="Predicciones inteligentes basadas en IA para optimizar tu producción"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Configurar
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Precisión del Modelo</p>
<p className="text-3xl font-bold text-green-600">{forecastData.accuracy}%</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Demanda Prevista</p>
<p className="text-3xl font-bold text-blue-600">{forecastData.totalDemand}</p>
<p className="text-xs text-gray-500">próximos {forecastPeriod} días</p>
</div>
<Calendar className="h-12 w-12 text-blue-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Tendencia</p>
<p className="text-3xl font-bold text-purple-600">+{forecastData.growthTrend}%</p>
<p className="text-xs text-gray-500">vs período anterior</p>
</div>
<TrendingUp className="h-12 w-12 text-purple-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Factor Estacional</p>
<p className="text-3xl font-bold text-orange-600">{forecastData.seasonalityFactor}x</p>
<p className="text-xs text-gray-500">multiplicador actual</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707" />
</svg>
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Producto</label>
<select
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{products.map(product => (
<option key={product.id} value={product.id}>{product.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={forecastPeriod}
onChange={(e) => setForecastPeriod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Vista</label>
<div className="flex rounded-md border border-gray-300">
<button
onClick={() => setViewMode('chart')}
className={`px-3 py-2 text-sm ${viewMode === 'chart' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-l-md`}
>
Gráfico
</button>
<button
onClick={() => setViewMode('table')}
className={`px-3 py-2 text-sm ${viewMode === 'table' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-r-md border-l`}
>
Tabla
</button>
</div>
</div>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Forecast Display */}
<div className="lg:col-span-2">
{viewMode === 'chart' ? (
<DemandChart
product={selectedProduct}
period={forecastPeriod}
/>
) : (
<ForecastTable forecasts={mockForecasts} />
)}
</div>
{/* Alerts Panel */}
<div className="space-y-6">
<AlertsPanel alerts={alerts} />
{/* Weather Impact */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Impacto Meteorológico</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Hoy:</span>
<div className="flex items-center">
<span className="text-sm font-medium">{weatherImpact.temperature}°C</span>
<div className="ml-2 w-6 h-6 bg-yellow-400 rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Factor de demanda:</span>
<span className="text-sm font-medium text-blue-600">{weatherImpact.demandFactor}x</span>
</div>
<div className="mt-4">
<p className="text-xs text-gray-500 mb-2">Categorías afectadas:</p>
<div className="flex flex-wrap gap-1">
{weatherImpact.affectedCategories.map((category, index) => (
<Badge key={index} variant="blue">{category}</Badge>
))}
</div>
</div>
</div>
</Card>
{/* Seasonal Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Patrones Estacionales</h3>
<div className="space-y-3">
{seasonalInsights.map((insight, index) => (
<div key={index} className="p-3 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{insight.period}</span>
<span className="text-sm text-purple-600 font-medium">{insight.factor}x</span>
</div>
<div className="flex flex-wrap gap-1">
{insight.products.map((product, idx) => (
<Badge key={idx} variant="purple">{product}</Badge>
))}
</div>
</div>
))}
</div>
</Card>
</div>
</div>
{/* Detailed Forecasts Table */}
<Card>
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Predicciones Detalladas</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Producto
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Stock Actual
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Demanda Prevista
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Producción Recomendada
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Confianza
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tendencia
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Riesgo Agotamiento
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockForecasts.map((forecast) => (
<tr key={forecast.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{forecast.product}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{forecast.currentStock}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-blue-600">
{forecast.forecastDemand}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-green-600">
{forecast.recommendedProduction}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{forecast.confidence}%
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getTrendIcon(forecast.trend)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getRiskBadge(forecast.stockoutRisk)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</div>
);
};
export default ForecastingPage;

View File

@@ -0,0 +1 @@
export { default as ForecastingPage } from './ForecastingPage';

View File

@@ -0,0 +1,2 @@
export * from './forecasting';
export * from './sales-analytics';

View File

@@ -0,0 +1,403 @@
import React, { useState } from 'react';
import { Activity, Clock, Users, TrendingUp, Target, AlertCircle, Download, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PerformanceAnalyticsPage: React.FC = () => {
const [selectedTimeframe, setSelectedTimeframe] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('efficiency');
const performanceMetrics = {
overallEfficiency: 87.5,
productionTime: 4.2,
qualityScore: 92.1,
employeeProductivity: 89.3,
customerSatisfaction: 94.7,
resourceUtilization: 78.9,
};
const timeframes = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Trimestre' },
{ value: 'year', label: 'Año' },
];
const departmentPerformance = [
{
department: 'Producción',
efficiency: 91.2,
trend: 5.3,
issues: 2,
employees: 8,
metrics: {
avgBatchTime: '2.3h',
qualityRate: '94%',
wastePercentage: '3.1%'
}
},
{
department: 'Ventas',
efficiency: 88.7,
trend: -1.2,
issues: 1,
employees: 4,
metrics: {
avgServiceTime: '3.2min',
customerWaitTime: '2.1min',
salesPerHour: '€127'
}
},
{
department: 'Inventario',
efficiency: 82.4,
trend: 2.8,
issues: 3,
employees: 2,
metrics: {
stockAccuracy: '96.7%',
turnoverRate: '12.3',
wastageRate: '4.2%'
}
},
{
department: 'Administración',
efficiency: 94.1,
trend: 8.1,
issues: 0,
employees: 3,
metrics: {
responseTime: '1.2h',
taskCompletion: '98%',
documentAccuracy: '99.1%'
}
}
];
const kpiTrends = [
{
name: 'Eficiencia General',
current: 87.5,
target: 90.0,
previous: 84.2,
unit: '%',
color: 'blue'
},
{
name: 'Tiempo de Producción',
current: 4.2,
target: 4.0,
previous: 4.5,
unit: 'h',
color: 'green',
inverse: true
},
{
name: 'Satisfacción Cliente',
current: 94.7,
target: 95.0,
previous: 93.1,
unit: '%',
color: 'purple'
},
{
name: 'Utilización de Recursos',
current: 78.9,
target: 85.0,
previous: 76.3,
unit: '%',
color: 'orange'
}
];
const performanceAlerts = [
{
id: '1',
type: 'warning',
title: 'Eficiencia de Inventario Baja',
description: 'El departamento de inventario está por debajo del objetivo del 85%',
value: '82.4%',
target: '85%',
department: 'Inventario'
},
{
id: '2',
type: 'info',
title: 'Tiempo de Producción Mejorado',
description: 'El tiempo promedio de producción ha mejorado este mes',
value: '4.2h',
target: '4.0h',
department: 'Producción'
},
{
id: '3',
type: 'success',
title: 'Administración Supera Objetivos',
description: 'El departamento administrativo está funcionando por encima del objetivo',
value: '94.1%',
target: '90%',
department: 'Administración'
}
];
const productivityData = [
{ hour: '07:00', efficiency: 75, transactions: 12, employees: 3 },
{ hour: '08:00', efficiency: 82, transactions: 18, employees: 5 },
{ hour: '09:00', efficiency: 89, transactions: 28, employees: 6 },
{ hour: '10:00', efficiency: 91, transactions: 32, employees: 7 },
{ hour: '11:00', efficiency: 94, transactions: 38, employees: 8 },
{ hour: '12:00', efficiency: 96, transactions: 45, employees: 8 },
{ hour: '13:00', efficiency: 95, transactions: 42, employees: 8 },
{ hour: '14:00', efficiency: 88, transactions: 35, employees: 7 },
{ hour: '15:00', efficiency: 85, transactions: 28, employees: 6 },
{ hour: '16:00', efficiency: 83, transactions: 25, employees: 5 },
{ hour: '17:00', efficiency: 87, transactions: 31, employees: 6 },
{ hour: '18:00', efficiency: 90, transactions: 38, employees: 7 },
{ hour: '19:00', efficiency: 86, transactions: 29, employees: 5 },
{ hour: '20:00', efficiency: 78, transactions: 18, employees: 3 },
];
const getTrendIcon = (trend: number) => {
if (trend > 0) {
return <TrendingUp className="w-4 h-4 text-[var(--color-success)]" />;
} else {
return <TrendingUp className="w-4 h-4 text-[var(--color-error)] transform rotate-180" />;
}
};
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]';
};
const getPerformanceColor = (value: number, target: number, inverse = false) => {
const comparison = inverse ? value < target : value >= target;
return comparison ? 'text-[var(--color-success)]' : value >= target * 0.9 ? 'text-yellow-600' : 'text-[var(--color-error)]';
};
const getAlertIcon = (type: string) => {
switch (type) {
case 'warning':
return <AlertCircle className="w-5 h-5 text-yellow-600" />;
case 'success':
return <TrendingUp className="w-5 h-5 text-[var(--color-success)]" />;
default:
return <Activity className="w-5 h-5 text-[var(--color-info)]" />;
}
};
const getAlertColor = (type: string) => {
switch (type) {
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'success':
return 'bg-green-50 border-green-200';
default:
return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea la eficiencia operativa y el rendimiento de todos los departamentos"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Configurar Alertas
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{timeframes.map(timeframe => (
<option key={timeframe.value} value={timeframe.value}>{timeframe.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="efficiency">Eficiencia</option>
<option value="productivity">Productividad</option>
<option value="quality">Calidad</option>
<option value="satisfaction">Satisfacción</option>
</select>
</div>
</div>
</Card>
{/* KPI Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpiTrends.map((kpi) => (
<Card key={kpi.name} className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-[var(--text-secondary)]">{kpi.name}</h3>
<div className={`w-3 h-3 rounded-full bg-${kpi.color}-500`}></div>
</div>
<div className="flex items-end justify-between">
<div>
<p className={`text-2xl font-bold ${getPerformanceColor(kpi.current, kpi.target, kpi.inverse)}`}>
{kpi.current}{kpi.unit}
</p>
<p className="text-xs text-[var(--text-tertiary)]">
Objetivo: {kpi.target}{kpi.unit}
</p>
</div>
<div className="text-right">
<div className="flex items-center">
{getTrendIcon(kpi.current - kpi.previous)}
<span className={`text-sm ml-1 ${getTrendColor(kpi.current - kpi.previous)}`}>
{Math.abs(kpi.current - kpi.previous).toFixed(1)}{kpi.unit}
</span>
</div>
</div>
</div>
<div className="mt-3 w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className={`bg-${kpi.color}-500 h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min((kpi.current / kpi.target) * 100, 100)}%` }}
></div>
</div>
</Card>
))}
</div>
{/* Performance Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas de Rendimiento</h3>
<div className="space-y-3">
{performanceAlerts.map((alert) => (
<div key={alert.id} className={`p-4 rounded-lg border ${getAlertColor(alert.type)}`}>
<div className="flex items-start space-x-3">
{getAlertIcon(alert.type)}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{alert.title}</h4>
<Badge variant="gray">{alert.department}</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mt-1">{alert.description}</p>
<div className="flex items-center space-x-4 mt-2">
<span className="text-sm">
<strong>Actual:</strong> {alert.value}
</span>
<span className="text-sm">
<strong>Objetivo:</strong> {alert.target}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Department Performance */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Rendimiento por Departamento</h3>
<div className="space-y-4">
{departmentPerformance.map((dept) => (
<div key={dept.department} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<h4 className="font-medium text-[var(--text-primary)]">{dept.department}</h4>
<Badge variant="gray">{dept.employees} empleados</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-lg font-semibold text-[var(--text-primary)]">
{dept.efficiency}%
</span>
<div className="flex items-center">
{getTrendIcon(dept.trend)}
<span className={`text-sm ml-1 ${getTrendColor(dept.trend)}`}>
{Math.abs(dept.trend).toFixed(1)}%
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
{Object.entries(dept.metrics).map(([key, value]) => (
<div key={key}>
<p className="text-[var(--text-tertiary)] text-xs">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="font-medium">{value}</p>
</div>
))}
</div>
{dept.issues > 0 && (
<div className="mt-3 flex items-center text-sm text-[var(--color-warning)]">
<AlertCircle className="w-4 h-4 mr-1" />
{dept.issues} problema{dept.issues > 1 ? 's' : ''} detectado{dept.issues > 1 ? 's' : ''}
</div>
)}
</div>
))}
</div>
</Card>
{/* Hourly Productivity */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Eficiencia por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{productivityData.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.efficiency}%</div>
<div
className="w-full bg-[var(--color-info)]/50 rounded-t"
style={{
height: `${(data.efficiency / 100) * 200}px`,
minHeight: '8px',
backgroundColor: data.efficiency >= 90 ? '#10B981' : data.efficiency >= 80 ? '#F59E0B' : '#EF4444'
}}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
<div className="mt-4 flex justify-center space-x-6 text-xs">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded mr-1"></div>
<span>90% Excelente</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-yellow-500 rounded mr-1"></div>
<span>80-89% Bueno</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded mr-1"></div>
<span>&lt;80% Bajo</span>
</div>
</div>
</Card>
</div>
</div>
);
};
export default PerformanceAnalyticsPage;

View File

@@ -0,0 +1,403 @@
import React, { useState } from 'react';
import { Activity, Clock, Users, TrendingUp, Target, AlertCircle, Download, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PerformanceAnalyticsPage: React.FC = () => {
const [selectedTimeframe, setSelectedTimeframe] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('efficiency');
const performanceMetrics = {
overallEfficiency: 87.5,
productionTime: 4.2,
qualityScore: 92.1,
employeeProductivity: 89.3,
customerSatisfaction: 94.7,
resourceUtilization: 78.9,
};
const timeframes = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Trimestre' },
{ value: 'year', label: 'Año' },
];
const departmentPerformance = [
{
department: 'Producción',
efficiency: 91.2,
trend: 5.3,
issues: 2,
employees: 8,
metrics: {
avgBatchTime: '2.3h',
qualityRate: '94%',
wastePercentage: '3.1%'
}
},
{
department: 'Ventas',
efficiency: 88.7,
trend: -1.2,
issues: 1,
employees: 4,
metrics: {
avgServiceTime: '3.2min',
customerWaitTime: '2.1min',
salesPerHour: '€127'
}
},
{
department: 'Inventario',
efficiency: 82.4,
trend: 2.8,
issues: 3,
employees: 2,
metrics: {
stockAccuracy: '96.7%',
turnoverRate: '12.3',
wastageRate: '4.2%'
}
},
{
department: 'Administración',
efficiency: 94.1,
trend: 8.1,
issues: 0,
employees: 3,
metrics: {
responseTime: '1.2h',
taskCompletion: '98%',
documentAccuracy: '99.1%'
}
}
];
const kpiTrends = [
{
name: 'Eficiencia General',
current: 87.5,
target: 90.0,
previous: 84.2,
unit: '%',
color: 'blue'
},
{
name: 'Tiempo de Producción',
current: 4.2,
target: 4.0,
previous: 4.5,
unit: 'h',
color: 'green',
inverse: true
},
{
name: 'Satisfacción Cliente',
current: 94.7,
target: 95.0,
previous: 93.1,
unit: '%',
color: 'purple'
},
{
name: 'Utilización de Recursos',
current: 78.9,
target: 85.0,
previous: 76.3,
unit: '%',
color: 'orange'
}
];
const performanceAlerts = [
{
id: '1',
type: 'warning',
title: 'Eficiencia de Inventario Baja',
description: 'El departamento de inventario está por debajo del objetivo del 85%',
value: '82.4%',
target: '85%',
department: 'Inventario'
},
{
id: '2',
type: 'info',
title: 'Tiempo de Producción Mejorado',
description: 'El tiempo promedio de producción ha mejorado este mes',
value: '4.2h',
target: '4.0h',
department: 'Producción'
},
{
id: '3',
type: 'success',
title: 'Administración Supera Objetivos',
description: 'El departamento administrativo está funcionando por encima del objetivo',
value: '94.1%',
target: '90%',
department: 'Administración'
}
];
const productivityData = [
{ hour: '07:00', efficiency: 75, transactions: 12, employees: 3 },
{ hour: '08:00', efficiency: 82, transactions: 18, employees: 5 },
{ hour: '09:00', efficiency: 89, transactions: 28, employees: 6 },
{ hour: '10:00', efficiency: 91, transactions: 32, employees: 7 },
{ hour: '11:00', efficiency: 94, transactions: 38, employees: 8 },
{ hour: '12:00', efficiency: 96, transactions: 45, employees: 8 },
{ hour: '13:00', efficiency: 95, transactions: 42, employees: 8 },
{ hour: '14:00', efficiency: 88, transactions: 35, employees: 7 },
{ hour: '15:00', efficiency: 85, transactions: 28, employees: 6 },
{ hour: '16:00', efficiency: 83, transactions: 25, employees: 5 },
{ hour: '17:00', efficiency: 87, transactions: 31, employees: 6 },
{ hour: '18:00', efficiency: 90, transactions: 38, employees: 7 },
{ hour: '19:00', efficiency: 86, transactions: 29, employees: 5 },
{ hour: '20:00', efficiency: 78, transactions: 18, employees: 3 },
];
const getTrendIcon = (trend: number) => {
if (trend > 0) {
return <TrendingUp className="w-4 h-4 text-green-600" />;
} else {
return <TrendingUp className="w-4 h-4 text-red-600 transform rotate-180" />;
}
};
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-green-600' : 'text-red-600';
};
const getPerformanceColor = (value: number, target: number, inverse = false) => {
const comparison = inverse ? value < target : value >= target;
return comparison ? 'text-green-600' : value >= target * 0.9 ? 'text-yellow-600' : 'text-red-600';
};
const getAlertIcon = (type: string) => {
switch (type) {
case 'warning':
return <AlertCircle className="w-5 h-5 text-yellow-600" />;
case 'success':
return <TrendingUp className="w-5 h-5 text-green-600" />;
default:
return <Activity className="w-5 h-5 text-blue-600" />;
}
};
const getAlertColor = (type: string) => {
switch (type) {
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'success':
return 'bg-green-50 border-green-200';
default:
return 'bg-blue-50 border-blue-200';
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea la eficiencia operativa y el rendimiento de todos los departamentos"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Configurar Alertas
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{timeframes.map(timeframe => (
<option key={timeframe.value} value={timeframe.value}>{timeframe.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="efficiency">Eficiencia</option>
<option value="productivity">Productividad</option>
<option value="quality">Calidad</option>
<option value="satisfaction">Satisfacción</option>
</select>
</div>
</div>
</Card>
{/* KPI Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpiTrends.map((kpi) => (
<Card key={kpi.name} className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">{kpi.name}</h3>
<div className={`w-3 h-3 rounded-full bg-${kpi.color}-500`}></div>
</div>
<div className="flex items-end justify-between">
<div>
<p className={`text-2xl font-bold ${getPerformanceColor(kpi.current, kpi.target, kpi.inverse)}`}>
{kpi.current}{kpi.unit}
</p>
<p className="text-xs text-gray-500">
Objetivo: {kpi.target}{kpi.unit}
</p>
</div>
<div className="text-right">
<div className="flex items-center">
{getTrendIcon(kpi.current - kpi.previous)}
<span className={`text-sm ml-1 ${getTrendColor(kpi.current - kpi.previous)}`}>
{Math.abs(kpi.current - kpi.previous).toFixed(1)}{kpi.unit}
</span>
</div>
</div>
</div>
<div className="mt-3 w-full bg-gray-200 rounded-full h-2">
<div
className={`bg-${kpi.color}-500 h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min((kpi.current / kpi.target) * 100, 100)}%` }}
></div>
</div>
</Card>
))}
</div>
{/* Performance Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alertas de Rendimiento</h3>
<div className="space-y-3">
{performanceAlerts.map((alert) => (
<div key={alert.id} className={`p-4 rounded-lg border ${getAlertColor(alert.type)}`}>
<div className="flex items-start space-x-3">
{getAlertIcon(alert.type)}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">{alert.title}</h4>
<Badge variant="gray">{alert.department}</Badge>
</div>
<p className="text-sm text-gray-600 mt-1">{alert.description}</p>
<div className="flex items-center space-x-4 mt-2">
<span className="text-sm">
<strong>Actual:</strong> {alert.value}
</span>
<span className="text-sm">
<strong>Objetivo:</strong> {alert.target}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Department Performance */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Rendimiento por Departamento</h3>
<div className="space-y-4">
{departmentPerformance.map((dept) => (
<div key={dept.department} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<h4 className="font-medium text-gray-900">{dept.department}</h4>
<Badge variant="gray">{dept.employees} empleados</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-lg font-semibold text-gray-900">
{dept.efficiency}%
</span>
<div className="flex items-center">
{getTrendIcon(dept.trend)}
<span className={`text-sm ml-1 ${getTrendColor(dept.trend)}`}>
{Math.abs(dept.trend).toFixed(1)}%
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
{Object.entries(dept.metrics).map(([key, value]) => (
<div key={key}>
<p className="text-gray-500 text-xs">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="font-medium">{value}</p>
</div>
))}
</div>
{dept.issues > 0 && (
<div className="mt-3 flex items-center text-sm text-amber-600">
<AlertCircle className="w-4 h-4 mr-1" />
{dept.issues} problema{dept.issues > 1 ? 's' : ''} detectado{dept.issues > 1 ? 's' : ''}
</div>
)}
</div>
))}
</div>
</Card>
{/* Hourly Productivity */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Eficiencia por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{productivityData.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.efficiency}%</div>
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.efficiency / 100) * 200}px`,
minHeight: '8px',
backgroundColor: data.efficiency >= 90 ? '#10B981' : data.efficiency >= 80 ? '#F59E0B' : '#EF4444'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
<div className="mt-4 flex justify-center space-x-6 text-xs">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded mr-1"></div>
<span>≥90% Excelente</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-yellow-500 rounded mr-1"></div>
<span>80-89% Bueno</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded mr-1"></div>
<span>&lt;80% Bajo</span>
</div>
</div>
</Card>
</div>
</div>
);
};
export default PerformanceAnalyticsPage;

View File

@@ -0,0 +1 @@
export { default as PerformanceAnalyticsPage } from './PerformanceAnalyticsPage';

View File

@@ -0,0 +1,379 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, DollarSign, ShoppingCart, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { AnalyticsDashboard, ChartWidget, ReportsTable } from '../../../../components/domain/analytics';
const SalesAnalyticsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('revenue');
const [viewMode, setViewMode] = useState<'overview' | 'detailed'>('overview');
const salesMetrics = {
totalRevenue: 45678.90,
totalOrders: 1234,
averageOrderValue: 37.02,
customerCount: 856,
growthRate: 12.5,
conversionRate: 68.4,
};
const periods = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Este Trimestre' },
{ value: 'year', label: 'Este Año' },
];
const metrics = [
{ value: 'revenue', label: 'Ingresos' },
{ value: 'orders', label: 'Pedidos' },
{ value: 'customers', label: 'Clientes' },
{ value: 'products', label: 'Productos' },
];
const topProducts = [
{
id: '1',
name: 'Pan de Molde Integral',
revenue: 2250.50,
units: 245,
growth: 8.2,
category: 'Panes'
},
{
id: '2',
name: 'Croissants de Mantequilla',
revenue: 1890.75,
units: 412,
growth: 15.4,
category: 'Bollería'
},
{
id: '3',
name: 'Tarta de Chocolate',
revenue: 1675.00,
units: 67,
growth: -2.1,
category: 'Tartas'
},
{
id: '4',
name: 'Empanadas Variadas',
revenue: 1425.25,
units: 285,
growth: 22.8,
category: 'Salados'
},
{
id: '5',
name: 'Magdalenas',
revenue: 1180.50,
units: 394,
growth: 5.7,
category: 'Bollería'
},
];
const salesByHour = [
{ hour: '07:00', sales: 145, orders: 12 },
{ hour: '08:00', sales: 289, orders: 18 },
{ hour: '09:00', sales: 425, orders: 28 },
{ hour: '10:00', sales: 380, orders: 24 },
{ hour: '11:00', sales: 520, orders: 31 },
{ hour: '12:00', sales: 675, orders: 42 },
{ hour: '13:00', sales: 720, orders: 45 },
{ hour: '14:00', sales: 580, orders: 35 },
{ hour: '15:00', sales: 420, orders: 28 },
{ hour: '16:00', sales: 350, orders: 22 },
{ hour: '17:00', sales: 480, orders: 31 },
{ hour: '18:00', sales: 620, orders: 38 },
{ hour: '19:00', sales: 450, orders: 29 },
{ hour: '20:00', sales: 280, orders: 18 },
];
const customerSegments = [
{ segment: 'Clientes Frecuentes', count: 123, revenue: 15678, percentage: 34.3 },
{ segment: 'Clientes Regulares', count: 245, revenue: 18950, percentage: 41.5 },
{ segment: 'Clientes Ocasionales', count: 356, revenue: 8760, percentage: 19.2 },
{ segment: 'Clientes Nuevos', count: 132, revenue: 2290, percentage: 5.0 },
];
const paymentMethods = [
{ method: 'Tarjeta', count: 567, revenue: 28450, percentage: 62.3 },
{ method: 'Efectivo', count: 445, revenue: 13890, percentage: 30.4 },
{ method: 'Transferencia', count: 178, revenue: 2890, percentage: 6.3 },
{ method: 'Otros', count: 44, revenue: 448, percentage: 1.0 },
];
const getGrowthBadge = (growth: number) => {
if (growth > 0) {
return <Badge variant="green">+{growth.toFixed(1)}%</Badge>;
} else if (growth < 0) {
return <Badge variant="red">{growth.toFixed(1)}%</Badge>;
} else {
return <Badge variant="gray">0%</Badge>;
}
};
const getGrowthColor = (growth: number) => {
return growth >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Ventas"
description="Análisis detallado del rendimiento de ventas y tendencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{metrics.map(metric => (
<option key={metric.value} value={metric.value}>{metric.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Vista</label>
<div className="flex rounded-md border border-[var(--border-secondary)]">
<button
onClick={() => setViewMode('overview')}
className={`px-3 py-2 text-sm ${viewMode === 'overview' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-l-md`}
>
General
</button>
<button
onClick={() => setViewMode('detailed')}
className={`px-3 py-2 text-sm ${viewMode === 'detailed' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-r-md border-l`}
>
Detallado
</button>
</div>
</div>
</div>
</div>
</Card>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ingresos Totales</p>
<p className="text-2xl font-bold text-[var(--color-success)]">{salesMetrics.totalRevenue.toLocaleString()}</p>
</div>
<DollarSign className="h-8 w-8 text-[var(--color-success)]" />
</div>
<div className="mt-2">
{getGrowthBadge(salesMetrics.growthRate)}
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Pedidos</p>
<p className="text-2xl font-bold text-[var(--color-info)]">{salesMetrics.totalOrders.toLocaleString()}</p>
</div>
<ShoppingCart className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Valor Promedio</p>
<p className="text-2xl font-bold text-purple-600">{salesMetrics.averageOrderValue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Clientes</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">{salesMetrics.customerCount}</p>
</div>
<div className="h-8 w-8 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tasa Crecimiento</p>
<p className={`text-2xl font-bold ${getGrowthColor(salesMetrics.growthRate)}`}>
{salesMetrics.growthRate > 0 ? '+' : ''}{salesMetrics.growthRate.toFixed(1)}%
</p>
</div>
<TrendingUp className={`h-8 w-8 ${getGrowthColor(salesMetrics.growthRate)}`} />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Conversión</p>
<p className="text-2xl font-bold text-indigo-600">{salesMetrics.conversionRate}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
</div>
{viewMode === 'overview' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sales by Hour Chart */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Ventas por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{salesByHour.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div
className="w-full bg-[var(--color-info)]/50 rounded-t"
style={{
height: `${(data.sales / Math.max(...salesByHour.map(d => d.sales))) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Top Products */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Productos Más Vendidos</h3>
<div className="space-y-3">
{topProducts.slice(0, 5).map((product, index) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-[var(--text-tertiary)] w-6">{index + 1}.</span>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{product.name}</p>
<p className="text-xs text-[var(--text-tertiary)]">{product.category} {product.units} unidades</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">{product.revenue.toLocaleString()}</p>
{getGrowthBadge(product.growth)}
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-[var(--text-primary)]">{segment.segment}</span>
<span className="text-sm text-[var(--text-secondary)]">{segment.percentage}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${segment.percentage}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-[var(--text-tertiary)]">
<span>{segment.count} clientes</span>
<span>{segment.revenue.toLocaleString()}</span>
</div>
</div>
))}
</div>
</Card>
{/* Payment Methods */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Métodos de Pago</h3>
<div className="space-y-3">
{paymentMethods.map((method, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-[var(--color-info)]/50 rounded-full"></div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{method.method}</p>
<p className="text-xs text-[var(--text-tertiary)]">{method.count} transacciones</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">{method.revenue.toLocaleString()}</p>
<p className="text-xs text-[var(--text-tertiary)]">{method.percentage}%</p>
</div>
</div>
))}
</div>
</Card>
</div>
) : (
<div className="space-y-6">
{/* Detailed Analytics Dashboard */}
<AnalyticsDashboard />
{/* Detailed Reports Table */}
<ReportsTable />
</div>
)}
</div>
);
};
export default SalesAnalyticsPage;

View File

@@ -0,0 +1,379 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, DollarSign, ShoppingCart, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { AnalyticsDashboard, ChartWidget, ReportsTable } from '../../../../components/domain/analytics';
const SalesAnalyticsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('revenue');
const [viewMode, setViewMode] = useState<'overview' | 'detailed'>('overview');
const salesMetrics = {
totalRevenue: 45678.90,
totalOrders: 1234,
averageOrderValue: 37.02,
customerCount: 856,
growthRate: 12.5,
conversionRate: 68.4,
};
const periods = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
{ value: 'quarter', label: 'Este Trimestre' },
{ value: 'year', label: 'Este Año' },
];
const metrics = [
{ value: 'revenue', label: 'Ingresos' },
{ value: 'orders', label: 'Pedidos' },
{ value: 'customers', label: 'Clientes' },
{ value: 'products', label: 'Productos' },
];
const topProducts = [
{
id: '1',
name: 'Pan de Molde Integral',
revenue: 2250.50,
units: 245,
growth: 8.2,
category: 'Panes'
},
{
id: '2',
name: 'Croissants de Mantequilla',
revenue: 1890.75,
units: 412,
growth: 15.4,
category: 'Bollería'
},
{
id: '3',
name: 'Tarta de Chocolate',
revenue: 1675.00,
units: 67,
growth: -2.1,
category: 'Tartas'
},
{
id: '4',
name: 'Empanadas Variadas',
revenue: 1425.25,
units: 285,
growth: 22.8,
category: 'Salados'
},
{
id: '5',
name: 'Magdalenas',
revenue: 1180.50,
units: 394,
growth: 5.7,
category: 'Bollería'
},
];
const salesByHour = [
{ hour: '07:00', sales: 145, orders: 12 },
{ hour: '08:00', sales: 289, orders: 18 },
{ hour: '09:00', sales: 425, orders: 28 },
{ hour: '10:00', sales: 380, orders: 24 },
{ hour: '11:00', sales: 520, orders: 31 },
{ hour: '12:00', sales: 675, orders: 42 },
{ hour: '13:00', sales: 720, orders: 45 },
{ hour: '14:00', sales: 580, orders: 35 },
{ hour: '15:00', sales: 420, orders: 28 },
{ hour: '16:00', sales: 350, orders: 22 },
{ hour: '17:00', sales: 480, orders: 31 },
{ hour: '18:00', sales: 620, orders: 38 },
{ hour: '19:00', sales: 450, orders: 29 },
{ hour: '20:00', sales: 280, orders: 18 },
];
const customerSegments = [
{ segment: 'Clientes Frecuentes', count: 123, revenue: 15678, percentage: 34.3 },
{ segment: 'Clientes Regulares', count: 245, revenue: 18950, percentage: 41.5 },
{ segment: 'Clientes Ocasionales', count: 356, revenue: 8760, percentage: 19.2 },
{ segment: 'Clientes Nuevos', count: 132, revenue: 2290, percentage: 5.0 },
];
const paymentMethods = [
{ method: 'Tarjeta', count: 567, revenue: 28450, percentage: 62.3 },
{ method: 'Efectivo', count: 445, revenue: 13890, percentage: 30.4 },
{ method: 'Transferencia', count: 178, revenue: 2890, percentage: 6.3 },
{ method: 'Otros', count: 44, revenue: 448, percentage: 1.0 },
];
const getGrowthBadge = (growth: number) => {
if (growth > 0) {
return <Badge variant="green">+{growth.toFixed(1)}%</Badge>;
} else if (growth < 0) {
return <Badge variant="red">{growth.toFixed(1)}%</Badge>;
} else {
return <Badge variant="gray">0%</Badge>;
}
};
const getGrowthColor = (growth: number) => {
return growth >= 0 ? 'text-green-600' : 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Ventas"
description="Análisis detallado del rendimiento de ventas y tendencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{periods.map(period => (
<option key={period.value} value={period.value}>{period.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
{metrics.map(metric => (
<option key={metric.value} value={metric.value}>{metric.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Vista</label>
<div className="flex rounded-md border border-gray-300">
<button
onClick={() => setViewMode('overview')}
className={`px-3 py-2 text-sm ${viewMode === 'overview' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-l-md`}
>
General
</button>
<button
onClick={() => setViewMode('detailed')}
className={`px-3 py-2 text-sm ${viewMode === 'detailed' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'} rounded-r-md border-l`}
>
Detallado
</button>
</div>
</div>
</div>
</div>
</Card>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ingresos Totales</p>
<p className="text-2xl font-bold text-green-600">€{salesMetrics.totalRevenue.toLocaleString()}</p>
</div>
<DollarSign className="h-8 w-8 text-green-600" />
</div>
<div className="mt-2">
{getGrowthBadge(salesMetrics.growthRate)}
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-blue-600">{salesMetrics.totalOrders.toLocaleString()}</p>
</div>
<ShoppingCart className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Valor Promedio</p>
<p className="text-2xl font-bold text-purple-600">€{salesMetrics.averageOrderValue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Clientes</p>
<p className="text-2xl font-bold text-orange-600">{salesMetrics.customerCount}</p>
</div>
<div className="h-8 w-8 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Tasa Crecimiento</p>
<p className={`text-2xl font-bold ${getGrowthColor(salesMetrics.growthRate)}`}>
{salesMetrics.growthRate > 0 ? '+' : ''}{salesMetrics.growthRate.toFixed(1)}%
</p>
</div>
<TrendingUp className={`h-8 w-8 ${getGrowthColor(salesMetrics.growthRate)}`} />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Conversión</p>
<p className="text-2xl font-bold text-indigo-600">{salesMetrics.conversionRate}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
</div>
{viewMode === 'overview' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sales by Hour Chart */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ventas por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{salesByHour.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.sales / Math.max(...salesByHour.map(d => d.sales))) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Top Products */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Productos Más Vendidos</h3>
<div className="space-y-3">
{topProducts.slice(0, 5).map((product, index) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-gray-500 w-6">{index + 1}.</span>
<div>
<p className="text-sm font-medium text-gray-900">{product.name}</p>
<p className="text-xs text-gray-500">{product.category} • {product.units} unidades</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">€{product.revenue.toLocaleString()}</p>
{getGrowthBadge(product.growth)}
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-900">{segment.segment}</span>
<span className="text-sm text-gray-600">{segment.percentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${segment.percentage}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{segment.count} clientes</span>
<span>€{segment.revenue.toLocaleString()}</span>
</div>
</div>
))}
</div>
</Card>
{/* Payment Methods */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Métodos de Pago</h3>
<div className="space-y-3">
{paymentMethods.map((method, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<div>
<p className="text-sm font-medium text-gray-900">{method.method}</p>
<p className="text-xs text-gray-500">{method.count} transacciones</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">€{method.revenue.toLocaleString()}</p>
<p className="text-xs text-gray-500">{method.percentage}%</p>
</div>
</div>
))}
</div>
</Card>
</div>
) : (
<div className="space-y-6">
{/* Detailed Analytics Dashboard */}
<AnalyticsDashboard />
{/* Detailed Reports Table */}
<ReportsTable />
</div>
)}
</div>
);
};
export default SalesAnalyticsPage;

View File

@@ -0,0 +1 @@
export { default as SalesAnalyticsPage } from './SalesAnalyticsPage';

View File

@@ -0,0 +1,414 @@
import React, { useState } from 'react';
import { Bell, AlertTriangle, AlertCircle, CheckCircle, Clock, Settings, Filter, Search } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const AlertsPage: React.FC = () => {
const [selectedFilter, setSelectedFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [selectedAlert, setSelectedAlert] = useState<string | null>(null);
const alerts = [
{
id: '1',
type: 'critical',
category: 'inventory',
title: 'Stock Crítico - Harina de Trigo',
message: 'Quedan solo 5kg de harina de trigo. El stock mínimo es de 20kg.',
timestamp: '2024-01-26 10:30:00',
read: false,
actionRequired: true,
priority: 'high',
source: 'Sistema de Inventario',
details: {
currentStock: '5kg',
minimumStock: '20kg',
supplier: 'Molinos del Sur',
estimatedDepletion: '1 día'
}
},
{
id: '2',
type: 'warning',
category: 'production',
title: 'Retraso en Producción',
message: 'El lote de croissants #CR-024 lleva 45 minutos de retraso.',
timestamp: '2024-01-26 09:15:00',
read: false,
actionRequired: true,
priority: 'medium',
source: 'Control de Producción',
details: {
batchId: 'CR-024',
expectedTime: '2.5h',
actualTime: '3.25h',
delayReason: 'Problema con el horno #2'
}
},
{
id: '3',
type: 'info',
category: 'sales',
title: 'Pico de Ventas Detectado',
message: 'Las ventas han aumentado un 35% en la última hora.',
timestamp: '2024-01-26 08:45:00',
read: true,
actionRequired: false,
priority: 'low',
source: 'Sistema de Ventas',
details: {
increase: '35%',
period: 'Última hora',
topProducts: ['Croissants', 'Pan Integral', 'Empanadas'],
expectedRevenue: '€320'
}
},
{
id: '4',
type: 'success',
category: 'quality',
title: 'Control de Calidad Completado',
message: 'Lote de pan integral #PI-156 aprobado con puntuación de 9.2/10.',
timestamp: '2024-01-26 07:30:00',
read: true,
actionRequired: false,
priority: 'low',
source: 'Control de Calidad',
details: {
batchId: 'PI-156',
score: '9.2/10',
inspector: 'María González',
testsPassed: '15/15'
}
},
{
id: '5',
type: 'critical',
category: 'equipment',
title: 'Fallo del Horno Principal',
message: 'El horno #1 ha presentado una falla en el sistema de temperatura.',
timestamp: '2024-01-25 16:20:00',
read: false,
actionRequired: true,
priority: 'high',
source: 'Monitoreo de Equipos',
details: {
equipment: 'Horno #1',
error: 'Sistema de temperatura',
impact: 'Producción reducida 50%',
technician: 'Pendiente'
}
},
{
id: '6',
type: 'warning',
category: 'staff',
title: 'Ausentismo del Personal',
message: '2 empleados del turno matutino no se han presentado.',
timestamp: '2024-01-25 07:00:00',
read: true,
actionRequired: true,
priority: 'medium',
source: 'Gestión de Personal',
details: {
absentEmployees: ['Juan Pérez', 'Ana García'],
shift: 'Matutino',
coverage: '75%',
replacement: 'Solicitada'
}
}
];
const alertStats = {
total: alerts.length,
unread: alerts.filter(a => !a.read).length,
critical: alerts.filter(a => a.type === 'critical').length,
actionRequired: alerts.filter(a => a.actionRequired).length
};
const categories = [
{ value: 'all', label: 'Todas', count: alerts.length },
{ value: 'inventory', label: 'Inventario', count: alerts.filter(a => a.category === 'inventory').length },
{ value: 'production', label: 'Producción', count: alerts.filter(a => a.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: alerts.filter(a => a.category === 'sales').length },
{ value: 'quality', label: 'Calidad', count: alerts.filter(a => a.category === 'quality').length },
{ value: 'equipment', label: 'Equipos', count: alerts.filter(a => a.category === 'equipment').length },
{ value: 'staff', label: 'Personal', count: alerts.filter(a => a.category === 'staff').length }
];
const getAlertIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'critical': return <AlertTriangle {...iconProps} />;
case 'warning': return <AlertCircle {...iconProps} />;
case 'success': return <CheckCircle {...iconProps} />;
default: return <Bell {...iconProps} />;
}
};
const getAlertColor = (type: string) => {
switch (type) {
case 'critical': return 'red';
case 'warning': return 'yellow';
case 'success': return 'green';
default: return 'blue';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'bg-[var(--color-error)]/10 text-[var(--color-error)]';
case 'medium': return 'bg-yellow-100 text-yellow-800';
case 'low': return 'bg-[var(--color-success)]/10 text-[var(--color-success)]';
default: return 'bg-[var(--bg-tertiary)] text-[var(--text-primary)]';
}
};
const filteredAlerts = alerts.filter(alert => {
const matchesFilter = selectedFilter === 'all' || alert.category === selectedFilter;
const matchesSearch = alert.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
alert.message.toLowerCase().includes(searchTerm.toLowerCase());
return matchesFilter && matchesSearch;
});
const handleMarkAsRead = (alertId: string) => {
// Handle mark as read logic
console.log('Marking alert as read:', alertId);
};
const handleDismissAlert = (alertId: string) => {
// Handle dismiss alert logic
console.log('Dismissing alert:', alertId);
};
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const alertTime = new Date(timestamp);
const diffInMs = now.getTime() - alertTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInMins = Math.floor(diffInMs / (1000 * 60));
if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${diffInMins}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Alertas y Notificaciones"
description="Gestiona y supervisa todas las alertas del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Configurar
</Button>
<Button>
Marcar Todas Leídas
</Button>
</div>
}
/>
{/* Alert Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Alertas</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{alertStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Sin Leer</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{alertStats.unread}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Críticas</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{alertStats.critical}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Acción Requerida</p>
<p className="text-3xl font-bold text-purple-600">{alertStats.actionRequired}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Settings className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar alertas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{categories.map(category => (
<option key={category.value} value={category.value}>
{category.label} ({category.count})
</option>
))}
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
</div>
</div>
</Card>
{/* Alerts List */}
<div className="space-y-4">
{filteredAlerts.map((alert) => (
<Card
key={alert.id}
className={`p-6 cursor-pointer transition-colors ${
!alert.read ? 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20' : ''
} ${selectedAlert === alert.id ? 'ring-2 ring-blue-500' : ''}`}
onClick={() => setSelectedAlert(selectedAlert === alert.id ? null : alert.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getAlertColor(alert.type)}-100`}>
{getAlertIcon(alert.type)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{alert.title}</h3>
{!alert.read && (
<div className="w-2 h-2 bg-blue-600 rounded-full"></div>
)}
<Badge variant={getAlertColor(alert.type)}>
{alert.type === 'critical' ? 'Crítica' :
alert.type === 'warning' ? 'Advertencia' :
alert.type === 'success' ? 'Éxito' : 'Info'}
</Badge>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(alert.priority)}`}>
Prioridad {alert.priority === 'high' ? 'Alta' : alert.priority === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
<p className="text-[var(--text-secondary)] mb-3">{alert.message}</p>
<div className="flex items-center space-x-4 text-sm text-[var(--text-tertiary)]">
<span>{formatTimeAgo(alert.timestamp)}</span>
<span></span>
<span>{alert.source}</span>
{alert.actionRequired && (
<>
<span></span>
<Badge variant="yellow">Acción Requerida</Badge>
</>
)}
</div>
</div>
</div>
<div className="flex space-x-2">
{!alert.read && (
<Button size="sm" variant="outline" onClick={(e) => {
e.stopPropagation();
handleMarkAsRead(alert.id);
}}>
Marcar Leída
</Button>
)}
<Button size="sm" variant="outline" onClick={(e) => {
e.stopPropagation();
handleDismissAlert(alert.id);
}}>
Descartar
</Button>
</div>
</div>
{/* Alert Details - Expandible */}
{selectedAlert === alert.id && (
<div className="mt-4 pt-4 border-t border-[var(--border-primary)]">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">Detalles de la Alerta</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.entries(alert.details).map(([key, value]) => (
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{value}</p>
</div>
))}
</div>
{alert.actionRequired && (
<div className="mt-4 flex space-x-2">
<Button size="sm">
Tomar Acción
</Button>
<Button size="sm" variant="outline">
Escalar
</Button>
<Button size="sm" variant="outline">
Programar
</Button>
</div>
)}
</div>
)}
</Card>
))}
</div>
{filteredAlerts.length === 0 && (
<Card className="p-12 text-center">
<Bell className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay alertas</h3>
<p className="text-[var(--text-secondary)]">
No se encontraron alertas que coincidan con los filtros seleccionados.
</p>
</Card>
)}
</div>
);
};
export default AlertsPage;

View File

@@ -0,0 +1,414 @@
import React, { useState } from 'react';
import { Bell, AlertTriangle, AlertCircle, CheckCircle, Clock, Settings, Filter, Search } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const AlertsPage: React.FC = () => {
const [selectedFilter, setSelectedFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [selectedAlert, setSelectedAlert] = useState<string | null>(null);
const alerts = [
{
id: '1',
type: 'critical',
category: 'inventory',
title: 'Stock Crítico - Harina de Trigo',
message: 'Quedan solo 5kg de harina de trigo. El stock mínimo es de 20kg.',
timestamp: '2024-01-26 10:30:00',
read: false,
actionRequired: true,
priority: 'high',
source: 'Sistema de Inventario',
details: {
currentStock: '5kg',
minimumStock: '20kg',
supplier: 'Molinos del Sur',
estimatedDepletion: '1 día'
}
},
{
id: '2',
type: 'warning',
category: 'production',
title: 'Retraso en Producción',
message: 'El lote de croissants #CR-024 lleva 45 minutos de retraso.',
timestamp: '2024-01-26 09:15:00',
read: false,
actionRequired: true,
priority: 'medium',
source: 'Control de Producción',
details: {
batchId: 'CR-024',
expectedTime: '2.5h',
actualTime: '3.25h',
delayReason: 'Problema con el horno #2'
}
},
{
id: '3',
type: 'info',
category: 'sales',
title: 'Pico de Ventas Detectado',
message: 'Las ventas han aumentado un 35% en la última hora.',
timestamp: '2024-01-26 08:45:00',
read: true,
actionRequired: false,
priority: 'low',
source: 'Sistema de Ventas',
details: {
increase: '35%',
period: 'Última hora',
topProducts: ['Croissants', 'Pan Integral', 'Empanadas'],
expectedRevenue: '€320'
}
},
{
id: '4',
type: 'success',
category: 'quality',
title: 'Control de Calidad Completado',
message: 'Lote de pan integral #PI-156 aprobado con puntuación de 9.2/10.',
timestamp: '2024-01-26 07:30:00',
read: true,
actionRequired: false,
priority: 'low',
source: 'Control de Calidad',
details: {
batchId: 'PI-156',
score: '9.2/10',
inspector: 'María González',
testsPassed: '15/15'
}
},
{
id: '5',
type: 'critical',
category: 'equipment',
title: 'Fallo del Horno Principal',
message: 'El horno #1 ha presentado una falla en el sistema de temperatura.',
timestamp: '2024-01-25 16:20:00',
read: false,
actionRequired: true,
priority: 'high',
source: 'Monitoreo de Equipos',
details: {
equipment: 'Horno #1',
error: 'Sistema de temperatura',
impact: 'Producción reducida 50%',
technician: 'Pendiente'
}
},
{
id: '6',
type: 'warning',
category: 'staff',
title: 'Ausentismo del Personal',
message: '2 empleados del turno matutino no se han presentado.',
timestamp: '2024-01-25 07:00:00',
read: true,
actionRequired: true,
priority: 'medium',
source: 'Gestión de Personal',
details: {
absentEmployees: ['Juan Pérez', 'Ana García'],
shift: 'Matutino',
coverage: '75%',
replacement: 'Solicitada'
}
}
];
const alertStats = {
total: alerts.length,
unread: alerts.filter(a => !a.read).length,
critical: alerts.filter(a => a.type === 'critical').length,
actionRequired: alerts.filter(a => a.actionRequired).length
};
const categories = [
{ value: 'all', label: 'Todas', count: alerts.length },
{ value: 'inventory', label: 'Inventario', count: alerts.filter(a => a.category === 'inventory').length },
{ value: 'production', label: 'Producción', count: alerts.filter(a => a.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: alerts.filter(a => a.category === 'sales').length },
{ value: 'quality', label: 'Calidad', count: alerts.filter(a => a.category === 'quality').length },
{ value: 'equipment', label: 'Equipos', count: alerts.filter(a => a.category === 'equipment').length },
{ value: 'staff', label: 'Personal', count: alerts.filter(a => a.category === 'staff').length }
];
const getAlertIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'critical': return <AlertTriangle {...iconProps} />;
case 'warning': return <AlertCircle {...iconProps} />;
case 'success': return <CheckCircle {...iconProps} />;
default: return <Bell {...iconProps} />;
}
};
const getAlertColor = (type: string) => {
switch (type) {
case 'critical': return 'red';
case 'warning': return 'yellow';
case 'success': return 'green';
default: return 'blue';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'bg-red-100 text-red-800';
case 'medium': return 'bg-yellow-100 text-yellow-800';
case 'low': return 'bg-green-100 text-green-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const filteredAlerts = alerts.filter(alert => {
const matchesFilter = selectedFilter === 'all' || alert.category === selectedFilter;
const matchesSearch = alert.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
alert.message.toLowerCase().includes(searchTerm.toLowerCase());
return matchesFilter && matchesSearch;
});
const handleMarkAsRead = (alertId: string) => {
// Handle mark as read logic
console.log('Marking alert as read:', alertId);
};
const handleDismissAlert = (alertId: string) => {
// Handle dismiss alert logic
console.log('Dismissing alert:', alertId);
};
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const alertTime = new Date(timestamp);
const diffInMs = now.getTime() - alertTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInMins = Math.floor(diffInMs / (1000 * 60));
if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${diffInMins}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Alertas y Notificaciones"
description="Gestiona y supervisa todas las alertas del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Configurar
</Button>
<Button>
Marcar Todas Leídas
</Button>
</div>
}
/>
{/* Alert Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Alertas</p>
<p className="text-3xl font-bold text-gray-900">{alertStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Sin Leer</p>
<p className="text-3xl font-bold text-orange-600">{alertStats.unread}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Críticas</p>
<p className="text-3xl font-bold text-red-600">{alertStats.critical}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Acción Requerida</p>
<p className="text-3xl font-bold text-purple-600">{alertStats.actionRequired}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Settings className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar alertas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{categories.map(category => (
<option key={category.value} value={category.value}>
{category.label} ({category.count})
</option>
))}
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
</div>
</div>
</Card>
{/* Alerts List */}
<div className="space-y-4">
{filteredAlerts.map((alert) => (
<Card
key={alert.id}
className={`p-6 cursor-pointer transition-colors ${
!alert.read ? 'bg-blue-50 border-blue-200' : ''
} ${selectedAlert === alert.id ? 'ring-2 ring-blue-500' : ''}`}
onClick={() => setSelectedAlert(selectedAlert === alert.id ? null : alert.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getAlertColor(alert.type)}-100`}>
{getAlertIcon(alert.type)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{alert.title}</h3>
{!alert.read && (
<div className="w-2 h-2 bg-blue-600 rounded-full"></div>
)}
<Badge variant={getAlertColor(alert.type)}>
{alert.type === 'critical' ? 'Crítica' :
alert.type === 'warning' ? 'Advertencia' :
alert.type === 'success' ? 'Éxito' : 'Info'}
</Badge>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(alert.priority)}`}>
Prioridad {alert.priority === 'high' ? 'Alta' : alert.priority === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
<p className="text-gray-700 mb-3">{alert.message}</p>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<span>{formatTimeAgo(alert.timestamp)}</span>
<span>•</span>
<span>{alert.source}</span>
{alert.actionRequired && (
<>
<span>•</span>
<Badge variant="yellow">Acción Requerida</Badge>
</>
)}
</div>
</div>
</div>
<div className="flex space-x-2">
{!alert.read && (
<Button size="sm" variant="outline" onClick={(e) => {
e.stopPropagation();
handleMarkAsRead(alert.id);
}}>
Marcar Leída
</Button>
)}
<Button size="sm" variant="outline" onClick={(e) => {
e.stopPropagation();
handleDismissAlert(alert.id);
}}>
Descartar
</Button>
</div>
</div>
{/* Alert Details - Expandible */}
{selectedAlert === alert.id && (
<div className="mt-4 pt-4 border-t border-gray-200">
<h4 className="text-sm font-medium text-gray-900 mb-3">Detalles de la Alerta</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.entries(alert.details).map(([key, value]) => (
<div key={key} className="bg-gray-50 p-3 rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-gray-900">{value}</p>
</div>
))}
</div>
{alert.actionRequired && (
<div className="mt-4 flex space-x-2">
<Button size="sm">
Tomar Acción
</Button>
<Button size="sm" variant="outline">
Escalar
</Button>
<Button size="sm" variant="outline">
Programar
</Button>
</div>
)}
</div>
)}
</Card>
))}
</div>
{filteredAlerts.length === 0 && (
<Card className="p-12 text-center">
<Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay alertas</h3>
<p className="text-gray-600">
No se encontraron alertas que coincidan con los filtros seleccionados.
</p>
</Card>
)}
</div>
);
};
export default AlertsPage;

View File

@@ -0,0 +1 @@
export { default as AlertsPage } from './AlertsPage';

View File

@@ -0,0 +1,402 @@
import React, { useState } from 'react';
import { Bell, Mail, MessageSquare, Settings, Archive, Trash2, CheckCircle, Filter } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const NotificationsPage: React.FC = () => {
const [selectedTab, setSelectedTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [selectedNotifications, setSelectedNotifications] = useState<string[]>([]);
const notifications = [
{
id: '1',
type: 'system',
channel: 'app',
title: 'Actualización del Sistema',
message: 'Nueva versión 2.1.0 disponible con mejoras en el módulo de inventario',
timestamp: '2024-01-26 10:15:00',
read: false,
priority: 'medium',
category: 'update',
sender: 'Sistema',
actions: ['Ver Detalles', 'Instalar Después']
},
{
id: '2',
type: 'order',
channel: 'email',
title: 'Nuevo Pedido Recibido',
message: 'Pedido #ORD-456 por €127.50 de Panadería Central',
timestamp: '2024-01-26 09:30:00',
read: false,
priority: 'high',
category: 'sales',
sender: 'Sistema de Ventas',
actions: ['Ver Pedido', 'Procesar']
},
{
id: '3',
type: 'inventory',
channel: 'sms',
title: 'Stock Repuesto',
message: 'Se ha repuesto el stock de azúcar. Nivel actual: 50kg',
timestamp: '2024-01-26 08:45:00',
read: true,
priority: 'low',
category: 'inventory',
sender: 'Gestión de Inventario',
actions: ['Ver Inventario']
},
{
id: '4',
type: 'reminder',
channel: 'app',
title: 'Recordatorio de Mantenimiento',
message: 'El horno #2 requiere mantenimiento preventivo programado para mañana',
timestamp: '2024-01-26 07:00:00',
read: true,
priority: 'medium',
category: 'maintenance',
sender: 'Sistema de Mantenimiento',
actions: ['Programar', 'Posponer']
},
{
id: '5',
type: 'customer',
channel: 'app',
title: 'Reseña de Cliente',
message: 'Nueva reseña de 5 estrellas de María L.: "Excelente calidad y servicio"',
timestamp: '2024-01-25 19:20:00',
read: false,
priority: 'low',
category: 'feedback',
sender: 'Sistema de Reseñas',
actions: ['Ver Reseña', 'Responder']
},
{
id: '6',
type: 'promotion',
channel: 'email',
title: 'Campaña de Marketing Completada',
message: 'La campaña "Desayunos Especiales" ha terminado con 340 interacciones',
timestamp: '2024-01-25 16:30:00',
read: true,
priority: 'low',
category: 'marketing',
sender: 'Sistema de Marketing',
actions: ['Ver Resultados']
}
];
const notificationStats = {
total: notifications.length,
unread: notifications.filter(n => !n.read).length,
high: notifications.filter(n => n.priority === 'high').length,
today: notifications.filter(n =>
new Date(n.timestamp).toDateString() === new Date().toDateString()
).length
};
const tabs = [
{ id: 'all', label: 'Todas', count: notifications.length },
{ id: 'unread', label: 'Sin Leer', count: notificationStats.unread },
{ id: 'system', label: 'Sistema', count: notifications.filter(n => n.type === 'system').length },
{ id: 'order', label: 'Pedidos', count: notifications.filter(n => n.type === 'order').length },
{ id: 'inventory', label: 'Inventario', count: notifications.filter(n => n.type === 'inventory').length }
];
const getNotificationIcon = (type: string, channel: string) => {
const iconProps = { className: "w-5 h-5" };
if (channel === 'email') return <Mail {...iconProps} />;
if (channel === 'sms') return <MessageSquare {...iconProps} />;
switch (type) {
case 'system': return <Settings {...iconProps} />;
case 'order': return <Bell {...iconProps} />;
default: return <Bell {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getChannelBadge = (channel: string) => {
const colors = {
app: 'blue',
email: 'purple',
sms: 'green'
};
return colors[channel as keyof typeof colors] || 'gray';
};
const filteredNotifications = notifications.filter(notification => {
let matchesTab = true;
if (selectedTab === 'unread') {
matchesTab = !notification.read;
} else if (selectedTab !== 'all') {
matchesTab = notification.type === selectedTab;
}
const matchesSearch = notification.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
notification.message.toLowerCase().includes(searchTerm.toLowerCase());
return matchesTab && matchesSearch;
});
const handleSelectNotification = (notificationId: string) => {
setSelectedNotifications(prev =>
prev.includes(notificationId)
? prev.filter(id => id !== notificationId)
: [...prev, notificationId]
);
};
const handleSelectAll = () => {
setSelectedNotifications(
selectedNotifications.length === filteredNotifications.length
? []
: filteredNotifications.map(n => n.id)
);
};
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const notificationTime = new Date(timestamp);
const diffInMs = now.getTime() - notificationTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return `hace ${diffInDays}d`;
} else if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Notificaciones"
description="Centro de notificaciones y mensajes del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Preferencias
</Button>
<Button>
Marcar Todas Leídas
</Button>
</div>
}
/>
{/* Notification Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{notificationStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Sin Leer</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{notificationStats.unread}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<CheckCircle className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Alta Prioridad</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{notificationStats.high}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Hoy</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{notificationStats.today}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
</div>
{/* Tabs and Search */}
<Card className="p-6">
<div className="flex flex-col space-y-4">
{/* Tabs */}
<div className="flex space-x-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setSelectedTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedTab === tab.id
? 'border-blue-600 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'
}`}
>
{tab.label} ({tab.count})
</button>
))}
</div>
{/* Search and Actions */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar notificaciones..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
{selectedNotifications.length > 0 && (
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<CheckCircle className="w-4 h-4 mr-2" />
Marcar Leídas ({selectedNotifications.length})
</Button>
<Button size="sm" variant="outline">
<Archive className="w-4 h-4 mr-2" />
Archivar
</Button>
<Button size="sm" variant="outline">
<Trash2 className="w-4 h-4 mr-2" />
Eliminar
</Button>
</div>
)}
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
</div>
</div>
</Card>
{/* Bulk Actions */}
{filteredNotifications.length > 0 && (
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedNotifications.length === filteredNotifications.length}
onChange={handleSelectAll}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm text-[var(--text-secondary)]">
Seleccionar todas ({filteredNotifications.length})
</span>
</label>
</div>
)}
{/* Notifications List */}
<div className="space-y-3">
{filteredNotifications.map((notification) => (
<Card
key={notification.id}
className={`p-4 transition-colors ${
!notification.read ? 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20' : ''
} ${selectedNotifications.includes(notification.id) ? 'ring-2 ring-blue-500' : ''}`}
>
<div className="flex items-start space-x-4">
<input
type="checkbox"
checked={selectedNotifications.includes(notification.id)}
onChange={() => handleSelectNotification(notification.id)}
className="rounded border-[var(--border-secondary)] mt-1"
/>
<div className={`p-2 rounded-lg bg-${getChannelBadge(notification.channel)}-100 mt-1`}>
{getNotificationIcon(notification.type, notification.channel)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-sm font-semibold text-[var(--text-primary)] truncate">
{notification.title}
</h3>
{!notification.read && (
<div className="w-2 h-2 bg-blue-600 rounded-full flex-shrink-0"></div>
)}
<Badge variant={getPriorityColor(notification.priority)}>
{notification.priority === 'high' ? 'Alta' :
notification.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<Badge variant={getChannelBadge(notification.channel)}>
{notification.channel.toUpperCase()}
</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">{notification.message}</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span>{formatTimeAgo(notification.timestamp)}</span>
<span></span>
<span>{notification.sender}</span>
</div>
<div className="flex space-x-2">
{notification.actions.map((action, index) => (
<Button key={index} size="sm" variant="outline" className="text-xs">
{action}
</Button>
))}
</div>
</div>
</div>
</div>
</Card>
))}
</div>
{filteredNotifications.length === 0 && (
<Card className="p-12 text-center">
<Bell className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay notificaciones</h3>
<p className="text-[var(--text-secondary)]">
No se encontraron notificaciones que coincidan con los filtros seleccionados.
</p>
</Card>
)}
</div>
);
};
export default NotificationsPage;

View File

@@ -0,0 +1,402 @@
import React, { useState } from 'react';
import { Bell, Mail, MessageSquare, Settings, Archive, Trash2, MarkAsRead, Filter } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const NotificationsPage: React.FC = () => {
const [selectedTab, setSelectedTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [selectedNotifications, setSelectedNotifications] = useState<string[]>([]);
const notifications = [
{
id: '1',
type: 'system',
channel: 'app',
title: 'Actualización del Sistema',
message: 'Nueva versión 2.1.0 disponible con mejoras en el módulo de inventario',
timestamp: '2024-01-26 10:15:00',
read: false,
priority: 'medium',
category: 'update',
sender: 'Sistema',
actions: ['Ver Detalles', 'Instalar Después']
},
{
id: '2',
type: 'order',
channel: 'email',
title: 'Nuevo Pedido Recibido',
message: 'Pedido #ORD-456 por €127.50 de Panadería Central',
timestamp: '2024-01-26 09:30:00',
read: false,
priority: 'high',
category: 'sales',
sender: 'Sistema de Ventas',
actions: ['Ver Pedido', 'Procesar']
},
{
id: '3',
type: 'inventory',
channel: 'sms',
title: 'Stock Repuesto',
message: 'Se ha repuesto el stock de azúcar. Nivel actual: 50kg',
timestamp: '2024-01-26 08:45:00',
read: true,
priority: 'low',
category: 'inventory',
sender: 'Gestión de Inventario',
actions: ['Ver Inventario']
},
{
id: '4',
type: 'reminder',
channel: 'app',
title: 'Recordatorio de Mantenimiento',
message: 'El horno #2 requiere mantenimiento preventivo programado para mañana',
timestamp: '2024-01-26 07:00:00',
read: true,
priority: 'medium',
category: 'maintenance',
sender: 'Sistema de Mantenimiento',
actions: ['Programar', 'Posponer']
},
{
id: '5',
type: 'customer',
channel: 'app',
title: 'Reseña de Cliente',
message: 'Nueva reseña de 5 estrellas de María L.: "Excelente calidad y servicio"',
timestamp: '2024-01-25 19:20:00',
read: false,
priority: 'low',
category: 'feedback',
sender: 'Sistema de Reseñas',
actions: ['Ver Reseña', 'Responder']
},
{
id: '6',
type: 'promotion',
channel: 'email',
title: 'Campaña de Marketing Completada',
message: 'La campaña "Desayunos Especiales" ha terminado con 340 interacciones',
timestamp: '2024-01-25 16:30:00',
read: true,
priority: 'low',
category: 'marketing',
sender: 'Sistema de Marketing',
actions: ['Ver Resultados']
}
];
const notificationStats = {
total: notifications.length,
unread: notifications.filter(n => !n.read).length,
high: notifications.filter(n => n.priority === 'high').length,
today: notifications.filter(n =>
new Date(n.timestamp).toDateString() === new Date().toDateString()
).length
};
const tabs = [
{ id: 'all', label: 'Todas', count: notifications.length },
{ id: 'unread', label: 'Sin Leer', count: notificationStats.unread },
{ id: 'system', label: 'Sistema', count: notifications.filter(n => n.type === 'system').length },
{ id: 'order', label: 'Pedidos', count: notifications.filter(n => n.type === 'order').length },
{ id: 'inventory', label: 'Inventario', count: notifications.filter(n => n.type === 'inventory').length }
];
const getNotificationIcon = (type: string, channel: string) => {
const iconProps = { className: "w-5 h-5" };
if (channel === 'email') return <Mail {...iconProps} />;
if (channel === 'sms') return <MessageSquare {...iconProps} />;
switch (type) {
case 'system': return <Settings {...iconProps} />;
case 'order': return <Bell {...iconProps} />;
default: return <Bell {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getChannelBadge = (channel: string) => {
const colors = {
app: 'blue',
email: 'purple',
sms: 'green'
};
return colors[channel as keyof typeof colors] || 'gray';
};
const filteredNotifications = notifications.filter(notification => {
let matchesTab = true;
if (selectedTab === 'unread') {
matchesTab = !notification.read;
} else if (selectedTab !== 'all') {
matchesTab = notification.type === selectedTab;
}
const matchesSearch = notification.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
notification.message.toLowerCase().includes(searchTerm.toLowerCase());
return matchesTab && matchesSearch;
});
const handleSelectNotification = (notificationId: string) => {
setSelectedNotifications(prev =>
prev.includes(notificationId)
? prev.filter(id => id !== notificationId)
: [...prev, notificationId]
);
};
const handleSelectAll = () => {
setSelectedNotifications(
selectedNotifications.length === filteredNotifications.length
? []
: filteredNotifications.map(n => n.id)
);
};
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const notificationTime = new Date(timestamp);
const diffInMs = now.getTime() - notificationTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return `hace ${diffInDays}d`;
} else if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Notificaciones"
description="Centro de notificaciones y mensajes del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Preferencias
</Button>
<Button>
Marcar Todas Leídas
</Button>
</div>
}
/>
{/* Notification Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total</p>
<p className="text-3xl font-bold text-gray-900">{notificationStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Sin Leer</p>
<p className="text-3xl font-bold text-orange-600">{notificationStats.unread}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<MarkAsRead className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Alta Prioridad</p>
<p className="text-3xl font-bold text-red-600">{notificationStats.high}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Hoy</p>
<p className="text-3xl font-bold text-green-600">{notificationStats.today}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Bell className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
</div>
{/* Tabs and Search */}
<Card className="p-6">
<div className="flex flex-col space-y-4">
{/* Tabs */}
<div className="flex space-x-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setSelectedTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedTab === tab.id
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label} ({tab.count})
</button>
))}
</div>
{/* Search and Actions */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar notificaciones..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
{selectedNotifications.length > 0 && (
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<MarkAsRead className="w-4 h-4 mr-2" />
Marcar Leídas ({selectedNotifications.length})
</Button>
<Button size="sm" variant="outline">
<Archive className="w-4 h-4 mr-2" />
Archivar
</Button>
<Button size="sm" variant="outline">
<Trash2 className="w-4 h-4 mr-2" />
Eliminar
</Button>
</div>
)}
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
</div>
</div>
</Card>
{/* Bulk Actions */}
{filteredNotifications.length > 0 && (
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedNotifications.length === filteredNotifications.length}
onChange={handleSelectAll}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-600">
Seleccionar todas ({filteredNotifications.length})
</span>
</label>
</div>
)}
{/* Notifications List */}
<div className="space-y-3">
{filteredNotifications.map((notification) => (
<Card
key={notification.id}
className={`p-4 transition-colors ${
!notification.read ? 'bg-blue-50 border-blue-200' : ''
} ${selectedNotifications.includes(notification.id) ? 'ring-2 ring-blue-500' : ''}`}
>
<div className="flex items-start space-x-4">
<input
type="checkbox"
checked={selectedNotifications.includes(notification.id)}
onChange={() => handleSelectNotification(notification.id)}
className="rounded border-gray-300 mt-1"
/>
<div className={`p-2 rounded-lg bg-${getChannelBadge(notification.channel)}-100 mt-1`}>
{getNotificationIcon(notification.type, notification.channel)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-sm font-semibold text-gray-900 truncate">
{notification.title}
</h3>
{!notification.read && (
<div className="w-2 h-2 bg-blue-600 rounded-full flex-shrink-0"></div>
)}
<Badge variant={getPriorityColor(notification.priority)}>
{notification.priority === 'high' ? 'Alta' :
notification.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<Badge variant={getChannelBadge(notification.channel)}>
{notification.channel.toUpperCase()}
</Badge>
</div>
<p className="text-sm text-gray-700 mb-2">{notification.message}</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>{formatTimeAgo(notification.timestamp)}</span>
<span>•</span>
<span>{notification.sender}</span>
</div>
<div className="flex space-x-2">
{notification.actions.map((action, index) => (
<Button key={index} size="sm" variant="outline" className="text-xs">
{action}
</Button>
))}
</div>
</div>
</div>
</div>
</Card>
))}
</div>
{filteredNotifications.length === 0 && (
<Card className="p-12 text-center">
<Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay notificaciones</h3>
<p className="text-gray-600">
No se encontraron notificaciones que coincidan con los filtros seleccionados.
</p>
</Card>
)}
</div>
);
};
export default NotificationsPage;

View File

@@ -0,0 +1 @@
export { default as NotificationsPage } from './NotificationsPage';

View File

@@ -0,0 +1,388 @@
import React, { useState } from 'react';
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PreferencesPage: React.FC = () => {
const [preferences, setPreferences] = useState({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: 'panaderia@example.com',
phone: '+34 600 123 456',
slack: false,
webhook: ''
}
});
const [hasChanges, setHasChanges] = useState(false);
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving preferences:', preferences);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Preferencias de Comunicación"
description="Configura cómo y cuándo recibir notificaciones"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">No molestar</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Sonidos</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: e.target.value
})}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">{category.name}</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-[var(--text-secondary)] capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default PreferencesPage;

View File

@@ -0,0 +1,388 @@
import React, { useState } from 'react';
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const PreferencesPage: React.FC = () => {
const [preferences, setPreferences] = useState({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: 'panaderia@example.com',
phone: '+34 600 123 456',
slack: false,
webhook: ''
}
});
const [hasChanges, setHasChanges] = useState(false);
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving preferences:', preferences);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Preferencias de Comunicación"
description="Configura cómo y cuándo recibir notificaciones"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">No molestar</span>
</label>
<p className="text-xs text-gray-500 mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Sonidos</span>
</label>
<p className="text-xs text-gray-500 mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-gray-500 mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-gray-500 mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">{category.name}</h3>
<p className="text-sm text-gray-600 mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-gray-300"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-gray-700 capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default PreferencesPage;

View File

@@ -0,0 +1 @@
export { default as PreferencesPage } from './PreferencesPage';

View File

@@ -0,0 +1,312 @@
import React, { useState } from 'react';
import { Calendar, Activity, Filter, Download, Eye, BarChart3 } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const EventsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedCategory, setSelectedCategory] = useState('all');
const events = [
{
id: '1',
timestamp: '2024-01-26 10:30:00',
category: 'sales',
type: 'order_completed',
title: 'Pedido Completado',
description: 'Pedido #ORD-456 completado por €127.50',
metadata: {
orderId: 'ORD-456',
amount: 127.50,
customer: 'María González',
items: 8
},
severity: 'info'
},
{
id: '2',
timestamp: '2024-01-26 09:15:00',
category: 'production',
type: 'batch_started',
title: 'Lote Iniciado',
description: 'Iniciado lote de croissants CR-024',
metadata: {
batchId: 'CR-024',
product: 'Croissants',
quantity: 48,
expectedDuration: '2.5h'
},
severity: 'info'
},
{
id: '3',
timestamp: '2024-01-26 08:45:00',
category: 'inventory',
type: 'stock_updated',
title: 'Stock Actualizado',
description: 'Repuesto stock de harina - Nivel: 50kg',
metadata: {
item: 'Harina de Trigo',
previousLevel: '5kg',
newLevel: '50kg',
supplier: 'Molinos del Sur'
},
severity: 'success'
},
{
id: '4',
timestamp: '2024-01-26 07:30:00',
category: 'system',
type: 'user_login',
title: 'Inicio de Sesión',
description: 'Usuario admin ha iniciado sesión',
metadata: {
userId: 'admin',
ipAddress: '192.168.1.100',
userAgent: 'Chrome/120.0',
location: 'Madrid, ES'
},
severity: 'info'
},
{
id: '5',
timestamp: '2024-01-25 19:20:00',
category: 'sales',
type: 'payment_processed',
title: 'Pago Procesado',
description: 'Pago de €45.80 procesado exitosamente',
metadata: {
amount: 45.80,
method: 'Tarjeta',
reference: 'PAY-789',
customer: 'Juan Pérez'
},
severity: 'success'
}
];
const eventStats = {
total: events.length,
today: events.filter(e =>
new Date(e.timestamp).toDateString() === new Date().toDateString()
).length,
sales: events.filter(e => e.category === 'sales').length,
production: events.filter(e => e.category === 'production').length,
system: events.filter(e => e.category === 'system').length
};
const categories = [
{ value: 'all', label: 'Todos', count: events.length },
{ value: 'sales', label: 'Ventas', count: eventStats.sales },
{ value: 'production', label: 'Producción', count: eventStats.production },
{ value: 'inventory', label: 'Inventario', count: events.filter(e => e.category === 'inventory').length },
{ value: 'system', label: 'Sistema', count: eventStats.system }
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'success': return 'green';
case 'warning': return 'yellow';
case 'error': return 'red';
default: return 'blue';
}
};
const getCategoryIcon = (category: string) => {
const iconProps = { className: "w-4 h-4" };
switch (category) {
case 'sales': return <BarChart3 {...iconProps} />;
case 'production': return <Activity {...iconProps} />;
case 'inventory': return <Calendar {...iconProps} />;
default: return <Activity {...iconProps} />;
}
};
const filteredEvents = selectedCategory === 'all'
? events
: events.filter(event => event.category === selectedCategory);
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const eventTime = new Date(timestamp);
const diffInMs = now.getTime() - eventTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return `hace ${diffInDays}d`;
} else if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Registro de Eventos"
description="Seguimiento de todas las actividades y eventos del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros Avanzados
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Event Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Eventos</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{eventStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Hoy</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{eventStats.today}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{eventStats.production}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
</div>
{/* Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="all">Todos</option>
</select>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
{/* Events List */}
<div className="space-y-4">
{filteredEvents.map((event) => (
<Card key={event.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
{getCategoryIcon(event.category)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{event.title}</h3>
<Badge variant={getSeverityColor(event.severity)}>
{event.category}
</Badge>
</div>
<p className="text-[var(--text-secondary)] mb-3">{event.description}</p>
{/* Event Metadata */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{Object.entries(event.metadata).map(([key, value]) => (
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{value}</p>
</div>
))}
</div>
<div className="flex items-center text-sm text-[var(--text-tertiary)]">
<span>{formatTimeAgo(event.timestamp)}</span>
<span className="mx-2"></span>
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
</div>
</div>
</div>
<Button size="sm" variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
</Card>
))}
</div>
{filteredEvents.length === 0 && (
<Card className="p-12 text-center">
<Activity className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay eventos</h3>
<p className="text-[var(--text-secondary)]">
No se encontraron eventos para el período y categoría seleccionados.
</p>
</Card>
)}
</div>
);
};
export default EventsPage;

View File

@@ -0,0 +1,312 @@
import React, { useState } from 'react';
import { Calendar, Activity, Filter, Download, Eye, BarChart3 } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const EventsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedCategory, setSelectedCategory] = useState('all');
const events = [
{
id: '1',
timestamp: '2024-01-26 10:30:00',
category: 'sales',
type: 'order_completed',
title: 'Pedido Completado',
description: 'Pedido #ORD-456 completado por €127.50',
metadata: {
orderId: 'ORD-456',
amount: 127.50,
customer: 'María González',
items: 8
},
severity: 'info'
},
{
id: '2',
timestamp: '2024-01-26 09:15:00',
category: 'production',
type: 'batch_started',
title: 'Lote Iniciado',
description: 'Iniciado lote de croissants CR-024',
metadata: {
batchId: 'CR-024',
product: 'Croissants',
quantity: 48,
expectedDuration: '2.5h'
},
severity: 'info'
},
{
id: '3',
timestamp: '2024-01-26 08:45:00',
category: 'inventory',
type: 'stock_updated',
title: 'Stock Actualizado',
description: 'Repuesto stock de harina - Nivel: 50kg',
metadata: {
item: 'Harina de Trigo',
previousLevel: '5kg',
newLevel: '50kg',
supplier: 'Molinos del Sur'
},
severity: 'success'
},
{
id: '4',
timestamp: '2024-01-26 07:30:00',
category: 'system',
type: 'user_login',
title: 'Inicio de Sesión',
description: 'Usuario admin ha iniciado sesión',
metadata: {
userId: 'admin',
ipAddress: '192.168.1.100',
userAgent: 'Chrome/120.0',
location: 'Madrid, ES'
},
severity: 'info'
},
{
id: '5',
timestamp: '2024-01-25 19:20:00',
category: 'sales',
type: 'payment_processed',
title: 'Pago Procesado',
description: 'Pago de €45.80 procesado exitosamente',
metadata: {
amount: 45.80,
method: 'Tarjeta',
reference: 'PAY-789',
customer: 'Juan Pérez'
},
severity: 'success'
}
];
const eventStats = {
total: events.length,
today: events.filter(e =>
new Date(e.timestamp).toDateString() === new Date().toDateString()
).length,
sales: events.filter(e => e.category === 'sales').length,
production: events.filter(e => e.category === 'production').length,
system: events.filter(e => e.category === 'system').length
};
const categories = [
{ value: 'all', label: 'Todos', count: events.length },
{ value: 'sales', label: 'Ventas', count: eventStats.sales },
{ value: 'production', label: 'Producción', count: eventStats.production },
{ value: 'inventory', label: 'Inventario', count: events.filter(e => e.category === 'inventory').length },
{ value: 'system', label: 'Sistema', count: eventStats.system }
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'success': return 'green';
case 'warning': return 'yellow';
case 'error': return 'red';
default: return 'blue';
}
};
const getCategoryIcon = (category: string) => {
const iconProps = { className: "w-4 h-4" };
switch (category) {
case 'sales': return <BarChart3 {...iconProps} />;
case 'production': return <Activity {...iconProps} />;
case 'inventory': return <Calendar {...iconProps} />;
default: return <Activity {...iconProps} />;
}
};
const filteredEvents = selectedCategory === 'all'
? events
: events.filter(event => event.category === selectedCategory);
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const eventTime = new Date(timestamp);
const diffInMs = now.getTime() - eventTime.getTime();
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return `hace ${diffInDays}d`;
} else if (diffInHours > 0) {
return `hace ${diffInHours}h`;
} else {
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Registro de Eventos"
description="Seguimiento de todas las actividades y eventos del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros Avanzados
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Event Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Eventos</p>
<p className="text-3xl font-bold text-gray-900">{eventStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Hoy</p>
<p className="text-3xl font-bold text-green-600">{eventStats.today}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Producción</p>
<p className="text-3xl font-bold text-orange-600">{eventStats.production}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
</div>
{/* Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="all">Todos</option>
</select>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
{/* Events List */}
<div className="space-y-4">
{filteredEvents.map((event) => (
<Card key={event.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
{getCategoryIcon(event.category)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{event.title}</h3>
<Badge variant={getSeverityColor(event.severity)}>
{event.category}
</Badge>
</div>
<p className="text-gray-700 mb-3">{event.description}</p>
{/* Event Metadata */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{Object.entries(event.metadata).map(([key, value]) => (
<div key={key} className="bg-gray-50 p-3 rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-gray-900">{value}</p>
</div>
))}
</div>
<div className="flex items-center text-sm text-gray-500">
<span>{formatTimeAgo(event.timestamp)}</span>
<span className="mx-2">•</span>
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
</div>
</div>
</div>
<Button size="sm" variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
</Card>
))}
</div>
{filteredEvents.length === 0 && (
<Card className="p-12 text-center">
<Activity className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay eventos</h3>
<p className="text-gray-600">
No se encontraron eventos para el período y categoría seleccionados.
</p>
</Card>
)}
</div>
);
};
export default EventsPage;

View File

@@ -0,0 +1 @@
export { default as EventsPage } from './EventsPage';

View File

@@ -0,0 +1,336 @@
import React, { useState } from 'react';
import { Users, Clock, TrendingUp, MapPin, Calendar, BarChart3, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TrafficPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedMetric, setSelectedMetric] = useState('visitors');
const trafficData = {
totalVisitors: 2847,
peakHour: '12:00',
averageVisitDuration: '23min',
busyDays: ['Viernes', 'Sábado'],
conversionRate: 68.4
};
const hourlyTraffic = [
{ hour: '07:00', visitors: 15, sales: 12, duration: '18min' },
{ hour: '08:00', visitors: 32, sales: 24, duration: '22min' },
{ hour: '09:00', visitors: 45, sales: 28, duration: '25min' },
{ hour: '10:00', visitors: 38, sales: 25, duration: '24min' },
{ hour: '11:00', visitors: 52, sales: 35, duration: '26min' },
{ hour: '12:00', visitors: 78, sales: 54, duration: '28min' },
{ hour: '13:00', visitors: 85, sales: 58, duration: '30min' },
{ hour: '14:00', visitors: 62, sales: 42, duration: '27min' },
{ hour: '15:00', visitors: 48, sales: 32, duration: '25min' },
{ hour: '16:00', visitors: 55, sales: 38, duration: '26min' },
{ hour: '17:00', visitors: 68, sales: 46, duration: '29min' },
{ hour: '18:00', visitors: 74, sales: 52, duration: '31min' },
{ hour: '19:00', visitors: 56, sales: 39, duration: '28min' },
{ hour: '20:00', visitors: 28, sales: 18, duration: '22min' }
];
const dailyTraffic = [
{ day: 'Lun', visitors: 245, sales: 168, conversion: 68.6, avgDuration: '22min' },
{ day: 'Mar', visitors: 289, sales: 195, conversion: 67.5, avgDuration: '24min' },
{ day: 'Mié', visitors: 321, sales: 218, conversion: 67.9, avgDuration: '25min' },
{ day: 'Jue', visitors: 356, sales: 242, conversion: 68.0, avgDuration: '26min' },
{ day: 'Vie', visitors: 445, sales: 312, conversion: 70.1, avgDuration: '28min' },
{ day: 'Sáb', visitors: 498, sales: 348, conversion: 69.9, avgDuration: '30min' },
{ day: 'Dom', visitors: 398, sales: 265, conversion: 66.6, avgDuration: '27min' }
];
const trafficSources = [
{ source: 'Pie', visitors: 1245, percentage: 43.7, trend: 5.2 },
{ source: 'Búsqueda Local', visitors: 687, percentage: 24.1, trend: 12.3 },
{ source: 'Recomendaciones', visitors: 423, percentage: 14.9, trend: -2.1 },
{ source: 'Redes Sociales', visitors: 298, percentage: 10.5, trend: 8.7 },
{ source: 'Publicidad', visitors: 194, percentage: 6.8, trend: 15.4 }
];
const customerSegments = [
{
segment: 'Regulares Matutinos',
count: 145,
percentage: 24.2,
peakHours: ['07:00-09:00'],
avgSpend: 12.50,
frequency: 'Diaria'
},
{
segment: 'Familia Fin de Semana',
count: 198,
percentage: 33.1,
peakHours: ['10:00-13:00'],
avgSpend: 28.90,
frequency: 'Semanal'
},
{
segment: 'Oficinistas Almuerzo',
count: 112,
percentage: 18.7,
peakHours: ['12:00-14:00'],
avgSpend: 8.75,
frequency: '2-3x semana'
},
{
segment: 'Clientes Ocasionales',
count: 143,
percentage: 23.9,
peakHours: ['16:00-19:00'],
avgSpend: 15.20,
frequency: 'Mensual'
}
];
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]';
};
const getTrendIcon = (trend: number) => {
return trend >= 0 ? '↗' : '↘';
};
const maxVisitors = Math.max(...hourlyTraffic.map(h => h.visitors));
const maxDailyVisitors = Math.max(...dailyTraffic.map(d => d.visitors));
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Tráfico"
description="Monitorea los patrones de visitas y flujo de clientes"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Traffic Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Visitantes Totales</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{trafficData.totalVisitors.toLocaleString()}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Hora Pico</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{trafficData.peakHour}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Duración Promedio</p>
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Conversión</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{trafficData.conversionRate}%</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<TrendingUp className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Días Ocupados</p>
<p className="text-sm font-bold text-[var(--color-error)]">{trafficData.busyDays.join(', ')}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="year">Este Año</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="visitors">Visitantes</option>
<option value="sales">Ventas</option>
<option value="duration">Duración</option>
<option value="conversion">Conversión</option>
</select>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hourly Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{hourlyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
<div
className="w-full bg-[var(--color-info)]/50 rounded-t"
style={{
height: `${(data.visitors / maxVisitors) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Daily Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico Semanal</h3>
<div className="h-64 flex items-end space-x-2 justify-between">
{dailyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
<div
className="w-full bg-green-500 rounded-t"
style={{
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
minHeight: '8px'
}}
></div>
<span className="text-sm text-[var(--text-secondary)] mt-2 font-medium">
{data.day}
</span>
<div className="text-xs text-[var(--text-tertiary)] mt-1">
{data.conversion}%
</div>
</div>
))}
</div>
</Card>
{/* Traffic Sources */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Fuentes de Tráfico</h3>
<div className="space-y-3">
{trafficSources.map((source, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-[var(--color-info)]/50 rounded-full"></div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{source.source}</p>
<p className="text-xs text-[var(--text-tertiary)]">{source.visitors} visitantes</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">{source.percentage}%</p>
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]">{segment.segment}</h4>
<Badge variant="blue">{segment.percentage}%</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-[var(--text-tertiary)]">Clientes</p>
<p className="font-medium">{segment.count}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Gasto Promedio</p>
<p className="font-medium">{segment.avgSpend}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Horario Pico</p>
<p className="font-medium">{segment.peakHours.join(', ')}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Frecuencia</p>
<p className="font-medium">{segment.frequency}</p>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Traffic Heat Map placeholder */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Mapa de Calor - Zonas de la Panadería</h3>
<div className="h-64 bg-[var(--bg-tertiary)] rounded-lg flex items-center justify-center">
<div className="text-center">
<MapPin className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">Visualización de zonas de mayor tráfico</p>
<p className="text-sm text-[var(--text-tertiary)] mt-1">Entrada: 45% Mostrador: 32% Zona sentada: 23%</p>
</div>
</div>
</Card>
</div>
);
};
export default TrafficPage;

View File

@@ -0,0 +1,336 @@
import React, { useState } from 'react';
import { Users, Clock, TrendingUp, MapPin, Calendar, BarChart3, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TrafficPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const [selectedMetric, setSelectedMetric] = useState('visitors');
const trafficData = {
totalVisitors: 2847,
peakHour: '12:00',
averageVisitDuration: '23min',
busyDays: ['Viernes', 'Sábado'],
conversionRate: 68.4
};
const hourlyTraffic = [
{ hour: '07:00', visitors: 15, sales: 12, duration: '18min' },
{ hour: '08:00', visitors: 32, sales: 24, duration: '22min' },
{ hour: '09:00', visitors: 45, sales: 28, duration: '25min' },
{ hour: '10:00', visitors: 38, sales: 25, duration: '24min' },
{ hour: '11:00', visitors: 52, sales: 35, duration: '26min' },
{ hour: '12:00', visitors: 78, sales: 54, duration: '28min' },
{ hour: '13:00', visitors: 85, sales: 58, duration: '30min' },
{ hour: '14:00', visitors: 62, sales: 42, duration: '27min' },
{ hour: '15:00', visitors: 48, sales: 32, duration: '25min' },
{ hour: '16:00', visitors: 55, sales: 38, duration: '26min' },
{ hour: '17:00', visitors: 68, sales: 46, duration: '29min' },
{ hour: '18:00', visitors: 74, sales: 52, duration: '31min' },
{ hour: '19:00', visitors: 56, sales: 39, duration: '28min' },
{ hour: '20:00', visitors: 28, sales: 18, duration: '22min' }
];
const dailyTraffic = [
{ day: 'Lun', visitors: 245, sales: 168, conversion: 68.6, avgDuration: '22min' },
{ day: 'Mar', visitors: 289, sales: 195, conversion: 67.5, avgDuration: '24min' },
{ day: 'Mié', visitors: 321, sales: 218, conversion: 67.9, avgDuration: '25min' },
{ day: 'Jue', visitors: 356, sales: 242, conversion: 68.0, avgDuration: '26min' },
{ day: 'Vie', visitors: 445, sales: 312, conversion: 70.1, avgDuration: '28min' },
{ day: 'Sáb', visitors: 498, sales: 348, conversion: 69.9, avgDuration: '30min' },
{ day: 'Dom', visitors: 398, sales: 265, conversion: 66.6, avgDuration: '27min' }
];
const trafficSources = [
{ source: 'Pie', visitors: 1245, percentage: 43.7, trend: 5.2 },
{ source: 'Búsqueda Local', visitors: 687, percentage: 24.1, trend: 12.3 },
{ source: 'Recomendaciones', visitors: 423, percentage: 14.9, trend: -2.1 },
{ source: 'Redes Sociales', visitors: 298, percentage: 10.5, trend: 8.7 },
{ source: 'Publicidad', visitors: 194, percentage: 6.8, trend: 15.4 }
];
const customerSegments = [
{
segment: 'Regulares Matutinos',
count: 145,
percentage: 24.2,
peakHours: ['07:00-09:00'],
avgSpend: 12.50,
frequency: 'Diaria'
},
{
segment: 'Familia Fin de Semana',
count: 198,
percentage: 33.1,
peakHours: ['10:00-13:00'],
avgSpend: 28.90,
frequency: 'Semanal'
},
{
segment: 'Oficinistas Almuerzo',
count: 112,
percentage: 18.7,
peakHours: ['12:00-14:00'],
avgSpend: 8.75,
frequency: '2-3x semana'
},
{
segment: 'Clientes Ocasionales',
count: 143,
percentage: 23.9,
peakHours: ['16:00-19:00'],
avgSpend: 15.20,
frequency: 'Mensual'
}
];
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-green-600' : 'text-red-600';
};
const getTrendIcon = (trend: number) => {
return trend >= 0 ? '↗' : '↘';
};
const maxVisitors = Math.max(...hourlyTraffic.map(h => h.visitors));
const maxDailyVisitors = Math.max(...dailyTraffic.map(d => d.visitors));
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Tráfico"
description="Monitorea los patrones de visitas y flujo de clientes"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Traffic Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Visitantes Totales</p>
<p className="text-3xl font-bold text-blue-600">{trafficData.totalVisitors.toLocaleString()}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Hora Pico</p>
<p className="text-3xl font-bold text-green-600">{trafficData.peakHour}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Duración Promedio</p>
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Conversión</p>
<p className="text-3xl font-bold text-orange-600">{trafficData.conversionRate}%</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<TrendingUp className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Días Ocupados</p>
<p className="text-sm font-bold text-red-600">{trafficData.busyDays.join(', ')}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="year">Este Año</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="visitors">Visitantes</option>
<option value="sales">Ventas</option>
<option value="duration">Duración</option>
<option value="conversion">Conversión</option>
</select>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hourly Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{hourlyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
<div
className="w-full bg-blue-500 rounded-t"
style={{
height: `${(data.visitors / maxVisitors) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Daily Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico Semanal</h3>
<div className="h-64 flex items-end space-x-2 justify-between">
{dailyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
<div
className="w-full bg-green-500 rounded-t"
style={{
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
minHeight: '8px'
}}
></div>
<span className="text-sm text-gray-700 mt-2 font-medium">
{data.day}
</span>
<div className="text-xs text-gray-500 mt-1">
{data.conversion}%
</div>
</div>
))}
</div>
</Card>
{/* Traffic Sources */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fuentes de Tráfico</h3>
<div className="space-y-3">
{trafficSources.map((source, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<div>
<p className="text-sm font-medium text-gray-900">{source.source}</p>
<p className="text-xs text-gray-500">{source.visitors} visitantes</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">{source.percentage}%</p>
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">{segment.segment}</h4>
<Badge variant="blue">{segment.percentage}%</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500">Clientes</p>
<p className="font-medium">{segment.count}</p>
</div>
<div>
<p className="text-gray-500">Gasto Promedio</p>
<p className="font-medium">€{segment.avgSpend}</p>
</div>
<div>
<p className="text-gray-500">Horario Pico</p>
<p className="font-medium">{segment.peakHours.join(', ')}</p>
</div>
<div>
<p className="text-gray-500">Frecuencia</p>
<p className="font-medium">{segment.frequency}</p>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Traffic Heat Map placeholder */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Mapa de Calor - Zonas de la Panadería</h3>
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
<div className="text-center">
<MapPin className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">Visualización de zonas de mayor tráfico</p>
<p className="text-sm text-gray-500 mt-1">Entrada: 45% • Mostrador: 32% • Zona sentada: 23%</p>
</div>
</div>
</Card>
</div>
);
};
export default TrafficPage;

View File

@@ -0,0 +1 @@
export { default as TrafficPage } from './TrafficPage';

View File

@@ -0,0 +1,423 @@
import React, { useState } from 'react';
import { Cloud, Sun, CloudRain, Thermometer, Wind, Droplets, Calendar, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const WeatherPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const currentWeather = {
temperature: 18,
condition: 'partly-cloudy',
humidity: 65,
windSpeed: 12,
pressure: 1013,
uvIndex: 4,
visibility: 10,
description: 'Parcialmente nublado'
};
const forecast = [
{
date: '2024-01-27',
day: 'Sábado',
condition: 'sunny',
tempMax: 22,
tempMin: 12,
humidity: 45,
precipitation: 0,
wind: 8,
impact: 'high-demand',
recommendation: 'Incrementar producción de helados y bebidas frías'
},
{
date: '2024-01-28',
day: 'Domingo',
condition: 'partly-cloudy',
tempMax: 19,
tempMin: 11,
humidity: 55,
precipitation: 20,
wind: 15,
impact: 'normal',
recommendation: 'Producción estándar'
},
{
date: '2024-01-29',
day: 'Lunes',
condition: 'rainy',
tempMax: 15,
tempMin: 8,
humidity: 85,
precipitation: 80,
wind: 22,
impact: 'comfort-food',
recommendation: 'Aumentar sopas, chocolates calientes y pan recién horneado'
},
{
date: '2024-01-30',
day: 'Martes',
condition: 'cloudy',
tempMax: 16,
tempMin: 9,
humidity: 70,
precipitation: 40,
wind: 18,
impact: 'moderate',
recommendation: 'Enfoque en productos de interior'
},
{
date: '2024-01-31',
day: 'Miércoles',
condition: 'sunny',
tempMax: 24,
tempMin: 14,
humidity: 40,
precipitation: 0,
wind: 10,
impact: 'high-demand',
recommendation: 'Incrementar productos frescos y ensaladas'
}
];
const weatherImpacts = [
{
condition: 'Día Soleado',
icon: Sun,
impact: 'Aumento del 25% en bebidas frías',
recommendations: [
'Incrementar producción de helados',
'Más bebidas refrescantes',
'Ensaladas y productos frescos',
'Horario extendido de terraza'
],
color: 'yellow'
},
{
condition: 'Día Lluvioso',
icon: CloudRain,
impact: 'Aumento del 40% en productos calientes',
recommendations: [
'Más sopas y caldos',
'Chocolates calientes',
'Pan recién horneado',
'Productos de repostería'
],
color: 'blue'
},
{
condition: 'Frío Intenso',
icon: Thermometer,
impact: 'Preferencia por comida reconfortante',
recommendations: [
'Aumentar productos horneados',
'Bebidas calientes especiales',
'Productos energéticos',
'Promociones de interior'
],
color: 'purple'
}
];
const seasonalTrends = [
{
season: 'Primavera',
period: 'Mar - May',
trends: [
'Aumento en productos frescos (+30%)',
'Mayor demanda de ensaladas',
'Bebidas naturales populares',
'Horarios extendidos efectivos'
],
avgTemp: '15-20°C',
impact: 'positive'
},
{
season: 'Verano',
period: 'Jun - Ago',
trends: [
'Pico de helados y granizados (+60%)',
'Productos ligeros preferidos',
'Horario matutino crítico',
'Mayor tráfico de turistas'
],
avgTemp: '25-35°C',
impact: 'high'
},
{
season: 'Otoño',
period: 'Sep - Nov',
trends: [
'Regreso a productos tradicionales',
'Aumento en bollería (+20%)',
'Bebidas calientes populares',
'Horarios regulares'
],
avgTemp: '10-18°C',
impact: 'stable'
},
{
season: 'Invierno',
period: 'Dec - Feb',
trends: [
'Máximo de productos calientes (+50%)',
'Pan recién horneado crítico',
'Chocolates y dulces festivos',
'Menor tráfico general (-15%)'
],
avgTemp: '5-12°C',
impact: 'comfort'
}
];
const getWeatherIcon = (condition: string) => {
const iconProps = { className: "w-8 h-8" };
switch (condition) {
case 'sunny': return <Sun {...iconProps} className="w-8 h-8 text-yellow-500" />;
case 'partly-cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-[var(--text-tertiary)]" />;
case 'cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-[var(--text-secondary)]" />;
case 'rainy': return <CloudRain {...iconProps} className="w-8 h-8 text-blue-500" />;
default: return <Cloud {...iconProps} />;
}
};
const getConditionLabel = (condition: string) => {
switch (condition) {
case 'sunny': return 'Soleado';
case 'partly-cloudy': return 'Parcialmente nublado';
case 'cloudy': return 'Nublado';
case 'rainy': return 'Lluvioso';
default: return condition;
}
};
const getImpactColor = (impact: string) => {
switch (impact) {
case 'high-demand': return 'green';
case 'comfort-food': return 'orange';
case 'moderate': return 'blue';
case 'normal': return 'gray';
default: return 'gray';
}
};
const getImpactLabel = (impact: string) => {
switch (impact) {
case 'high-demand': return 'Alta Demanda';
case 'comfort-food': return 'Comida Reconfortante';
case 'moderate': return 'Demanda Moderada';
case 'normal': return 'Demanda Normal';
default: return impact;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Datos Meteorológicos"
description="Integra información del clima para optimizar la producción y ventas"
/>
{/* Current Weather */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Condiciones Actuales</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="flex items-center space-x-4">
{getWeatherIcon(currentWeather.condition)}
<div>
<p className="text-3xl font-bold text-[var(--text-primary)]">{currentWeather.temperature}°C</p>
<p className="text-sm text-[var(--text-secondary)]">{currentWeather.description}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Droplets className="w-4 h-4 text-blue-500" />
<span className="text-sm text-[var(--text-secondary)]">Humedad: {currentWeather.humidity}%</span>
</div>
<div className="flex items-center space-x-2">
<Wind className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-secondary)]">Viento: {currentWeather.windSpeed} km/h</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">Presión:</span> {currentWeather.pressure} hPa
</div>
<div className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">UV:</span> {currentWeather.uvIndex}
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">Visibilidad:</span> {currentWeather.visibility} km
</div>
<Badge variant="blue">Condiciones favorables</Badge>
</div>
</div>
</Card>
{/* Weather Forecast */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Pronóstico Extendido</h3>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
>
<option value="week">Próxima Semana</option>
<option value="month">Próximo Mes</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{forecast.map((day, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="text-center mb-3">
<p className="font-medium text-[var(--text-primary)]">{day.day}</p>
<p className="text-xs text-[var(--text-tertiary)]">{new Date(day.date).toLocaleDateString('es-ES')}</p>
</div>
<div className="flex justify-center mb-3">
{getWeatherIcon(day.condition)}
</div>
<div className="text-center mb-3">
<p className="text-sm text-[var(--text-secondary)]">{getConditionLabel(day.condition)}</p>
<p className="text-lg font-semibold">
{day.tempMax}° <span className="text-sm text-[var(--text-tertiary)]">/ {day.tempMin}°</span>
</p>
</div>
<div className="space-y-2 text-xs text-[var(--text-secondary)]">
<div className="flex justify-between">
<span>Humedad:</span>
<span>{day.humidity}%</span>
</div>
<div className="flex justify-between">
<span>Lluvia:</span>
<span>{day.precipitation}%</span>
</div>
<div className="flex justify-between">
<span>Viento:</span>
<span>{day.wind} km/h</span>
</div>
</div>
<div className="mt-3">
<Badge variant={getImpactColor(day.impact)} className="text-xs">
{getImpactLabel(day.impact)}
</Badge>
</div>
<div className="mt-2">
<p className="text-xs text-[var(--text-secondary)]">{day.recommendation}</p>
</div>
</div>
))}
</div>
</Card>
{/* Weather Impact Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Impacto del Clima</h3>
<div className="space-y-4">
{weatherImpacts.map((impact, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg bg-${impact.color}-100`}>
<impact.icon className={`w-5 h-5 text-${impact.color}-600`} />
</div>
<div>
<h4 className="font-medium text-[var(--text-primary)]">{impact.condition}</h4>
<p className="text-sm text-[var(--text-secondary)]">{impact.impact}</p>
</div>
</div>
<div className="ml-10">
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">Recomendaciones:</p>
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
{impact.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-center">
<span className="w-1 h-1 bg-gray-400 rounded-full mr-2"></span>
{rec}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</Card>
{/* Seasonal Trends */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tendencias Estacionales</h3>
<div className="space-y-4">
{seasonalTrends.map((season, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="font-medium text-[var(--text-primary)]">{season.season}</h4>
<p className="text-sm text-[var(--text-tertiary)]">{season.period}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-secondary)]">{season.avgTemp}</p>
<Badge variant={
season.impact === 'high' ? 'green' :
season.impact === 'positive' ? 'blue' :
season.impact === 'comfort' ? 'orange' : 'gray'
}>
{season.impact === 'high' ? 'Alto' :
season.impact === 'positive' ? 'Positivo' :
season.impact === 'comfort' ? 'Confort' : 'Estable'}
</Badge>
</div>
</div>
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
{season.trends.map((trend, idx) => (
<li key={idx} className="flex items-center">
<TrendingUp className="w-3 h-3 mr-2 text-green-500" />
{trend}
</li>
))}
</ul>
</div>
))}
</div>
</Card>
</div>
{/* Weather Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Meteorológicas</h3>
<div className="space-y-3">
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
</div>
</div>
<div className="flex items-center p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
<CloudRain className="w-5 h-5 text-[var(--color-info)] mr-3" />
<div>
<p className="text-sm font-medium text-[var(--color-info)]">Lluvia intensa el lunes</p>
<p className="text-sm text-[var(--color-info)]">80% probabilidad de precipitación con vientos fuertes</p>
<p className="text-xs text-[var(--color-info)] mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
</div>
</div>
</div>
</Card>
</div>
);
};
export default WeatherPage;

View File

@@ -0,0 +1,423 @@
import React, { useState } from 'react';
import { Cloud, Sun, CloudRain, Thermometer, Wind, Droplets, Calendar, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const WeatherPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('week');
const currentWeather = {
temperature: 18,
condition: 'partly-cloudy',
humidity: 65,
windSpeed: 12,
pressure: 1013,
uvIndex: 4,
visibility: 10,
description: 'Parcialmente nublado'
};
const forecast = [
{
date: '2024-01-27',
day: 'Sábado',
condition: 'sunny',
tempMax: 22,
tempMin: 12,
humidity: 45,
precipitation: 0,
wind: 8,
impact: 'high-demand',
recommendation: 'Incrementar producción de helados y bebidas frías'
},
{
date: '2024-01-28',
day: 'Domingo',
condition: 'partly-cloudy',
tempMax: 19,
tempMin: 11,
humidity: 55,
precipitation: 20,
wind: 15,
impact: 'normal',
recommendation: 'Producción estándar'
},
{
date: '2024-01-29',
day: 'Lunes',
condition: 'rainy',
tempMax: 15,
tempMin: 8,
humidity: 85,
precipitation: 80,
wind: 22,
impact: 'comfort-food',
recommendation: 'Aumentar sopas, chocolates calientes y pan recién horneado'
},
{
date: '2024-01-30',
day: 'Martes',
condition: 'cloudy',
tempMax: 16,
tempMin: 9,
humidity: 70,
precipitation: 40,
wind: 18,
impact: 'moderate',
recommendation: 'Enfoque en productos de interior'
},
{
date: '2024-01-31',
day: 'Miércoles',
condition: 'sunny',
tempMax: 24,
tempMin: 14,
humidity: 40,
precipitation: 0,
wind: 10,
impact: 'high-demand',
recommendation: 'Incrementar productos frescos y ensaladas'
}
];
const weatherImpacts = [
{
condition: 'Día Soleado',
icon: Sun,
impact: 'Aumento del 25% en bebidas frías',
recommendations: [
'Incrementar producción de helados',
'Más bebidas refrescantes',
'Ensaladas y productos frescos',
'Horario extendido de terraza'
],
color: 'yellow'
},
{
condition: 'Día Lluvioso',
icon: CloudRain,
impact: 'Aumento del 40% en productos calientes',
recommendations: [
'Más sopas y caldos',
'Chocolates calientes',
'Pan recién horneado',
'Productos de repostería'
],
color: 'blue'
},
{
condition: 'Frío Intenso',
icon: Thermometer,
impact: 'Preferencia por comida reconfortante',
recommendations: [
'Aumentar productos horneados',
'Bebidas calientes especiales',
'Productos energéticos',
'Promociones de interior'
],
color: 'purple'
}
];
const seasonalTrends = [
{
season: 'Primavera',
period: 'Mar - May',
trends: [
'Aumento en productos frescos (+30%)',
'Mayor demanda de ensaladas',
'Bebidas naturales populares',
'Horarios extendidos efectivos'
],
avgTemp: '15-20°C',
impact: 'positive'
},
{
season: 'Verano',
period: 'Jun - Ago',
trends: [
'Pico de helados y granizados (+60%)',
'Productos ligeros preferidos',
'Horario matutino crítico',
'Mayor tráfico de turistas'
],
avgTemp: '25-35°C',
impact: 'high'
},
{
season: 'Otoño',
period: 'Sep - Nov',
trends: [
'Regreso a productos tradicionales',
'Aumento en bollería (+20%)',
'Bebidas calientes populares',
'Horarios regulares'
],
avgTemp: '10-18°C',
impact: 'stable'
},
{
season: 'Invierno',
period: 'Dec - Feb',
trends: [
'Máximo de productos calientes (+50%)',
'Pan recién horneado crítico',
'Chocolates y dulces festivos',
'Menor tráfico general (-15%)'
],
avgTemp: '5-12°C',
impact: 'comfort'
}
];
const getWeatherIcon = (condition: string) => {
const iconProps = { className: "w-8 h-8" };
switch (condition) {
case 'sunny': return <Sun {...iconProps} className="w-8 h-8 text-yellow-500" />;
case 'partly-cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-400" />;
case 'cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-600" />;
case 'rainy': return <CloudRain {...iconProps} className="w-8 h-8 text-blue-500" />;
default: return <Cloud {...iconProps} />;
}
};
const getConditionLabel = (condition: string) => {
switch (condition) {
case 'sunny': return 'Soleado';
case 'partly-cloudy': return 'Parcialmente nublado';
case 'cloudy': return 'Nublado';
case 'rainy': return 'Lluvioso';
default: return condition;
}
};
const getImpactColor = (impact: string) => {
switch (impact) {
case 'high-demand': return 'green';
case 'comfort-food': return 'orange';
case 'moderate': return 'blue';
case 'normal': return 'gray';
default: return 'gray';
}
};
const getImpactLabel = (impact: string) => {
switch (impact) {
case 'high-demand': return 'Alta Demanda';
case 'comfort-food': return 'Comida Reconfortante';
case 'moderate': return 'Demanda Moderada';
case 'normal': return 'Demanda Normal';
default: return impact;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Datos Meteorológicos"
description="Integra información del clima para optimizar la producción y ventas"
/>
{/* Current Weather */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Condiciones Actuales</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="flex items-center space-x-4">
{getWeatherIcon(currentWeather.condition)}
<div>
<p className="text-3xl font-bold text-gray-900">{currentWeather.temperature}°C</p>
<p className="text-sm text-gray-600">{currentWeather.description}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Droplets className="w-4 h-4 text-blue-500" />
<span className="text-sm text-gray-600">Humedad: {currentWeather.humidity}%</span>
</div>
<div className="flex items-center space-x-2">
<Wind className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600">Viento: {currentWeather.windSpeed} km/h</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Presión:</span> {currentWeather.pressure} hPa
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">UV:</span> {currentWeather.uvIndex}
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Visibilidad:</span> {currentWeather.visibility} km
</div>
<Badge variant="blue">Condiciones favorables</Badge>
</div>
</div>
</Card>
{/* Weather Forecast */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Pronóstico Extendido</h3>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
>
<option value="week">Próxima Semana</option>
<option value="month">Próximo Mes</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{forecast.map((day, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="text-center mb-3">
<p className="font-medium text-gray-900">{day.day}</p>
<p className="text-xs text-gray-500">{new Date(day.date).toLocaleDateString('es-ES')}</p>
</div>
<div className="flex justify-center mb-3">
{getWeatherIcon(day.condition)}
</div>
<div className="text-center mb-3">
<p className="text-sm text-gray-600">{getConditionLabel(day.condition)}</p>
<p className="text-lg font-semibold">
{day.tempMax}° <span className="text-sm text-gray-500">/ {day.tempMin}°</span>
</p>
</div>
<div className="space-y-2 text-xs text-gray-600">
<div className="flex justify-between">
<span>Humedad:</span>
<span>{day.humidity}%</span>
</div>
<div className="flex justify-between">
<span>Lluvia:</span>
<span>{day.precipitation}%</span>
</div>
<div className="flex justify-between">
<span>Viento:</span>
<span>{day.wind} km/h</span>
</div>
</div>
<div className="mt-3">
<Badge variant={getImpactColor(day.impact)} className="text-xs">
{getImpactLabel(day.impact)}
</Badge>
</div>
<div className="mt-2">
<p className="text-xs text-gray-600">{day.recommendation}</p>
</div>
</div>
))}
</div>
</Card>
{/* Weather Impact Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Impacto del Clima</h3>
<div className="space-y-4">
{weatherImpacts.map((impact, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center space-x-3 mb-3">
<div className={`p-2 rounded-lg bg-${impact.color}-100`}>
<impact.icon className={`w-5 h-5 text-${impact.color}-600`} />
</div>
<div>
<h4 className="font-medium text-gray-900">{impact.condition}</h4>
<p className="text-sm text-gray-600">{impact.impact}</p>
</div>
</div>
<div className="ml-10">
<p className="text-sm font-medium text-gray-700 mb-2">Recomendaciones:</p>
<ul className="text-sm text-gray-600 space-y-1">
{impact.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-center">
<span className="w-1 h-1 bg-gray-400 rounded-full mr-2"></span>
{rec}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</Card>
{/* Seasonal Trends */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tendencias Estacionales</h3>
<div className="space-y-4">
{seasonalTrends.map((season, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="font-medium text-gray-900">{season.season}</h4>
<p className="text-sm text-gray-500">{season.period}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-700">{season.avgTemp}</p>
<Badge variant={
season.impact === 'high' ? 'green' :
season.impact === 'positive' ? 'blue' :
season.impact === 'comfort' ? 'orange' : 'gray'
}>
{season.impact === 'high' ? 'Alto' :
season.impact === 'positive' ? 'Positivo' :
season.impact === 'comfort' ? 'Confort' : 'Estable'}
</Badge>
</div>
</div>
<ul className="text-sm text-gray-600 space-y-1">
{season.trends.map((trend, idx) => (
<li key={idx} className="flex items-center">
<TrendingUp className="w-3 h-3 mr-2 text-green-500" />
{trend}
</li>
))}
</ul>
</div>
))}
</div>
</Card>
</div>
{/* Weather Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alertas Meteorológicas</h3>
<div className="space-y-3">
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
</div>
</div>
<div className="flex items-center p-3 bg-blue-50 border border-blue-200 rounded-lg">
<CloudRain className="w-5 h-5 text-blue-600 mr-3" />
<div>
<p className="text-sm font-medium text-blue-800">Lluvia intensa el lunes</p>
<p className="text-sm text-blue-700">80% probabilidad de precipitación con vientos fuertes</p>
<p className="text-xs text-blue-600 mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
</div>
</div>
</div>
</Card>
</div>
);
};
export default WeatherPage;

View File

@@ -0,0 +1 @@
export { default as WeatherPage } from './WeatherPage';

View File

@@ -0,0 +1,435 @@
import React, { useState } from 'react';
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingAnalysisPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('30days');
const analysisData = {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días',
stepsCompleted: 15,
totalSteps: 16,
dataQuality: 94
};
const stepProgress = [
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
];
const insights = [
{
type: 'success',
title: 'Excelente Progreso',
description: 'Has completado el 94% del proceso de configuración inicial',
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
impact: 'high'
},
{
type: 'info',
title: 'Calidad de Datos Alta',
description: 'Tus datos tienen una calidad promedio del 94%',
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
impact: 'medium'
},
{
type: 'warning',
title: 'Paso Pendiente',
description: 'Las pruebas del sistema están pendientes',
recommendation: 'Programa las pruebas para validar la configuración completa',
impact: 'high'
}
];
const dataAnalysis = [
{
category: 'Información del Negocio',
completeness: 100,
accuracy: 95,
items: 12,
issues: 0,
details: 'Toda la información básica está completa y verificada'
},
{
category: 'Menú y Productos',
completeness: 85,
accuracy: 88,
items: 45,
issues: 3,
details: '3 productos sin precios definidos'
},
{
category: 'Inventario Inicial',
completeness: 92,
accuracy: 90,
items: 28,
issues: 2,
details: '2 ingredientes sin stock mínimo definido'
},
{
category: 'Configuración Operativa',
completeness: 100,
accuracy: 100,
items: 8,
issues: 0,
details: 'Horarios y políticas completamente configuradas'
}
];
const benchmarkComparison = {
industry: {
onboardingScore: 74,
completionRate: 78,
averageTime: '6.8 días'
},
yourData: {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días'
}
};
const recommendations = [
{
priority: 'high',
title: 'Completar Pruebas del Sistema',
description: 'Realizar pruebas integrales para validar toda la configuración',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento óptimo del sistema'
},
{
priority: 'medium',
title: 'Revisar Precios de Productos',
description: 'Definir precios para los 3 productos pendientes',
estimatedTime: '15 minutos',
impact: 'Permitirá generar ventas de todos los productos'
},
{
priority: 'medium',
title: 'Configurar Stocks Mínimos',
description: 'Establecer niveles mínimos para 2 ingredientes',
estimatedTime: '10 minutos',
impact: 'Mejorará el control de inventario automático'
},
{
priority: 'low',
title: 'Optimizar Configuración de Pagos',
description: 'Revisar métodos de pago y comisiones',
estimatedTime: '20 minutos',
impact: 'Puede reducir costos de transacción'
}
];
const getInsightIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'info': return <Target {...iconProps} className="w-5 h-5 text-[var(--color-info)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getInsightColor = (type: string) => {
switch (type) {
case 'success': return 'bg-green-50 border-green-200';
case 'warning': return 'bg-yellow-50 border-yellow-200';
case 'info': return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
default: return 'bg-[var(--bg-secondary)] border-[var(--border-primary)]';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getCompletionColor = (percentage: number) => {
if (percentage >= 95) return 'text-[var(--color-success)]';
if (percentage >= 80) return 'text-yellow-600';
return 'text-[var(--color-error)]';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Configuración"
description="Análisis detallado de tu proceso de configuración y recomendaciones"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Overall Score */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
<div className="text-center">
<div className="relative w-24 h-24 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-[var(--color-success)]">{analysisData.onboardingScore}</span>
</div>
<svg className="w-24 h-24 transform -rotate-90">
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
/>
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
className="text-[var(--color-success)]"
/>
</svg>
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-info)]">{analysisData.completionRate}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Promedio</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-primary)]">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pasos Completados</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad de Datos</p>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp className="w-8 h-8 text-[var(--color-success)]" />
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Por encima del promedio</p>
</div>
</div>
</Card>
{/* Progress Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso por Pasos</h3>
<div className="space-y-4">
{stepProgress.map((step, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step.completed ? 'bg-[var(--color-success)]/10' : 'bg-[var(--bg-tertiary)]'
}`}>
{step.completed ? (
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
) : (
<span className="text-sm font-medium text-[var(--text-tertiary)]">{index + 1}</span>
)}
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{step.step}</p>
<p className="text-xs text-[var(--text-tertiary)]">Tiempo: {step.timeSpent}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
{step.quality}%
</p>
<p className="text-xs text-[var(--text-tertiary)]">Calidad</p>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Comparación con la Industria</h3>
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Puntuación de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-[var(--color-success)]">{benchmarkComparison.yourData.onboardingScore}</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
</div>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Tasa de Completado</span>
<div className="text-right">
<span className="text-lg font-bold text-[var(--color-info)]">{benchmarkComparison.yourData.completionRate}%</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
</div>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
<span className="text-sm text-[var(--text-tertiary)] ml-2">vs {benchmarkComparison.industry.averageTime}</span>
</div>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-[var(--color-success)]">38% más rápido que el promedio de la industria</p>
</div>
</div>
</div>
</Card>
</div>
{/* Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Insights y Recomendaciones</h3>
<div className="space-y-4">
{insights.map((insight, index) => (
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
<div className="flex items-start space-x-3">
{getInsightIcon(insight.type)}
<div className="flex-1">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{insight.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-2">{insight.description}</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
Recomendación: {insight.recommendation}
</p>
</div>
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
</Badge>
</div>
</div>
))}
</div>
</Card>
{/* Data Analysis */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Análisis de Calidad de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataAnalysis.map((category, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-[var(--text-primary)]">{category.category}</h4>
<div className="flex items-center space-x-2">
<BarChart3 className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-tertiary)]">{category.items} elementos</span>
</div>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Completitud</span>
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.completeness}%` }}
></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>Precisión</span>
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.accuracy}%` }}
></div>
</div>
</div>
{category.issues > 0 && (
<div className="flex items-center text-sm text-[var(--color-error)]">
<AlertCircle className="w-4 h-4 mr-1" />
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
</div>
)}
<p className="text-xs text-[var(--text-secondary)]">{category.details}</p>
</div>
</div>
))}
</div>
</Card>
{/* Action Items */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Elementos de Acción</h3>
<div className="space-y-3">
{recommendations.map((rec, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-1">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span>Tiempo estimado: {rec.estimatedTime}</span>
<span></span>
<span>Impacto: {rec.impact}</span>
</div>
</div>
</div>
<Button size="sm">
Completar
</Button>
</div>
))}
</div>
</Card>
</div>
);
};
export default OnboardingAnalysisPage;

View File

@@ -0,0 +1,435 @@
import React, { useState } from 'react';
import { BarChart3, TrendingUp, Target, AlertCircle, CheckCircle, Eye, Download } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingAnalysisPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('30days');
const analysisData = {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días',
stepsCompleted: 15,
totalSteps: 16,
dataQuality: 94
};
const stepProgress = [
{ step: 'Información Básica', completed: true, quality: 95, timeSpent: '25 min' },
{ step: 'Configuración de Menú', completed: true, quality: 88, timeSpent: '1.2 horas' },
{ step: 'Datos de Inventario', completed: true, quality: 92, timeSpent: '45 min' },
{ step: 'Configuración de Horarios', completed: true, quality: 100, timeSpent: '15 min' },
{ step: 'Integración de Pagos', completed: true, quality: 85, timeSpent: '30 min' },
{ step: 'Carga de Productos', completed: true, quality: 90, timeSpent: '2.1 horas' },
{ step: 'Configuración de Personal', completed: true, quality: 87, timeSpent: '40 min' },
{ step: 'Pruebas del Sistema', completed: false, quality: 0, timeSpent: '-' }
];
const insights = [
{
type: 'success',
title: 'Excelente Progreso',
description: 'Has completado el 94% del proceso de configuración inicial',
recommendation: 'Solo faltan las pruebas finales del sistema para completar tu configuración',
impact: 'high'
},
{
type: 'info',
title: 'Calidad de Datos Alta',
description: 'Tus datos tienen una calidad promedio del 94%',
recommendation: 'Considera revisar la configuración de pagos para mejorar la puntuación',
impact: 'medium'
},
{
type: 'warning',
title: 'Paso Pendiente',
description: 'Las pruebas del sistema están pendientes',
recommendation: 'Programa las pruebas para validar la configuración completa',
impact: 'high'
}
];
const dataAnalysis = [
{
category: 'Información del Negocio',
completeness: 100,
accuracy: 95,
items: 12,
issues: 0,
details: 'Toda la información básica está completa y verificada'
},
{
category: 'Menú y Productos',
completeness: 85,
accuracy: 88,
items: 45,
issues: 3,
details: '3 productos sin precios definidos'
},
{
category: 'Inventario Inicial',
completeness: 92,
accuracy: 90,
items: 28,
issues: 2,
details: '2 ingredientes sin stock mínimo definido'
},
{
category: 'Configuración Operativa',
completeness: 100,
accuracy: 100,
items: 8,
issues: 0,
details: 'Horarios y políticas completamente configuradas'
}
];
const benchmarkComparison = {
industry: {
onboardingScore: 74,
completionRate: 78,
averageTime: '6.8 días'
},
yourData: {
onboardingScore: 87,
completionRate: 92,
averageTime: '4.2 días'
}
};
const recommendations = [
{
priority: 'high',
title: 'Completar Pruebas del Sistema',
description: 'Realizar pruebas integrales para validar toda la configuración',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento óptimo del sistema'
},
{
priority: 'medium',
title: 'Revisar Precios de Productos',
description: 'Definir precios para los 3 productos pendientes',
estimatedTime: '15 minutos',
impact: 'Permitirá generar ventas de todos los productos'
},
{
priority: 'medium',
title: 'Configurar Stocks Mínimos',
description: 'Establecer niveles mínimos para 2 ingredientes',
estimatedTime: '10 minutos',
impact: 'Mejorará el control de inventario automático'
},
{
priority: 'low',
title: 'Optimizar Configuración de Pagos',
description: 'Revisar métodos de pago y comisiones',
estimatedTime: '20 minutos',
impact: 'Puede reducir costos de transacción'
}
];
const getInsightIcon = (type: string) => {
const iconProps = { className: "w-5 h-5" };
switch (type) {
case 'success': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'info': return <Target {...iconProps} className="w-5 h-5 text-blue-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getInsightColor = (type: string) => {
switch (type) {
case 'success': return 'bg-green-50 border-green-200';
case 'warning': return 'bg-yellow-50 border-yellow-200';
case 'info': return 'bg-blue-50 border-blue-200';
default: return 'bg-gray-50 border-gray-200';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getCompletionColor = (percentage: number) => {
if (percentage >= 95) return 'text-green-600';
if (percentage >= 80) return 'text-yellow-600';
return 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Configuración"
description="Análisis detallado de tu proceso de configuración y recomendaciones"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
/>
{/* Overall Score */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-6">
<div className="text-center">
<div className="relative w-24 h-24 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-green-600">{analysisData.onboardingScore}</span>
</div>
<svg className="w-24 h-24 transform -rotate-90">
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
/>
<circle
cx="48"
cy="48"
r="40"
stroke="currentColor"
strokeWidth="8"
fill="none"
strokeDasharray={`${analysisData.onboardingScore * 2.51} 251`}
className="text-green-600"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">{analysisData.completionRate}%</p>
<p className="text-sm font-medium text-gray-700">Completado</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">{analysisData.averageTime}</p>
<p className="text-sm font-medium text-gray-700">Tiempo Promedio</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-orange-600">{analysisData.stepsCompleted}/{analysisData.totalSteps}</p>
<p className="text-sm font-medium text-gray-700">Pasos Completados</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-teal-600">{analysisData.dataQuality}%</p>
<p className="text-sm font-medium text-gray-700">Calidad de Datos</p>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp className="w-8 h-8 text-green-600" />
</div>
<p className="text-sm font-medium text-gray-700">Por encima del promedio</p>
</div>
</div>
</Card>
{/* Progress Analysis */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso por Pasos</h3>
<div className="space-y-4">
{stepProgress.map((step, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step.completed ? 'bg-green-100' : 'bg-gray-100'
}`}>
{step.completed ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<span className="text-sm font-medium text-gray-500">{index + 1}</span>
)}
</div>
<div>
<p className="text-sm font-medium text-gray-900">{step.step}</p>
<p className="text-xs text-gray-500">Tiempo: {step.timeSpent}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getCompletionColor(step.quality)}`}>
{step.quality}%
</p>
<p className="text-xs text-gray-500">Calidad</p>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Comparación con la Industria</h3>
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Puntuación de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-green-600">{benchmarkComparison.yourData.onboardingScore}</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.onboardingScore}</span>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(benchmarkComparison.yourData.onboardingScore / 100) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Tasa de Completado</span>
<div className="text-right">
<span className="text-lg font-bold text-blue-600">{benchmarkComparison.yourData.completionRate}%</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.completionRate}%</span>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${benchmarkComparison.yourData.completionRate}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Tiempo de Configuración</span>
<div className="text-right">
<span className="text-lg font-bold text-purple-600">{benchmarkComparison.yourData.averageTime}</span>
<span className="text-sm text-gray-500 ml-2">vs {benchmarkComparison.industry.averageTime}</span>
</div>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-700">38% más rápido que el promedio de la industria</p>
</div>
</div>
</div>
</Card>
</div>
{/* Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Insights y Recomendaciones</h3>
<div className="space-y-4">
{insights.map((insight, index) => (
<div key={index} className={`p-4 rounded-lg border ${getInsightColor(insight.type)}`}>
<div className="flex items-start space-x-3">
{getInsightIcon(insight.type)}
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 mb-1">{insight.title}</h4>
<p className="text-sm text-gray-700 mb-2">{insight.description}</p>
<p className="text-sm font-medium text-gray-900">
Recomendación: {insight.recommendation}
</p>
</div>
<Badge variant={insight.impact === 'high' ? 'red' : insight.impact === 'medium' ? 'yellow' : 'green'}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} Impacto
</Badge>
</div>
</div>
))}
</div>
</Card>
{/* Data Analysis */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Análisis de Calidad de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataAnalysis.map((category, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{category.category}</h4>
<div className="flex items-center space-x-2">
<BarChart3 className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-500">{category.items} elementos</span>
</div>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Completitud</span>
<span className={getCompletionColor(category.completeness)}>{category.completeness}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.completeness}%` }}
></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>Precisión</span>
<span className={getCompletionColor(category.accuracy)}>{category.accuracy}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${category.accuracy}%` }}
></div>
</div>
</div>
{category.issues > 0 && (
<div className="flex items-center text-sm text-red-600">
<AlertCircle className="w-4 h-4 mr-1" />
<span>{category.issues} problema{category.issues > 1 ? 's' : ''} detectado{category.issues > 1 ? 's' : ''}</span>
</div>
)}
<p className="text-xs text-gray-600">{category.details}</p>
</div>
</div>
))}
</div>
</Card>
{/* Action Items */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Elementos de Acción</h3>
<div className="space-y-3">
{recommendations.map((rec, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.priority === 'high' ? 'Alta' : rec.priority === 'medium' ? 'Media' : 'Baja'}
</Badge>
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
<p className="text-sm text-gray-600 mb-1">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>Tiempo estimado: {rec.estimatedTime}</span>
<span>•</span>
<span>Impacto: {rec.impact}</span>
</div>
</div>
</div>
<Button size="sm">
Completar
</Button>
</div>
))}
</div>
</Card>
</div>
);
};
export default OnboardingAnalysisPage;

View File

@@ -0,0 +1 @@
export { default as OnboardingAnalysisPage } from './OnboardingAnalysisPage';

View File

@@ -0,0 +1,579 @@
import React, { useState } from 'react';
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingReviewPage: React.FC = () => {
const [activeSection, setActiveSection] = useState<string>('overview');
const completionData = {
overallProgress: 95,
totalSteps: 8,
completedSteps: 7,
remainingSteps: 1,
estimatedTimeRemaining: '15 minutos',
overallScore: 87
};
const sectionReview = [
{
id: 'business-info',
title: 'Información del Negocio',
status: 'completed',
score: 98,
items: [
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
],
recommendations: []
},
{
id: 'menu-products',
title: 'Menú y Productos',
status: 'completed',
score: 85,
items: [
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
],
recommendations: [
'Completar precios para 3 productos pendientes',
'Añadir descripciones para 6 productos restantes'
]
},
{
id: 'inventory',
title: 'Inventario Inicial',
status: 'completed',
score: 92,
items: [
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Definir stocks iniciales para 2 ingredientes',
'Establecer puntos de reorden para 5 ingredientes'
]
},
{
id: 'staff-config',
title: 'Configuración de Personal',
status: 'completed',
score: 90,
items: [
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Completar horario para 1 empleado pendiente'
]
},
{
id: 'operations',
title: 'Configuración Operativa',
status: 'completed',
score: 95,
items: [
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
],
recommendations: []
},
{
id: 'integrations',
title: 'Integraciones',
status: 'completed',
score: 88,
items: [
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
],
recommendations: [
'Configurar API de delivery restante'
]
},
{
id: 'testing',
title: 'Pruebas del Sistema',
status: 'pending',
score: 0,
items: [
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
],
recommendations: [
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
]
},
{
id: 'training',
title: 'Capacitación del Equipo',
status: 'completed',
score: 82,
items: [
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
],
recommendations: [
'Completar capacitación para 2 empleados pendientes',
'Programar tercera sesión práctica',
'Realizar evaluaciones pendientes'
]
}
];
const overallRecommendations = [
{
priority: 'high',
category: 'Crítico',
title: 'Completar Pruebas del Sistema',
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento correcto del sistema'
},
{
priority: 'medium',
category: 'Importante',
title: 'Finalizar Configuración de Productos',
description: 'Completar precios y descripciones pendientes',
estimatedTime: '20 minutos',
impact: 'Permite ventas completas de todos los productos'
},
{
priority: 'medium',
category: 'Importante',
title: 'Completar Capacitación del Personal',
description: 'Finalizar entrenamiento para empleados pendientes',
estimatedTime: '45 minutos',
impact: 'Asegura operación eficiente desde el primer día'
},
{
priority: 'low',
category: 'Opcional',
title: 'Optimizar Configuración de Inventario',
description: 'Definir stocks y puntos de reorden pendientes',
estimatedTime: '15 minutos',
impact: 'Mejora control automático de inventario'
}
];
const launchReadiness = {
essential: {
completed: 6,
total: 7,
percentage: 86
},
recommended: {
completed: 8,
total: 12,
percentage: 67
},
optional: {
completed: 3,
total: 6,
percentage: 50
}
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-[var(--text-secondary)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'warning': return 'yellow';
case 'pending': return 'gray';
default: return 'red';
}
};
const getItemStatusIcon = (status: string) => {
const iconProps = { className: "w-4 h-4" };
switch (status) {
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-[var(--color-success)]" />;
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-[var(--text-secondary)]" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getScoreColor = (score: number) => {
if (score >= 90) return 'text-[var(--color-success)]';
if (score >= 80) return 'text-yellow-600';
if (score >= 70) return 'text-[var(--color-primary)]';
return 'text-[var(--color-error)]';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Revisión Final de Configuración"
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Edit2 className="w-4 h-4 mr-2" />
Editar Configuración
</Button>
<Button>
<Zap className="w-4 h-4 mr-2" />
Lanzar Sistema
</Button>
</div>
}
/>
{/* Overall Progress */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="relative w-20 h-20 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
{completionData.overallScore}
</span>
</div>
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
className="text-gray-200"
/>
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
className="text-[var(--color-success)]"
/>
</svg>
</div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-info)]">{completionData.overallProgress}%</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Progreso Total</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${completionData.overallProgress}%` }}
></div>
</div>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">
{completionData.completedSteps}/{completionData.totalSteps}
</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Secciones Completadas</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[var(--color-primary)]">{completionData.estimatedTimeRemaining}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Restante</p>
</div>
</div>
</Card>
{/* Navigation Tabs */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
<button
key={tab}
onClick={() => setActiveSection(tab)}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeSection === tab
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
{tab === 'overview' && 'Resumen General'}
{tab === 'sections' && 'Revisión por Secciones'}
{tab === 'recommendations' && 'Recomendaciones'}
{tab === 'readiness' && 'Preparación para Lanzamiento'}
</button>
))}
</nav>
</div>
{/* Content based on active section */}
{activeSection === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado por Secciones</h3>
<div className="space-y-3">
{sectionReview.map((section) => (
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{section.title}</p>
<p className="text-xs text-[var(--text-tertiary)]">
{section.recommendations.length > 0
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
: 'Completado correctamente'
}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
{section.score}%
</p>
<Badge variant={getStatusColor(section.status)}>
{section.status === 'completed' ? 'Completado' :
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
</Badge>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Próximos Pasos</h3>
<div className="space-y-4">
{overallRecommendations.slice(0, 3).map((rec, index) => (
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
{rec.category}
</Badge>
<div className="flex-1">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">{rec.description}</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-[var(--text-tertiary)]">
<span> {rec.estimatedTime}</span>
<span>💡 {rec.impact}</span>
</div>
</div>
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)] mt-1" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-[var(--color-info)]/5 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<Star className="w-5 h-5 text-[var(--color-info)]" />
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
</div>
<p className="text-sm text-[var(--color-info)]">
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
</p>
</div>
</Card>
</div>
)}
{activeSection === 'sections' && (
<div className="space-y-6">
{sectionReview.map((section) => (
<Card key={section.id} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{section.title}</h3>
<Badge variant={getStatusColor(section.status)}>
Puntuación: {section.score}%
</Badge>
</div>
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{section.items.map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-2">
{getItemStatusIcon(item.status)}
<span className="text-sm font-medium text-[var(--text-secondary)]">{item.field}</span>
</div>
<span className="text-sm text-[var(--text-secondary)]">{item.value}</span>
</div>
))}
</div>
{section.recommendations.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
<ul className="text-sm text-yellow-700 space-y-1">
{section.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<span className="mr-2"></span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</Card>
))}
</div>
)}
{activeSection === 'recommendations' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Todas las Recomendaciones</h3>
<div className="space-y-4">
{overallRecommendations.map((rec, index) => (
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.category}
</Badge>
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-1">{rec.title}</h4>
<p className="text-sm text-[var(--text-secondary)] mb-2">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span> Tiempo estimado: {rec.estimatedTime}</span>
<span>💡 Impacto: {rec.impact}</span>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">Más Información</Button>
<Button size="sm">Completar</Button>
</div>
</div>
))}
</div>
</Card>
)}
{activeSection === 'readiness' && (
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Preparación para el Lanzamiento</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-[var(--color-success)]" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Elementos Esenciales</h4>
<p className="text-2xl font-bold text-[var(--color-success)] mb-1">
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.essential.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${launchReadiness.essential.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
<Star className="w-8 h-8 text-yellow-600" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Recomendados</h4>
<p className="text-2xl font-bold text-yellow-600 mb-1">
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.recommended.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-yellow-600 h-2 rounded-full"
style={{ width: `${launchReadiness.recommended.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="w-8 h-8 text-[var(--color-info)]" />
</div>
<h4 className="font-medium text-[var(--text-primary)] mb-2">Opcionales</h4>
<p className="text-2xl font-bold text-[var(--color-info)] mb-1">
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
</p>
<p className="text-sm text-[var(--text-secondary)]">{launchReadiness.optional.percentage}% completado</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${launchReadiness.optional.percentage}%` }}
></div>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<div className="flex items-center space-x-3 mb-4">
<CheckCircle className="w-6 h-6 text-[var(--color-success)]" />
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
</div>
<p className="text-[var(--color-success)] mb-4">
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
y el sistema está preparado para comenzar a operar.
</p>
<div className="flex space-x-3">
<Button className="bg-green-600 hover:bg-green-700">
<Zap className="w-4 h-4 mr-2" />
Lanzar Ahora
</Button>
<Button variant="outline">
Ejecutar Pruebas Finales
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default OnboardingReviewPage;

View File

@@ -0,0 +1,579 @@
import React, { useState } from 'react';
import { CheckCircle, AlertCircle, Edit2, Eye, Star, ArrowRight, Clock, Users, Zap } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingReviewPage: React.FC = () => {
const [activeSection, setActiveSection] = useState<string>('overview');
const completionData = {
overallProgress: 95,
totalSteps: 8,
completedSteps: 7,
remainingSteps: 1,
estimatedTimeRemaining: '15 minutos',
overallScore: 87
};
const sectionReview = [
{
id: 'business-info',
title: 'Información del Negocio',
status: 'completed',
score: 98,
items: [
{ field: 'Nombre de la panadería', value: 'Panadería Artesanal El Buen Pan', status: 'complete' },
{ field: 'Dirección', value: 'Av. Principal 123, Centro', status: 'complete' },
{ field: 'Teléfono', value: '+1 234 567 8900', status: 'complete' },
{ field: 'Email', value: 'info@elbuenpan.com', status: 'complete' },
{ field: 'Tipo de negocio', value: 'Panadería y Pastelería Artesanal', status: 'complete' },
{ field: 'Horario de operación', value: 'L-V: 6:00-20:00, S-D: 7:00-18:00', status: 'complete' }
],
recommendations: []
},
{
id: 'menu-products',
title: 'Menú y Productos',
status: 'completed',
score: 85,
items: [
{ field: 'Productos de panadería', value: '24 productos configurados', status: 'complete' },
{ field: 'Productos de pastelería', value: '18 productos configurados', status: 'complete' },
{ field: 'Bebidas', value: '12 opciones disponibles', status: 'complete' },
{ field: 'Precios configurados', value: '51 de 54 productos (94%)', status: 'warning' },
{ field: 'Categorías organizadas', value: '6 categorías principales', status: 'complete' },
{ field: 'Descripciones', value: '48 de 54 productos (89%)', status: 'warning' }
],
recommendations: [
'Completar precios para 3 productos pendientes',
'Añadir descripciones para 6 productos restantes'
]
},
{
id: 'inventory',
title: 'Inventario Inicial',
status: 'completed',
score: 92,
items: [
{ field: 'Materias primas', value: '45 ingredientes registrados', status: 'complete' },
{ field: 'Proveedores', value: '8 proveedores configurados', status: 'complete' },
{ field: 'Stocks iniciales', value: '43 de 45 ingredientes (96%)', status: 'warning' },
{ field: 'Puntos de reorden', value: '40 de 45 ingredientes (89%)', status: 'warning' },
{ field: 'Costos unitarios', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Definir stocks iniciales para 2 ingredientes',
'Establecer puntos de reorden para 5 ingredientes'
]
},
{
id: 'staff-config',
title: 'Configuración de Personal',
status: 'completed',
score: 90,
items: [
{ field: 'Empleados registrados', value: '12 miembros del equipo', status: 'complete' },
{ field: 'Roles asignados', value: '5 roles diferentes configurados', status: 'complete' },
{ field: 'Horarios de trabajo', value: '11 de 12 empleados (92%)', status: 'warning' },
{ field: 'Permisos del sistema', value: 'Configurado completamente', status: 'complete' },
{ field: 'Datos de contacto', value: 'Completado al 100%', status: 'complete' }
],
recommendations: [
'Completar horario para 1 empleado pendiente'
]
},
{
id: 'operations',
title: 'Configuración Operativa',
status: 'completed',
score: 95,
items: [
{ field: 'Horarios de operación', value: 'Configurado completamente', status: 'complete' },
{ field: 'Métodos de pago', value: '4 métodos activos', status: 'complete' },
{ field: 'Políticas de devoluciones', value: 'Definidas y configuradas', status: 'complete' },
{ field: 'Configuración de impuestos', value: '18% IVA configurado', status: 'complete' },
{ field: 'Zonas de entrega', value: '3 zonas configuradas', status: 'complete' }
],
recommendations: []
},
{
id: 'integrations',
title: 'Integraciones',
status: 'completed',
score: 88,
items: [
{ field: 'Sistema de pagos', value: 'Stripe configurado', status: 'complete' },
{ field: 'Notificaciones por email', value: 'SMTP configurado', status: 'complete' },
{ field: 'Notificaciones SMS', value: 'Twilio configurado', status: 'complete' },
{ field: 'Backup automático', value: 'Configurado diariamente', status: 'complete' },
{ field: 'APIs externas', value: '2 de 3 configuradas', status: 'warning' }
],
recommendations: [
'Configurar API de delivery restante'
]
},
{
id: 'testing',
title: 'Pruebas del Sistema',
status: 'pending',
score: 0,
items: [
{ field: 'Prueba de ventas', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de inventario', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de reportes', value: 'Pendiente', status: 'pending' },
{ field: 'Prueba de notificaciones', value: 'Pendiente', status: 'pending' },
{ field: 'Validación de integraciones', value: 'Pendiente', status: 'pending' }
],
recommendations: [
'Ejecutar todas las pruebas del sistema antes del lanzamiento'
]
},
{
id: 'training',
title: 'Capacitación del Equipo',
status: 'completed',
score: 82,
items: [
{ field: 'Capacitación básica', value: '10 de 12 empleados (83%)', status: 'warning' },
{ field: 'Manual del usuario', value: 'Entregado y revisado', status: 'complete' },
{ field: 'Sesiones prácticas', value: '2 de 3 sesiones completadas', status: 'warning' },
{ field: 'Evaluaciones', value: '8 de 10 empleados aprobados', status: 'warning' },
{ field: 'Material de referencia', value: 'Disponible en el sistema', status: 'complete' }
],
recommendations: [
'Completar capacitación para 2 empleados pendientes',
'Programar tercera sesión práctica',
'Realizar evaluaciones pendientes'
]
}
];
const overallRecommendations = [
{
priority: 'high',
category: 'Crítico',
title: 'Completar Pruebas del Sistema',
description: 'Ejecutar todas las pruebas funcionales antes del lanzamiento',
estimatedTime: '30 minutos',
impact: 'Garantiza funcionamiento correcto del sistema'
},
{
priority: 'medium',
category: 'Importante',
title: 'Finalizar Configuración de Productos',
description: 'Completar precios y descripciones pendientes',
estimatedTime: '20 minutos',
impact: 'Permite ventas completas de todos los productos'
},
{
priority: 'medium',
category: 'Importante',
title: 'Completar Capacitación del Personal',
description: 'Finalizar entrenamiento para empleados pendientes',
estimatedTime: '45 minutos',
impact: 'Asegura operación eficiente desde el primer día'
},
{
priority: 'low',
category: 'Opcional',
title: 'Optimizar Configuración de Inventario',
description: 'Definir stocks y puntos de reorden pendientes',
estimatedTime: '15 minutos',
impact: 'Mejora control automático de inventario'
}
];
const launchReadiness = {
essential: {
completed: 6,
total: 7,
percentage: 86
},
recommended: {
completed: 8,
total: 12,
percentage: 67
},
optional: {
completed: 3,
total: 6,
percentage: 50
}
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <CheckCircle {...iconProps} className="w-5 h-5 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-5 h-5 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-5 h-5 text-gray-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'warning': return 'yellow';
case 'pending': return 'gray';
default: return 'red';
}
};
const getItemStatusIcon = (status: string) => {
const iconProps = { className: "w-4 h-4" };
switch (status) {
case 'complete': return <CheckCircle {...iconProps} className="w-4 h-4 text-green-600" />;
case 'warning': return <AlertCircle {...iconProps} className="w-4 h-4 text-yellow-600" />;
case 'pending': return <Clock {...iconProps} className="w-4 h-4 text-gray-600" />;
default: return <AlertCircle {...iconProps} />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'red';
case 'medium': return 'yellow';
case 'low': return 'green';
default: return 'gray';
}
};
const getScoreColor = (score: number) => {
if (score >= 90) return 'text-green-600';
if (score >= 80) return 'text-yellow-600';
if (score >= 70) return 'text-orange-600';
return 'text-red-600';
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Revisión Final de Configuración"
description="Verifica todos los aspectos de tu configuración antes del lanzamiento"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Edit2 className="w-4 h-4 mr-2" />
Editar Configuración
</Button>
<Button>
<Zap className="w-4 h-4 mr-2" />
Lanzar Sistema
</Button>
</div>
}
/>
{/* Overall Progress */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="relative w-20 h-20 mx-auto mb-3">
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${getScoreColor(completionData.overallScore)}`}>
{completionData.overallScore}
</span>
</div>
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
className="text-gray-200"
/>
<circle
cx="40"
cy="40"
r="32"
stroke="currentColor"
strokeWidth="6"
fill="none"
strokeDasharray={`${completionData.overallScore * 2.01} 201`}
className="text-green-600"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">Puntuación General</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">{completionData.overallProgress}%</p>
<p className="text-sm font-medium text-gray-700">Progreso Total</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${completionData.overallProgress}%` }}
></div>
</div>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-purple-600">
{completionData.completedSteps}/{completionData.totalSteps}
</p>
<p className="text-sm font-medium text-gray-700">Secciones Completadas</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-orange-600">{completionData.estimatedTimeRemaining}</p>
<p className="text-sm font-medium text-gray-700">Tiempo Restante</p>
</div>
</div>
</Card>
{/* Navigation Tabs */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{['overview', 'sections', 'recommendations', 'readiness'].map((tab) => (
<button
key={tab}
onClick={() => setActiveSection(tab)}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeSection === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab === 'overview' && 'Resumen General'}
{tab === 'sections' && 'Revisión por Secciones'}
{tab === 'recommendations' && 'Recomendaciones'}
{tab === 'readiness' && 'Preparación para Lanzamiento'}
</button>
))}
</nav>
</div>
{/* Content based on active section */}
{activeSection === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Estado por Secciones</h3>
<div className="space-y-3">
{sectionReview.map((section) => (
<div key={section.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<div>
<p className="text-sm font-medium text-gray-900">{section.title}</p>
<p className="text-xs text-gray-500">
{section.recommendations.length > 0
? `${section.recommendations.length} recomendación${section.recommendations.length > 1 ? 'es' : ''}`
: 'Completado correctamente'
}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${getScoreColor(section.score)}`}>
{section.score}%
</p>
<Badge variant={getStatusColor(section.status)}>
{section.status === 'completed' ? 'Completado' :
section.status === 'warning' ? 'Con observaciones' : 'Pendiente'}
</Badge>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Próximos Pasos</h3>
<div className="space-y-4">
{overallRecommendations.slice(0, 3).map((rec, index) => (
<div key={index} className="flex items-start space-x-3 p-3 border rounded-lg">
<Badge variant={getPriorityColor(rec.priority)} className="mt-1">
{rec.category}
</Badge>
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900">{rec.title}</h4>
<p className="text-sm text-gray-600 mt-1">{rec.description}</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
<span>⏱️ {rec.estimatedTime}</span>
<span>💡 {rec.impact}</span>
</div>
</div>
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<Star className="w-5 h-5 text-blue-600" />
<h4 className="font-medium text-blue-900">¡Excelente progreso!</h4>
</div>
<p className="text-sm text-blue-800">
Has completado el 95% de la configuración. Solo quedan algunos detalles finales antes del lanzamiento.
</p>
</div>
</Card>
</div>
)}
{activeSection === 'sections' && (
<div className="space-y-6">
{sectionReview.map((section) => (
<Card key={section.id} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{getStatusIcon(section.status)}
<h3 className="text-lg font-semibold text-gray-900">{section.title}</h3>
<Badge variant={getStatusColor(section.status)}>
Puntuación: {section.score}%
</Badge>
</div>
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{section.items.map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-2">
{getItemStatusIcon(item.status)}
<span className="text-sm font-medium text-gray-700">{item.field}</span>
</div>
<span className="text-sm text-gray-600">{item.value}</span>
</div>
))}
</div>
{section.recommendations.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 mb-2">Recomendaciones:</h4>
<ul className="text-sm text-yellow-700 space-y-1">
{section.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<span className="mr-2">•</span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</Card>
))}
</div>
)}
{activeSection === 'recommendations' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Todas las Recomendaciones</h3>
<div className="space-y-4">
{overallRecommendations.map((rec, index) => (
<div key={index} className="flex items-start justify-between p-4 border rounded-lg">
<div className="flex items-start space-x-3 flex-1">
<Badge variant={getPriorityColor(rec.priority)}>
{rec.category}
</Badge>
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">{rec.title}</h4>
<p className="text-sm text-gray-600 mb-2">{rec.description}</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>⏱️ Tiempo estimado: {rec.estimatedTime}</span>
<span>💡 Impacto: {rec.impact}</span>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">Más Información</Button>
<Button size="sm">Completar</Button>
</div>
</div>
))}
</div>
</Card>
)}
{activeSection === 'readiness' && (
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Preparación para el Lanzamiento</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Elementos Esenciales</h4>
<p className="text-2xl font-bold text-green-600 mb-1">
{launchReadiness.essential.completed}/{launchReadiness.essential.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.essential.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${launchReadiness.essential.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-yellow-100 rounded-full flex items-center justify-center">
<Star className="w-8 h-8 text-yellow-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Recomendados</h4>
<p className="text-2xl font-bold text-yellow-600 mb-1">
{launchReadiness.recommended.completed}/{launchReadiness.recommended.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.recommended.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-yellow-600 h-2 rounded-full"
style={{ width: `${launchReadiness.recommended.percentage}%` }}
></div>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="w-16 h-16 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-8 h-8 text-blue-600" />
</div>
<h4 className="font-medium text-gray-900 mb-2">Opcionales</h4>
<p className="text-2xl font-bold text-blue-600 mb-1">
{launchReadiness.optional.completed}/{launchReadiness.optional.total}
</p>
<p className="text-sm text-gray-600">{launchReadiness.optional.percentage}% completado</p>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${launchReadiness.optional.percentage}%` }}
></div>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<div className="flex items-center space-x-3 mb-4">
<CheckCircle className="w-6 h-6 text-green-600" />
<h4 className="text-lg font-semibold text-green-900">¡Listo para Lanzar!</h4>
</div>
<p className="text-green-800 mb-4">
Tu configuración está lista para el lanzamiento. Todos los elementos esenciales están completados
y el sistema está preparado para comenzar a operar.
</p>
<div className="flex space-x-3">
<Button className="bg-green-600 hover:bg-green-700">
<Zap className="w-4 h-4 mr-2" />
Lanzar Ahora
</Button>
<Button variant="outline">
Ejecutar Pruebas Finales
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default OnboardingReviewPage;

View File

@@ -0,0 +1 @@
export { default as OnboardingReviewPage } from './OnboardingReviewPage';

View File

@@ -0,0 +1,499 @@
import React, { useState } from 'react';
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingSetupPage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
bakery: {
name: '',
type: 'traditional',
size: 'medium',
location: '',
phone: '',
email: ''
},
team: {
ownerName: '',
teamSize: '5-10',
roles: [],
experience: 'intermediate'
},
operations: {
openingHours: {
start: '07:00',
end: '20:00'
},
daysOpen: 6,
specialties: [],
dailyProduction: 'medium'
},
goals: {
primaryGoals: [],
expectedRevenue: '',
timeline: '6months'
}
});
const steps = [
{
id: 1,
title: 'Información de la Panadería',
description: 'Detalles básicos sobre tu negocio',
icon: Store,
fields: ['name', 'type', 'location', 'contact']
},
{
id: 2,
title: 'Equipo y Personal',
description: 'Información sobre tu equipo de trabajo',
icon: Users,
fields: ['owner', 'teamSize', 'roles', 'experience']
},
{
id: 3,
title: 'Operaciones',
description: 'Horarios y especialidades de producción',
icon: Settings,
fields: ['hours', 'specialties', 'production']
},
{
id: 4,
title: 'Objetivos',
description: 'Metas y expectativas para tu panadería',
icon: Zap,
fields: ['goals', 'revenue', 'timeline']
}
];
const bakeryTypes = [
{ value: 'traditional', label: 'Panadería Tradicional' },
{ value: 'artisan', label: 'Panadería Artesanal' },
{ value: 'cafe', label: 'Panadería-Café' },
{ value: 'industrial', label: 'Producción Industrial' }
];
const specialties = [
{ value: 'bread', label: 'Pan Tradicional' },
{ value: 'pastries', label: 'Bollería' },
{ value: 'cakes', label: 'Tartas y Pasteles' },
{ value: 'cookies', label: 'Galletas' },
{ value: 'savory', label: 'Productos Salados' },
{ value: 'gluten-free', label: 'Sin Gluten' },
{ value: 'vegan', label: 'Vegano' },
{ value: 'organic', label: 'Orgánico' }
];
const businessGoals = [
{ value: 'increase-sales', label: 'Aumentar Ventas' },
{ value: 'reduce-waste', label: 'Reducir Desperdicios' },
{ value: 'improve-efficiency', label: 'Mejorar Eficiencia' },
{ value: 'expand-menu', label: 'Ampliar Menú' },
{ value: 'digital-presence', label: 'Presencia Digital' },
{ value: 'customer-loyalty', label: 'Fidelización de Clientes' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
};
const handleArrayToggle = (section: string, field: string, value: string) => {
setFormData(prev => {
const currentArray = prev[section as keyof typeof prev][field] || [];
const newArray = currentArray.includes(value)
? currentArray.filter((item: string) => item !== value)
: [...currentArray, value];
return {
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: newArray
}
};
});
};
const nextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleFinish = () => {
console.log('Onboarding completed:', formData);
// Handle completion logic
};
const isStepComplete = (stepId: number) => {
// Basic validation logic
switch (stepId) {
case 1:
return formData.bakery.name && formData.bakery.location;
case 2:
return formData.team.ownerName;
case 3:
return formData.operations.specialties.length > 0;
case 4:
return formData.goals.primaryGoals.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nombre de la Panadería *
</label>
<Input
value={formData.bakery.name}
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
placeholder="Ej: Panadería San Miguel"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Tipo de Panadería
</label>
<div className="grid grid-cols-2 gap-4">
{bakeryTypes.map((type) => (
<label key={type.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]">
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.bakery.type === type.value}
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
className="text-[var(--color-info)]"
/>
<span className="text-sm">{type.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Ubicación *
</label>
<Input
value={formData.bakery.location}
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
placeholder="Dirección completa"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Teléfono
</label>
<Input
value={formData.bakery.phone}
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
placeholder="+34 xxx xxx xxx"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Email
</label>
<Input
value={formData.bakery.email}
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
placeholder="contacto@panaderia.com"
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nombre del Propietario *
</label>
<Input
value={formData.team.ownerName}
onChange={(e) => handleInputChange('team', 'ownerName', e.target.value)}
placeholder="Tu nombre completo"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Tamaño del Equipo
</label>
<select
value={formData.team.teamSize}
onChange={(e) => handleInputChange('team', 'teamSize', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="1-2">Solo yo o 1-2 personas</option>
<option value="3-5">3-5 empleados</option>
<option value="5-10">5-10 empleados</option>
<option value="10+">Más de 10 empleados</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Experiencia en el Sector
</label>
<div className="space-y-2">
{[
{ value: 'beginner', label: 'Principiante (menos de 2 años)' },
{ value: 'intermediate', label: 'Intermedio (2-5 años)' },
{ value: 'experienced', label: 'Experimentado (5-10 años)' },
{ value: 'expert', label: 'Experto (más de 10 años)' }
].map((exp) => (
<label key={exp.value} className="flex items-center space-x-3">
<input
type="radio"
name="experience"
value={exp.value}
checked={formData.team.experience === exp.value}
onChange={(e) => handleInputChange('team', 'experience', e.target.value)}
className="text-[var(--color-info)]"
/>
<span className="text-sm">{exp.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Hora de Apertura
</label>
<input
type="time"
value={formData.operations.openingHours.start}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
start: e.target.value
})}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Hora de Cierre
</label>
<input
type="time"
value={formData.operations.openingHours.end}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
end: e.target.value
})}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Días de Operación por Semana
</label>
<select
value={formData.operations.daysOpen}
onChange={(e) => handleInputChange('operations', 'daysOpen', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value={5}>5 días</option>
<option value={6}>6 días</option>
<option value={7}>7 días</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">
Especialidades *
</label>
<div className="grid grid-cols-2 gap-3">
{specialties.map((specialty) => (
<label key={specialty.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]">
<input
type="checkbox"
checked={formData.operations.specialties.includes(specialty.value)}
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
className="text-[var(--color-info)] rounded"
/>
<span className="text-sm">{specialty.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">
Objetivos Principales *
</label>
<div className="grid grid-cols-2 gap-3">
{businessGoals.map((goal) => (
<label key={goal.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]">
<input
type="checkbox"
checked={formData.goals.primaryGoals.includes(goal.value)}
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
className="text-[var(--color-info)] rounded"
/>
<span className="text-sm">{goal.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Ingresos Mensuales Esperados (opcional)
</label>
<select
value={formData.goals.expectedRevenue}
onChange={(e) => handleInputChange('goals', 'expectedRevenue', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="">Seleccionar rango</option>
<option value="0-5000">Menos de 5,000</option>
<option value="5000-15000">5,000 - 15,000</option>
<option value="15000-30000">15,000 - 30,000</option>
<option value="30000+">Más de 30,000</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Plazo para Alcanzar Objetivos
</label>
<select
value={formData.goals.timeline}
onChange={(e) => handleInputChange('goals', 'timeline', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="3months">3 meses</option>
<option value="6months">6 meses</option>
<option value="1year">1 año</option>
<option value="2years">2 años o más</option>
</select>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="p-6 max-w-4xl mx-auto">
<PageHeader
title="Configuración Inicial"
description="Configura tu panadería paso a paso para comenzar"
/>
{/* Progress Steps */}
<Card className="p-6 mb-8">
<div className="flex items-center justify-between mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
step.id === currentStep
? 'bg-blue-600 text-white'
: step.id < currentStep || isStepComplete(step.id)
? 'bg-green-600 text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-secondary)]'
}`}>
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
<Check className="w-5 h-5" />
) : (
<step.icon className="w-5 h-5" />
)}
</div>
{index < steps.length - 1 && (
<div className={`w-full h-1 mx-4 ${
step.id < currentStep ? 'bg-green-600' : 'bg-[var(--bg-quaternary)]'
}`} />
)}
</div>
))}
</div>
<div className="text-center">
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
Paso {currentStep}: {steps[currentStep - 1].title}
</h2>
<p className="text-[var(--text-secondary)]">
{steps[currentStep - 1].description}
</p>
</div>
</Card>
{/* Step Content */}
<Card className="p-8 mb-8">
{renderStepContent()}
</Card>
{/* Navigation */}
<div className="flex justify-between">
<Button
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
>
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
{currentStep === steps.length ? (
<Button onClick={handleFinish}>
<Check className="w-4 h-4 mr-2" />
Finalizar Configuración
</Button>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
);
};
export default OnboardingSetupPage;

View File

@@ -0,0 +1,499 @@
import React, { useState } from 'react';
import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingSetupPage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
bakery: {
name: '',
type: 'traditional',
size: 'medium',
location: '',
phone: '',
email: ''
},
team: {
ownerName: '',
teamSize: '5-10',
roles: [],
experience: 'intermediate'
},
operations: {
openingHours: {
start: '07:00',
end: '20:00'
},
daysOpen: 6,
specialties: [],
dailyProduction: 'medium'
},
goals: {
primaryGoals: [],
expectedRevenue: '',
timeline: '6months'
}
});
const steps = [
{
id: 1,
title: 'Información de la Panadería',
description: 'Detalles básicos sobre tu negocio',
icon: Store,
fields: ['name', 'type', 'location', 'contact']
},
{
id: 2,
title: 'Equipo y Personal',
description: 'Información sobre tu equipo de trabajo',
icon: Users,
fields: ['owner', 'teamSize', 'roles', 'experience']
},
{
id: 3,
title: 'Operaciones',
description: 'Horarios y especialidades de producción',
icon: Settings,
fields: ['hours', 'specialties', 'production']
},
{
id: 4,
title: 'Objetivos',
description: 'Metas y expectativas para tu panadería',
icon: Zap,
fields: ['goals', 'revenue', 'timeline']
}
];
const bakeryTypes = [
{ value: 'traditional', label: 'Panadería Tradicional' },
{ value: 'artisan', label: 'Panadería Artesanal' },
{ value: 'cafe', label: 'Panadería-Café' },
{ value: 'industrial', label: 'Producción Industrial' }
];
const specialties = [
{ value: 'bread', label: 'Pan Tradicional' },
{ value: 'pastries', label: 'Bollería' },
{ value: 'cakes', label: 'Tartas y Pasteles' },
{ value: 'cookies', label: 'Galletas' },
{ value: 'savory', label: 'Productos Salados' },
{ value: 'gluten-free', label: 'Sin Gluten' },
{ value: 'vegan', label: 'Vegano' },
{ value: 'organic', label: 'Orgánico' }
];
const businessGoals = [
{ value: 'increase-sales', label: 'Aumentar Ventas' },
{ value: 'reduce-waste', label: 'Reducir Desperdicios' },
{ value: 'improve-efficiency', label: 'Mejorar Eficiencia' },
{ value: 'expand-menu', label: 'Ampliar Menú' },
{ value: 'digital-presence', label: 'Presencia Digital' },
{ value: 'customer-loyalty', label: 'Fidelización de Clientes' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
};
const handleArrayToggle = (section: string, field: string, value: string) => {
setFormData(prev => {
const currentArray = prev[section as keyof typeof prev][field] || [];
const newArray = currentArray.includes(value)
? currentArray.filter((item: string) => item !== value)
: [...currentArray, value];
return {
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: newArray
}
};
});
};
const nextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleFinish = () => {
console.log('Onboarding completed:', formData);
// Handle completion logic
};
const isStepComplete = (stepId: number) => {
// Basic validation logic
switch (stepId) {
case 1:
return formData.bakery.name && formData.bakery.location;
case 2:
return formData.team.ownerName;
case 3:
return formData.operations.specialties.length > 0;
case 4:
return formData.goals.primaryGoals.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la Panadería *
</label>
<Input
value={formData.bakery.name}
onChange={(e) => handleInputChange('bakery', 'name', e.target.value)}
placeholder="Ej: Panadería San Miguel"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Panadería
</label>
<div className="grid grid-cols-2 gap-4">
{bakeryTypes.map((type) => (
<label key={type.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.bakery.type === type.value}
onChange={(e) => handleInputChange('bakery', 'type', e.target.value)}
className="text-blue-600"
/>
<span className="text-sm">{type.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ubicación *
</label>
<Input
value={formData.bakery.location}
onChange={(e) => handleInputChange('bakery', 'location', e.target.value)}
placeholder="Dirección completa"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Teléfono
</label>
<Input
value={formData.bakery.phone}
onChange={(e) => handleInputChange('bakery', 'phone', e.target.value)}
placeholder="+34 xxx xxx xxx"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<Input
value={formData.bakery.email}
onChange={(e) => handleInputChange('bakery', 'email', e.target.value)}
placeholder="contacto@panaderia.com"
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre del Propietario *
</label>
<Input
value={formData.team.ownerName}
onChange={(e) => handleInputChange('team', 'ownerName', e.target.value)}
placeholder="Tu nombre completo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tamaño del Equipo
</label>
<select
value={formData.team.teamSize}
onChange={(e) => handleInputChange('team', 'teamSize', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="1-2">Solo yo o 1-2 personas</option>
<option value="3-5">3-5 empleados</option>
<option value="5-10">5-10 empleados</option>
<option value="10+">Más de 10 empleados</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Experiencia en el Sector
</label>
<div className="space-y-2">
{[
{ value: 'beginner', label: 'Principiante (menos de 2 años)' },
{ value: 'intermediate', label: 'Intermedio (2-5 años)' },
{ value: 'experienced', label: 'Experimentado (5-10 años)' },
{ value: 'expert', label: 'Experto (más de 10 años)' }
].map((exp) => (
<label key={exp.value} className="flex items-center space-x-3">
<input
type="radio"
name="experience"
value={exp.value}
checked={formData.team.experience === exp.value}
onChange={(e) => handleInputChange('team', 'experience', e.target.value)}
className="text-blue-600"
/>
<span className="text-sm">{exp.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora de Apertura
</label>
<input
type="time"
value={formData.operations.openingHours.start}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
start: e.target.value
})}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora de Cierre
</label>
<input
type="time"
value={formData.operations.openingHours.end}
onChange={(e) => handleInputChange('operations', 'openingHours', {
...formData.operations.openingHours,
end: e.target.value
})}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Días de Operación por Semana
</label>
<select
value={formData.operations.daysOpen}
onChange={(e) => handleInputChange('operations', 'daysOpen', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value={5}>5 días</option>
<option value={6}>6 días</option>
<option value={7}>7 días</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Especialidades *
</label>
<div className="grid grid-cols-2 gap-3">
{specialties.map((specialty) => (
<label key={specialty.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="checkbox"
checked={formData.operations.specialties.includes(specialty.value)}
onChange={() => handleArrayToggle('operations', 'specialties', specialty.value)}
className="text-blue-600 rounded"
/>
<span className="text-sm">{specialty.label}</span>
</label>
))}
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Objetivos Principales *
</label>
<div className="grid grid-cols-2 gap-3">
{businessGoals.map((goal) => (
<label key={goal.value} className="flex items-center space-x-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="checkbox"
checked={formData.goals.primaryGoals.includes(goal.value)}
onChange={() => handleArrayToggle('goals', 'primaryGoals', goal.value)}
className="text-blue-600 rounded"
/>
<span className="text-sm">{goal.label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ingresos Mensuales Esperados (opcional)
</label>
<select
value={formData.goals.expectedRevenue}
onChange={(e) => handleInputChange('goals', 'expectedRevenue', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="">Seleccionar rango</option>
<option value="0-5000">Menos de €5,000</option>
<option value="5000-15000">€5,000 - €15,000</option>
<option value="15000-30000">€15,000 - €30,000</option>
<option value="30000+">Más de €30,000</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Plazo para Alcanzar Objetivos
</label>
<select
value={formData.goals.timeline}
onChange={(e) => handleInputChange('goals', 'timeline', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="3months">3 meses</option>
<option value="6months">6 meses</option>
<option value="1year">1 año</option>
<option value="2years">2 años o más</option>
</select>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="p-6 max-w-4xl mx-auto">
<PageHeader
title="Configuración Inicial"
description="Configura tu panadería paso a paso para comenzar"
/>
{/* Progress Steps */}
<Card className="p-6 mb-8">
<div className="flex items-center justify-between mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
step.id === currentStep
? 'bg-blue-600 text-white'
: step.id < currentStep || isStepComplete(step.id)
? 'bg-green-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{step.id < currentStep || (step.id === currentStep && isStepComplete(step.id)) ? (
<Check className="w-5 h-5" />
) : (
<step.icon className="w-5 h-5" />
)}
</div>
{index < steps.length - 1 && (
<div className={`w-full h-1 mx-4 ${
step.id < currentStep ? 'bg-green-600' : 'bg-gray-200'
}`} />
)}
</div>
))}
</div>
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Paso {currentStep}: {steps[currentStep - 1].title}
</h2>
<p className="text-gray-600">
{steps[currentStep - 1].description}
</p>
</div>
</Card>
{/* Step Content */}
<Card className="p-8 mb-8">
{renderStepContent()}
</Card>
{/* Navigation */}
<div className="flex justify-between">
<Button
variant="outline"
onClick={prevStep}
disabled={currentStep === 1}
>
<ChevronLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
{currentStep === steps.length ? (
<Button onClick={handleFinish}>
<Check className="w-4 h-4 mr-2" />
Finalizar Configuración
</Button>
) : (
<Button
onClick={nextStep}
disabled={!isStepComplete(currentStep)}
>
Siguiente
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
);
};
export default OnboardingSetupPage;

View File

@@ -0,0 +1 @@
export { default as OnboardingSetupPage } from './OnboardingSetupPage';

View File

@@ -0,0 +1,438 @@
import React, { useState } from 'react';
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingUploadPage: React.FC = () => {
const [dragActive, setDragActive] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const uploadedFiles = [
{
id: '1',
name: 'productos_menu.csv',
type: 'productos',
size: '45 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:30:00',
records: 127,
errors: 3,
warnings: 8
},
{
id: '2',
name: 'inventario_inicial.xlsx',
type: 'inventario',
size: '82 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:25:00',
records: 89,
errors: 0,
warnings: 2
},
{
id: '3',
name: 'empleados.csv',
type: 'empleados',
size: '12 KB',
status: 'processing',
uploadedAt: '2024-01-26 10:35:00',
records: 8,
errors: 0,
warnings: 0
},
{
id: '4',
name: 'ventas_historicas.csv',
type: 'ventas',
size: '256 KB',
status: 'error',
uploadedAt: '2024-01-26 10:20:00',
records: 0,
errors: 1,
warnings: 0,
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
}
];
const supportedFormats = [
{
type: 'productos',
name: 'Productos y Menú',
formats: ['CSV', 'Excel'],
description: 'Lista de productos con precios, categorías y descripciones',
template: 'template_productos.csv',
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
},
{
type: 'inventario',
name: 'Inventario Inicial',
formats: ['CSV', 'Excel'],
description: 'Stock inicial de ingredientes y materias primas',
template: 'template_inventario.xlsx',
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
},
{
type: 'empleados',
name: 'Empleados',
formats: ['CSV'],
description: 'Información del personal y roles',
template: 'template_empleados.csv',
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
},
{
type: 'ventas',
name: 'Historial de Ventas',
formats: ['CSV'],
description: 'Datos históricos de ventas para análisis',
template: 'template_ventas.csv',
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
},
{
type: 'proveedores',
name: 'Proveedores',
formats: ['CSV', 'Excel'],
description: 'Lista de proveedores y datos de contacto',
template: 'template_proveedores.csv',
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
}
];
const uploadStats = {
totalFiles: uploadedFiles.length,
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-[var(--color-success)]" />;
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-[var(--color-info)] animate-spin" />;
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-[var(--color-error)]" />;
default: return <File {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'processing': return 'blue';
case 'error': return 'red';
default: return 'gray';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'completed': return 'Completado';
case 'processing': return 'Procesando';
case 'error': return 'Error';
default: return status;
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
handleFiles(files);
}
};
const handleFiles = (files: File[]) => {
console.log('Files selected:', files);
// Simulate upload progress
setIsProcessing(true);
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setIsProcessing(false);
return 100;
}
return prev + 10;
});
}, 200);
};
const downloadTemplate = (template: string) => {
console.log('Downloading template:', template);
// Handle template download
};
const retryUpload = (fileId: string) => {
console.log('Retrying upload for file:', fileId);
// Handle retry logic
};
const deleteFile = (fileId: string) => {
console.log('Deleting file:', fileId);
// Handle delete logic
};
const viewDetails = (fileId: string) => {
console.log('Viewing details for file:', fileId);
// Handle view details logic
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Carga de Datos"
description="Importa tus datos existentes para acelerar la configuración inicial"
/>
{/* Upload Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Archivos Subidos</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{uploadStats.totalFiles}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Upload className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{uploadStats.completedFiles}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Check className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Registros</p>
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<File className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Errores</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{uploadStats.totalErrors}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Advertencias</p>
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Upload Area */}
<Card className="p-8">
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragActive
? 'border-blue-400 bg-[var(--color-info)]/5'
: 'border-[var(--border-secondary)] hover:border-[var(--border-tertiary)]'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Arrastra archivos aquí o haz clic para seleccionar
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
</p>
<input
type="file"
multiple
accept=".csv,.xlsx,.xls"
onChange={handleFileInput}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button className="cursor-pointer">
Seleccionar Archivos
</Button>
</label>
{isProcessing && (
<div className="mt-4">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mb-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<p className="text-sm text-[var(--text-secondary)]">Procesando... {uploadProgress}%</p>
</div>
)}
</div>
</Card>
{/* Supported Formats */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Formatos Soportados</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]">{format.name}</h4>
<div className="flex space-x-1">
{format.formats.map((fmt, idx) => (
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
))}
</div>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-3">{format.description}</p>
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-secondary)] mb-1">Campos requeridos:</p>
<div className="flex flex-wrap gap-1">
{format.requiredFields.map((field, idx) => (
<span key={idx} className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded">
{field}
</span>
))}
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => downloadTemplate(format.template)}
>
<Download className="w-3 h-3 mr-2" />
Descargar Plantilla
</Button>
</div>
))}
</div>
</Card>
{/* Uploaded Files */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Archivos Cargados</h3>
<div className="space-y-3">
{uploadedFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-4 flex-1">
{getStatusIcon(file.status)}
<div>
<div className="flex items-center space-x-3 mb-1">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{file.name}</h4>
<Badge variant={getStatusColor(file.status)}>
{getStatusLabel(file.status)}
</Badge>
<span className="text-xs text-[var(--text-tertiary)]">{file.size}</span>
</div>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{file.records} registros</span>
{file.errors > 0 && (
<span className="text-[var(--color-error)]">{file.errors} errores</span>
)}
{file.warnings > 0 && (
<span className="text-yellow-600">{file.warnings} advertencias</span>
)}
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
</div>
{file.status === 'error' && file.errorMessage && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-[var(--color-error)]">
{file.errorMessage}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
<Eye className="w-3 h-3" />
</Button>
{file.status === 'error' && (
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
<RefreshCw className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</Card>
{/* Help Section */}
<Card className="p-6 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-[var(--color-info)]">
<ul className="space-y-2">
<li> Usa las plantillas proporcionadas para garantizar el formato correcto</li>
<li> Verifica que todos los campos requeridos estén completos</li>
<li> Los archivos CSV deben usar codificación UTF-8</li>
<li> Las fechas deben estar en formato DD/MM/YYYY</li>
</ul>
<ul className="space-y-2">
<li> Los precios deben usar punto (.) como separador decimal</li>
<li> Evita caracteres especiales en los nombres de productos</li>
<li> Mantén los nombres de archivos descriptivos</li>
<li> Puedes cargar múltiples archivos del mismo tipo</li>
</ul>
</div>
</Card>
</div>
);
};
export default OnboardingUploadPage;

View File

@@ -0,0 +1,438 @@
import React, { useState } from 'react';
import { Upload, File, Check, AlertCircle, Download, RefreshCw, Trash2, Eye } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const OnboardingUploadPage: React.FC = () => {
const [dragActive, setDragActive] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const uploadedFiles = [
{
id: '1',
name: 'productos_menu.csv',
type: 'productos',
size: '45 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:30:00',
records: 127,
errors: 3,
warnings: 8
},
{
id: '2',
name: 'inventario_inicial.xlsx',
type: 'inventario',
size: '82 KB',
status: 'completed',
uploadedAt: '2024-01-26 10:25:00',
records: 89,
errors: 0,
warnings: 2
},
{
id: '3',
name: 'empleados.csv',
type: 'empleados',
size: '12 KB',
status: 'processing',
uploadedAt: '2024-01-26 10:35:00',
records: 8,
errors: 0,
warnings: 0
},
{
id: '4',
name: 'ventas_historicas.csv',
type: 'ventas',
size: '256 KB',
status: 'error',
uploadedAt: '2024-01-26 10:20:00',
records: 0,
errors: 1,
warnings: 0,
errorMessage: 'Formato de fecha incorrecto en la columna "fecha_venta"'
}
];
const supportedFormats = [
{
type: 'productos',
name: 'Productos y Menú',
formats: ['CSV', 'Excel'],
description: 'Lista de productos con precios, categorías y descripciones',
template: 'template_productos.csv',
requiredFields: ['nombre', 'categoria', 'precio', 'descripcion']
},
{
type: 'inventario',
name: 'Inventario Inicial',
formats: ['CSV', 'Excel'],
description: 'Stock inicial de ingredientes y materias primas',
template: 'template_inventario.xlsx',
requiredFields: ['item', 'cantidad', 'unidad', 'costo_unitario']
},
{
type: 'empleados',
name: 'Empleados',
formats: ['CSV'],
description: 'Información del personal y roles',
template: 'template_empleados.csv',
requiredFields: ['nombre', 'cargo', 'email', 'telefono']
},
{
type: 'ventas',
name: 'Historial de Ventas',
formats: ['CSV'],
description: 'Datos históricos de ventas para análisis',
template: 'template_ventas.csv',
requiredFields: ['fecha', 'producto', 'cantidad', 'total']
},
{
type: 'proveedores',
name: 'Proveedores',
formats: ['CSV', 'Excel'],
description: 'Lista de proveedores y datos de contacto',
template: 'template_proveedores.csv',
requiredFields: ['nombre', 'contacto', 'telefono', 'email']
}
];
const uploadStats = {
totalFiles: uploadedFiles.length,
completedFiles: uploadedFiles.filter(f => f.status === 'completed').length,
totalRecords: uploadedFiles.reduce((sum, f) => sum + f.records, 0),
totalErrors: uploadedFiles.reduce((sum, f) => sum + f.errors, 0),
totalWarnings: uploadedFiles.reduce((sum, f) => sum + f.warnings, 0)
};
const getStatusIcon = (status: string) => {
const iconProps = { className: "w-5 h-5" };
switch (status) {
case 'completed': return <Check {...iconProps} className="w-5 h-5 text-green-600" />;
case 'processing': return <RefreshCw {...iconProps} className="w-5 h-5 text-blue-600 animate-spin" />;
case 'error': return <AlertCircle {...iconProps} className="w-5 h-5 text-red-600" />;
default: return <File {...iconProps} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'green';
case 'processing': return 'blue';
case 'error': return 'red';
default: return 'gray';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'completed': return 'Completado';
case 'processing': return 'Procesando';
case 'error': return 'Error';
default: return status;
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
handleFiles(files);
}
};
const handleFiles = (files: File[]) => {
console.log('Files selected:', files);
// Simulate upload progress
setIsProcessing(true);
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setIsProcessing(false);
return 100;
}
return prev + 10;
});
}, 200);
};
const downloadTemplate = (template: string) => {
console.log('Downloading template:', template);
// Handle template download
};
const retryUpload = (fileId: string) => {
console.log('Retrying upload for file:', fileId);
// Handle retry logic
};
const deleteFile = (fileId: string) => {
console.log('Deleting file:', fileId);
// Handle delete logic
};
const viewDetails = (fileId: string) => {
console.log('Viewing details for file:', fileId);
// Handle view details logic
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Carga de Datos"
description="Importa tus datos existentes para acelerar la configuración inicial"
/>
{/* Upload Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Archivos Subidos</p>
<p className="text-3xl font-bold text-blue-600">{uploadStats.totalFiles}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Upload className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completados</p>
<p className="text-3xl font-bold text-green-600">{uploadStats.completedFiles}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Check className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Registros</p>
<p className="text-3xl font-bold text-purple-600">{uploadStats.totalRecords}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<File className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Errores</p>
<p className="text-3xl font-bold text-red-600">{uploadStats.totalErrors}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Advertencias</p>
<p className="text-3xl font-bold text-yellow-600">{uploadStats.totalWarnings}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Upload Area */}
<Card className="p-8">
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragActive
? 'border-blue-400 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Arrastra archivos aquí o haz clic para seleccionar
</h3>
<p className="text-gray-600 mb-4">
Formatos soportados: CSV, Excel (XLSX). Tamaño máximo: 10MB por archivo
</p>
<input
type="file"
multiple
accept=".csv,.xlsx,.xls"
onChange={handleFileInput}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button className="cursor-pointer">
Seleccionar Archivos
</Button>
</label>
{isProcessing && (
<div className="mt-4">
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<p className="text-sm text-gray-600">Procesando... {uploadProgress}%</p>
</div>
)}
</div>
</Card>
{/* Supported Formats */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Formatos Soportados</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">{format.name}</h4>
<div className="flex space-x-1">
{format.formats.map((fmt, idx) => (
<Badge key={idx} variant="blue" className="text-xs">{fmt}</Badge>
))}
</div>
</div>
<p className="text-sm text-gray-600 mb-3">{format.description}</p>
<div className="mb-3">
<p className="text-xs font-medium text-gray-700 mb-1">Campos requeridos:</p>
<div className="flex flex-wrap gap-1">
{format.requiredFields.map((field, idx) => (
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
{field}
</span>
))}
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => downloadTemplate(format.template)}
>
<Download className="w-3 h-3 mr-2" />
Descargar Plantilla
</Button>
</div>
))}
</div>
</Card>
{/* Uploaded Files */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Archivos Cargados</h3>
<div className="space-y-3">
{uploadedFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-4 flex-1">
{getStatusIcon(file.status)}
<div>
<div className="flex items-center space-x-3 mb-1">
<h4 className="text-sm font-medium text-gray-900">{file.name}</h4>
<Badge variant={getStatusColor(file.status)}>
{getStatusLabel(file.status)}
</Badge>
<span className="text-xs text-gray-500">{file.size}</span>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<span>{file.records} registros</span>
{file.errors > 0 && (
<span className="text-red-600">{file.errors} errores</span>
)}
{file.warnings > 0 && (
<span className="text-yellow-600">{file.warnings} advertencias</span>
)}
<span>Subido: {new Date(file.uploadedAt).toLocaleString('es-ES')}</span>
</div>
{file.status === 'error' && file.errorMessage && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
{file.errorMessage}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => viewDetails(file.id)}>
<Eye className="w-3 h-3" />
</Button>
{file.status === 'error' && (
<Button size="sm" variant="outline" onClick={() => retryUpload(file.id)}>
<RefreshCw className="w-3 h-3" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => deleteFile(file.id)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</Card>
{/* Help Section */}
<Card className="p-6 bg-blue-50 border-blue-200">
<h3 className="text-lg font-semibold text-blue-900 mb-4">Tips para la Carga de Datos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800">
<ul className="space-y-2">
<li>• Usa las plantillas proporcionadas para garantizar el formato correcto</li>
<li>• Verifica que todos los campos requeridos estén completos</li>
<li>• Los archivos CSV deben usar codificación UTF-8</li>
<li>• Las fechas deben estar en formato DD/MM/YYYY</li>
</ul>
<ul className="space-y-2">
<li>• Los precios deben usar punto (.) como separador decimal</li>
<li>• Evita caracteres especiales en los nombres de productos</li>
<li>• Mantén los nombres de archivos descriptivos</li>
<li>• Puedes cargar múltiples archivos del mismo tipo</li>
</ul>
</div>
</Card>
</div>
);
};
export default OnboardingUploadPage;

View File

@@ -0,0 +1 @@
export { default as OnboardingUploadPage } from './OnboardingUploadPage';

View File

@@ -0,0 +1,6 @@
export * from './inventory';
export * from './production';
export * from './recipes';
export * from './procurement';
export * from './orders';
export * from './pos';

View File

@@ -0,0 +1,229 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const [filterCategory, setFilterCategory] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const mockInventoryItems = [
{
id: '1',
name: 'Harina de Trigo',
category: 'Harinas',
currentStock: 45,
minStock: 20,
maxStock: 100,
unit: 'kg',
cost: 1.20,
supplier: 'Molinos del Sur',
lastRestocked: '2024-01-20',
expirationDate: '2024-06-30',
status: 'normal',
},
{
id: '2',
name: 'Levadura Fresca',
category: 'Levaduras',
currentStock: 8,
minStock: 10,
maxStock: 25,
unit: 'kg',
cost: 8.50,
supplier: 'Levaduras SA',
lastRestocked: '2024-01-25',
expirationDate: '2024-02-15',
status: 'low',
},
{
id: '3',
name: 'Mantequilla',
category: 'Lácteos',
currentStock: 15,
minStock: 5,
maxStock: 30,
unit: 'kg',
cost: 5.80,
supplier: 'Lácteos Frescos',
lastRestocked: '2024-01-24',
expirationDate: '2024-02-10',
status: 'normal',
},
];
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
const stats = {
totalItems: mockInventoryItems.length,
lowStockItems: lowStockItems.length,
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
needsReorder: lowStockItems.length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Inventario"
description="Controla el stock de ingredientes y materias primas"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Artículo
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Artículos</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalItems}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Stock Bajo</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{stats.lowStockItems}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Valor Total</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{stats.totalValue.toFixed(2)}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-success)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Necesita Reorden</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.needsReorder}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
</div>
</Card>
</div>
{/* Low Stock Alert */}
{lowStockItems.length > 0 && (
<LowStockAlert items={lowStockItems} />
)}
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar artículos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="all">Todas las categorías</option>
<option value="Harinas">Harinas</option>
<option value="Levaduras">Levaduras</option>
<option value="Lácteos">Lácteos</option>
<option value="Grasas">Grasas</option>
<option value="Azúcares">Azúcares</option>
<option value="Especias">Especias</option>
</select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="all">Todos los estados</option>
<option value="normal">Stock normal</option>
<option value="low">Stock bajo</option>
<option value="out">Sin stock</option>
<option value="expired">Caducado</option>
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Más filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Inventory Table */}
<Card>
<InventoryTable
data={mockInventoryItems}
onEdit={(item) => {
setSelectedItem(item);
setShowForm(true);
}}
/>
</Card>
{/* Inventory Form Modal */}
{showForm && (
<InventoryForm
item={selectedItem}
onClose={() => {
setShowForm(false);
setSelectedItem(null);
}}
onSave={(item) => {
// Handle save logic
console.log('Saving item:', item);
setShowForm(false);
setSelectedItem(null);
}}
/>
)}
</div>
);
};
export default InventoryPage;

View File

@@ -0,0 +1,232 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const [filterCategory, setFilterCategory] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const mockInventoryItems = [
{
id: '1',
name: 'Harina de Trigo',
category: 'Harinas',
currentStock: 45,
minStock: 20,
maxStock: 100,
unit: 'kg',
cost: 1.20,
supplier: 'Molinos del Sur',
lastRestocked: '2024-01-20',
expirationDate: '2024-06-30',
status: 'normal',
},
{
id: '2',
name: 'Levadura Fresca',
category: 'Levaduras',
currentStock: 8,
minStock: 10,
maxStock: 25,
unit: 'kg',
cost: 8.50,
supplier: 'Levaduras SA',
lastRestocked: '2024-01-25',
expirationDate: '2024-02-15',
status: 'low',
},
{
id: '3',
name: 'Mantequilla',
category: 'Lácteos',
currentStock: 15,
minStock: 5,
maxStock: 30,
unit: 'kg',
cost: 5.80,
supplier: 'Lácteos Frescos',
lastRestocked: '2024-01-24',
expirationDate: '2024-02-10',
status: 'normal',
},
];
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
const stats = {
totalItems: mockInventoryItems.length,
lowStockItems: lowStockItems.length,
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
needsReorder: lowStockItems.length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Inventario"
description="Controla el stock de ingredientes y materias primas"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Artículo
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Artículos</p>
<p className="text-3xl font-bold text-gray-900">{stats.totalItems}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Stock Bajo</p>
<p className="text-3xl font-bold text-red-600">{stats.lowStockItems}</p>
</div>
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Valor Total</p>
<p className="text-3xl font-bold text-green-600">€{stats.totalValue.toFixed(2)}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Necesita Reorden</p>
<p className="text-3xl font-bold text-orange-600">{stats.needsReorder}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
</div>
</Card>
</div>
{/* Low Stock Alert */}
{lowStockItems.length > 0 && (
<LowStockAlert items={lowStockItems} />
)}
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar artículos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="all">Todas las categorías</option>
<option value="Harinas">Harinas</option>
<option value="Levaduras">Levaduras</option>
<option value="Lácteos">Lácteos</option>
<option value="Grasas">Grasas</option>
<option value="Azúcares">Azúcares</option>
<option value="Especias">Especias</option>
</select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
<option value="all">Todos los estados</option>
<option value="normal">Stock normal</option>
<option value="low">Stock bajo</option>
<option value="out">Sin stock</option>
<option value="expired">Caducado</option>
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Más filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Inventory Table */}
<Card>
<InventoryTable
items={mockInventoryItems}
searchTerm={searchTerm}
filterCategory={filterCategory}
filterStatus={filterStatus}
onEdit={(item) => {
setSelectedItem(item);
setShowForm(true);
}}
/>
</Card>
{/* Inventory Form Modal */}
{showForm && (
<InventoryForm
item={selectedItem}
onClose={() => {
setShowForm(false);
setSelectedItem(null);
}}
onSave={(item) => {
// Handle save logic
console.log('Saving item:', item);
setShowForm(false);
setSelectedItem(null);
}}
/>
)}
</div>
);
};
export default InventoryPage;

View File

@@ -0,0 +1 @@
export { default as InventoryPage } from './InventoryPage';

View File

@@ -0,0 +1,405 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { OrdersTable, OrderForm } from '../../../../components/domain/sales';
const OrdersPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedOrder, setSelectedOrder] = useState(null);
const mockOrders = [
{
id: 'ORD-2024-001',
customerName: 'María García',
customerEmail: 'maria@email.com',
customerPhone: '+34 600 123 456',
status: 'pending',
orderDate: '2024-01-26T09:30:00Z',
deliveryDate: '2024-01-26T16:00:00Z',
items: [
{ id: '1', name: 'Pan de Molde Integral', quantity: 2, price: 4.50, total: 9.00 },
{ id: '2', name: 'Croissants de Mantequilla', quantity: 6, price: 1.50, total: 9.00 },
],
subtotal: 18.00,
tax: 1.89,
discount: 0,
total: 19.89,
paymentMethod: 'card',
paymentStatus: 'pending',
deliveryMethod: 'pickup',
notes: 'Sin gluten por favor en el pan',
priority: 'normal',
},
{
id: 'ORD-2024-002',
customerName: 'Juan Pérez',
customerEmail: 'juan@email.com',
customerPhone: '+34 600 654 321',
status: 'completed',
orderDate: '2024-01-25T14:15:00Z',
deliveryDate: '2024-01-25T18:30:00Z',
items: [
{ id: '3', name: 'Tarta de Chocolate', quantity: 1, price: 25.00, total: 25.00 },
{ id: '4', name: 'Magdalenas', quantity: 12, price: 0.75, total: 9.00 },
],
subtotal: 34.00,
tax: 3.57,
discount: 2.00,
total: 35.57,
paymentMethod: 'cash',
paymentStatus: 'paid',
deliveryMethod: 'delivery',
notes: 'Cumpleaños - decoración especial',
priority: 'high',
},
{
id: 'ORD-2024-003',
customerName: 'Ana Martínez',
customerEmail: 'ana@email.com',
customerPhone: '+34 600 987 654',
status: 'in_progress',
orderDate: '2024-01-26T07:45:00Z',
deliveryDate: '2024-01-26T12:00:00Z',
items: [
{ id: '5', name: 'Baguettes Francesas', quantity: 4, price: 2.80, total: 11.20 },
{ id: '6', name: 'Empanadas', quantity: 8, price: 2.50, total: 20.00 },
],
subtotal: 31.20,
tax: 3.28,
discount: 0,
total: 34.48,
paymentMethod: 'transfer',
paymentStatus: 'paid',
deliveryMethod: 'pickup',
notes: '',
priority: 'normal',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
ready: { color: 'green', text: 'Listo' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
normal: { color: 'blue', text: 'Normal' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config?.color as any}>{config?.text || priority}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
failed: { color: 'red', text: 'Fallido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const filteredOrders = mockOrders.filter(order => {
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTab = activeTab === 'all' || order.status === activeTab;
return matchesSearch && matchesTab;
});
const stats = {
total: mockOrders.length,
pending: mockOrders.filter(o => o.status === 'pending').length,
inProgress: mockOrders.filter(o => o.status === 'in_progress').length,
completed: mockOrders.filter(o => o.status === 'completed').length,
totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0),
};
const tabs = [
{ id: 'all', label: 'Todos', count: stats.total },
{ id: 'pending', label: 'Pendientes', count: stats.pending },
{ id: 'in_progress', label: 'En Proceso', count: stats.inProgress },
{ id: 'ready', label: 'Listos', count: 0 },
{ id: 'completed', label: 'Completados', count: stats.completed },
];
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Pedidos"
description="Administra y controla todos los pedidos de tu panadería"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Pedido
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Pedidos</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{stats.total}</p>
</div>
<Package className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pendientes</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.pending}</p>
</div>
<Clock className="h-8 w-8 text-[var(--color-primary)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">En Proceso</p>
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.inProgress}</p>
</div>
<div className="h-8 w-8 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.completed}</p>
</div>
<div className="h-8 w-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-[var(--color-success)]" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ingresos</p>
<p className="text-2xl font-bold text-purple-600">{stats.totalRevenue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${
activeTab === tab.id
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
{tab.label}
{tab.count > 0 && (
<span className="ml-2 bg-[var(--bg-tertiary)] text-[var(--text-primary)] py-0.5 px-2.5 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar pedidos por cliente, ID o email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Fecha
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Orders Table */}
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Cliente
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredOrders.map((order) => (
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-[var(--text-primary)]">{order.id}</div>
<div className="text-xs text-[var(--text-tertiary)]">{order.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<User className="h-4 w-4 text-[var(--text-tertiary)] mr-2" />
<div>
<div className="text-sm font-medium text-[var(--text-primary)]">{order.customerName}</div>
<div className="text-xs text-[var(--text-tertiary)]">{order.customerEmail}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
<div className="text-xs text-[var(--text-tertiary)]">
{new Date(order.orderDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
<div className="text-xs text-[var(--text-tertiary)]">
{new Date(order.deliveryDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-[var(--text-primary)]">{order.total.toFixed(2)}</div>
<div className="text-xs text-[var(--text-tertiary)]">{order.items.length} artículos</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
<div className="text-xs text-[var(--text-tertiary)] capitalize">{order.paymentMethod}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* Order Form Modal */}
{showForm && (
<OrderForm
order={selectedOrder}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
}}
onSave={(order) => {
// Handle save logic
console.log('Saving order:', order);
setShowForm(false);
setSelectedOrder(null);
}}
/>
)}
</div>
);
};
export default OrdersPage;

View File

@@ -0,0 +1,405 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { OrdersTable, OrderForm } from '../../../../components/domain/sales';
const OrdersPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedOrder, setSelectedOrder] = useState(null);
const mockOrders = [
{
id: 'ORD-2024-001',
customerName: 'María García',
customerEmail: 'maria@email.com',
customerPhone: '+34 600 123 456',
status: 'pending',
orderDate: '2024-01-26T09:30:00Z',
deliveryDate: '2024-01-26T16:00:00Z',
items: [
{ id: '1', name: 'Pan de Molde Integral', quantity: 2, price: 4.50, total: 9.00 },
{ id: '2', name: 'Croissants de Mantequilla', quantity: 6, price: 1.50, total: 9.00 },
],
subtotal: 18.00,
tax: 1.89,
discount: 0,
total: 19.89,
paymentMethod: 'card',
paymentStatus: 'pending',
deliveryMethod: 'pickup',
notes: 'Sin gluten por favor en el pan',
priority: 'normal',
},
{
id: 'ORD-2024-002',
customerName: 'Juan Pérez',
customerEmail: 'juan@email.com',
customerPhone: '+34 600 654 321',
status: 'completed',
orderDate: '2024-01-25T14:15:00Z',
deliveryDate: '2024-01-25T18:30:00Z',
items: [
{ id: '3', name: 'Tarta de Chocolate', quantity: 1, price: 25.00, total: 25.00 },
{ id: '4', name: 'Magdalenas', quantity: 12, price: 0.75, total: 9.00 },
],
subtotal: 34.00,
tax: 3.57,
discount: 2.00,
total: 35.57,
paymentMethod: 'cash',
paymentStatus: 'paid',
deliveryMethod: 'delivery',
notes: 'Cumpleaños - decoración especial',
priority: 'high',
},
{
id: 'ORD-2024-003',
customerName: 'Ana Martínez',
customerEmail: 'ana@email.com',
customerPhone: '+34 600 987 654',
status: 'in_progress',
orderDate: '2024-01-26T07:45:00Z',
deliveryDate: '2024-01-26T12:00:00Z',
items: [
{ id: '5', name: 'Baguettes Francesas', quantity: 4, price: 2.80, total: 11.20 },
{ id: '6', name: 'Empanadas', quantity: 8, price: 2.50, total: 20.00 },
],
subtotal: 31.20,
tax: 3.28,
discount: 0,
total: 34.48,
paymentMethod: 'transfer',
paymentStatus: 'paid',
deliveryMethod: 'pickup',
notes: '',
priority: 'normal',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
ready: { color: 'green', text: 'Listo' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
normal: { color: 'blue', text: 'Normal' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config?.color as any}>{config?.text || priority}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
failed: { color: 'red', text: 'Fallido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const filteredOrders = mockOrders.filter(order => {
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTab = activeTab === 'all' || order.status === activeTab;
return matchesSearch && matchesTab;
});
const stats = {
total: mockOrders.length,
pending: mockOrders.filter(o => o.status === 'pending').length,
inProgress: mockOrders.filter(o => o.status === 'in_progress').length,
completed: mockOrders.filter(o => o.status === 'completed').length,
totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0),
};
const tabs = [
{ id: 'all', label: 'Todos', count: stats.total },
{ id: 'pending', label: 'Pendientes', count: stats.pending },
{ id: 'in_progress', label: 'En Proceso', count: stats.inProgress },
{ id: 'ready', label: 'Listos', count: 0 },
{ id: 'completed', label: 'Completados', count: stats.completed },
];
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Pedidos"
description="Administra y controla todos los pedidos de tu panadería"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Pedido
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
</div>
<Package className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendientes</p>
<p className="text-2xl font-bold text-orange-600">{stats.pending}</p>
</div>
<Clock className="h-8 w-8 text-orange-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">En Proceso</p>
<p className="text-2xl font-bold text-blue-600">{stats.inProgress}</p>
</div>
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completados</p>
<p className="text-2xl font-bold text-green-600">{stats.completed}</p>
</div>
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ingresos</p>
<p className="text-2xl font-bold text-purple-600">€{stats.totalRevenue.toFixed(2)}</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${
activeTab === tab.id
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
{tab.count > 0 && (
<span className="ml-2 bg-gray-100 text-gray-900 py-0.5 px-2.5 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar pedidos por cliente, ID o email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Fecha
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Orders Table */}
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cliente
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.id}</div>
<div className="text-xs text-gray-500">{order.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<User className="h-4 w-4 text-gray-400 mr-2" />
<div>
<div className="text-sm font-medium text-gray-900">{order.customerName}</div>
<div className="text-xs text-gray-500">{order.customerEmail}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
<div className="text-xs text-gray-500">
{new Date(order.orderDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
<div className="text-xs text-gray-500">
{new Date(order.deliveryDate).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">€{order.total.toFixed(2)}</div>
<div className="text-xs text-gray-500">{order.items.length} artículos</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
<div className="text-xs text-gray-500 capitalize">{order.paymentMethod}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* Order Form Modal */}
{showForm && (
<OrderForm
order={selectedOrder}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
}}
onSave={(order) => {
// Handle save logic
console.log('Saving order:', order);
setShowForm(false);
setSelectedOrder(null);
}}
/>
)}
</div>
);
};
export default OrdersPage;

View File

@@ -0,0 +1 @@
export { default as OrdersPage } from './OrdersPage';

View File

@@ -0,0 +1,368 @@
import React, { useState } from 'react';
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const POSPage: React.FC = () => {
const [cart, setCart] = useState<Array<{
id: string;
name: string;
price: number;
quantity: number;
category: string;
}>>([]);
const [selectedCategory, setSelectedCategory] = useState('all');
const [customerInfo, setCustomerInfo] = useState({
name: '',
email: '',
phone: '',
});
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
const [cashReceived, setCashReceived] = useState('');
const products = [
{
id: '1',
name: 'Pan de Molde Integral',
price: 4.50,
category: 'bread',
stock: 25,
image: '/api/placeholder/100/100',
},
{
id: '2',
name: 'Croissants de Mantequilla',
price: 1.50,
category: 'pastry',
stock: 32,
image: '/api/placeholder/100/100',
},
{
id: '3',
name: 'Baguette Francesa',
price: 2.80,
category: 'bread',
stock: 18,
image: '/api/placeholder/100/100',
},
{
id: '4',
name: 'Tarta de Chocolate',
price: 25.00,
category: 'cake',
stock: 8,
image: '/api/placeholder/100/100',
},
{
id: '5',
name: 'Magdalenas',
price: 0.75,
category: 'pastry',
stock: 48,
image: '/api/placeholder/100/100',
},
{
id: '6',
name: 'Empanadas',
price: 2.50,
category: 'other',
stock: 24,
image: '/api/placeholder/100/100',
},
];
const categories = [
{ id: 'all', name: 'Todos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
{ id: 'other', name: 'Otros' },
];
const filteredProducts = products.filter(product =>
selectedCategory === 'all' || product.category === selectedCategory
);
const addToCart = (product: typeof products[0]) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
return [...prevCart, {
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
category: product.category,
}];
}
});
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity <= 0) {
setCart(prevCart => prevCart.filter(item => item.id !== id));
} else {
setCart(prevCart =>
prevCart.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
};
const clearCart = () => {
setCart([]);
};
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const taxRate = 0.21; // 21% IVA
const tax = subtotal * taxRate;
const total = subtotal + tax;
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
const processPayment = () => {
if (cart.length === 0) return;
// Process payment logic here
console.log('Processing payment:', {
cart,
customerInfo,
paymentMethod,
total,
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
change: paymentMethod === 'cash' ? change : undefined,
});
// Clear cart after successful payment
setCart([]);
setCustomerInfo({ name: '', email: '', phone: '' });
setCashReceived('');
alert('Venta procesada exitosamente');
};
return (
<div className="p-6 h-screen flex flex-col">
<PageHeader
title="Punto de Venta"
description="Sistema de ventas integrado"
/>
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
{/* Products Section */}
<div className="lg:col-span-2 space-y-6">
{/* Categories */}
<div className="flex space-x-2 overflow-x-auto">
{categories.map(category => (
<Button
key={category.id}
variant={selectedCategory === category.id ? 'default' : 'outline'}
onClick={() => setSelectedCategory(category.id)}
className="whitespace-nowrap"
>
{category.name}
</Button>
))}
</div>
{/* Products Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredProducts.map(product => (
<Card
key={product.id}
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => addToCart(product)}
>
<img
src={product.image}
alt={product.name}
className="w-full h-20 object-cover rounded mb-3"
/>
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
<p className="text-lg font-bold text-[var(--color-success)]">{product.price.toFixed(2)}</p>
<p className="text-xs text-[var(--text-tertiary)]">Stock: {product.stock}</p>
</Card>
))}
</div>
</div>
{/* Cart and Checkout Section */}
<div className="space-y-6">
{/* Cart */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center">
<ShoppingCart className="w-5 h-5 mr-2" />
Carrito ({cart.length})
</h3>
{cart.length > 0 && (
<Button variant="outline" size="sm" onClick={clearCart}>
Limpiar
</Button>
)}
</div>
<div className="space-y-3 max-h-64 overflow-y-auto">
{cart.length === 0 ? (
<p className="text-[var(--text-tertiary)] text-center py-8">Carrito vacío</p>
) : (
cart.map(item => (
<div key={item.id} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded">
<div className="flex-1">
<h4 className="text-sm font-medium">{item.name}</h4>
<p className="text-xs text-[var(--text-tertiary)]">{item.price.toFixed(2)} c/u</p>
</div>
<div className="flex items-center space-x-2">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity - 1);
}}
>
<Minus className="w-3 h-3" />
</Button>
<span className="w-8 text-center text-sm">{item.quantity}</span>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity + 1);
}}
>
<Plus className="w-3 h-3" />
</Button>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium">{(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
))
)}
</div>
{cart.length > 0 && (
<div className="mt-4 pt-4 border-t">
<div className="space-y-2">
<div className="flex justify-between">
<span>Subtotal:</span>
<span>{subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>IVA (21%):</span>
<span>{tax.toFixed(2)}</span>
</div>
<div className="flex justify-between text-lg font-bold border-t pt-2">
<span>Total:</span>
<span>{total.toFixed(2)}</span>
</div>
</div>
</div>
)}
</Card>
{/* Customer Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<User className="w-5 h-5 mr-2" />
Cliente (Opcional)
</h3>
<div className="space-y-3">
<Input
placeholder="Nombre"
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
/>
<Input
placeholder="Email"
type="email"
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
/>
<Input
placeholder="Teléfono"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
/>
</div>
</Card>
{/* Payment */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Calculator className="w-5 h-5 mr-2" />
Método de Pago
</h3>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2">
<Button
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('cash')}
className="flex items-center justify-center"
>
<Banknote className="w-4 h-4 mr-1" />
Efectivo
</Button>
<Button
variant={paymentMethod === 'card' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('card')}
className="flex items-center justify-center"
>
<CreditCard className="w-4 h-4 mr-1" />
Tarjeta
</Button>
<Button
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('transfer')}
className="flex items-center justify-center"
>
Transferencia
</Button>
</div>
{paymentMethod === 'cash' && (
<div className="space-y-2">
<Input
placeholder="Efectivo recibido"
type="number"
step="0.01"
value={cashReceived}
onChange={(e) => setCashReceived(e.target.value)}
/>
{cashReceived && parseFloat(cashReceived) >= total && (
<div className="p-2 bg-green-50 rounded text-center">
<p className="text-sm text-[var(--color-success)]">
Cambio: <span className="font-bold">{change.toFixed(2)}</span>
</p>
</div>
)}
</div>
)}
<Button
onClick={processPayment}
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
className="w-full"
size="lg"
>
<Receipt className="w-5 h-5 mr-2" />
Procesar Venta - {total.toFixed(2)}
</Button>
</div>
</Card>
</div>
</div>
</div>
);
};
export default POSPage;

View File

@@ -0,0 +1,368 @@
import React, { useState } from 'react';
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const POSPage: React.FC = () => {
const [cart, setCart] = useState<Array<{
id: string;
name: string;
price: number;
quantity: number;
category: string;
}>>([]);
const [selectedCategory, setSelectedCategory] = useState('all');
const [customerInfo, setCustomerInfo] = useState({
name: '',
email: '',
phone: '',
});
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
const [cashReceived, setCashReceived] = useState('');
const products = [
{
id: '1',
name: 'Pan de Molde Integral',
price: 4.50,
category: 'bread',
stock: 25,
image: '/api/placeholder/100/100',
},
{
id: '2',
name: 'Croissants de Mantequilla',
price: 1.50,
category: 'pastry',
stock: 32,
image: '/api/placeholder/100/100',
},
{
id: '3',
name: 'Baguette Francesa',
price: 2.80,
category: 'bread',
stock: 18,
image: '/api/placeholder/100/100',
},
{
id: '4',
name: 'Tarta de Chocolate',
price: 25.00,
category: 'cake',
stock: 8,
image: '/api/placeholder/100/100',
},
{
id: '5',
name: 'Magdalenas',
price: 0.75,
category: 'pastry',
stock: 48,
image: '/api/placeholder/100/100',
},
{
id: '6',
name: 'Empanadas',
price: 2.50,
category: 'other',
stock: 24,
image: '/api/placeholder/100/100',
},
];
const categories = [
{ id: 'all', name: 'Todos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
{ id: 'other', name: 'Otros' },
];
const filteredProducts = products.filter(product =>
selectedCategory === 'all' || product.category === selectedCategory
);
const addToCart = (product: typeof products[0]) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
return [...prevCart, {
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
category: product.category,
}];
}
});
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity <= 0) {
setCart(prevCart => prevCart.filter(item => item.id !== id));
} else {
setCart(prevCart =>
prevCart.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
};
const clearCart = () => {
setCart([]);
};
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const taxRate = 0.21; // 21% IVA
const tax = subtotal * taxRate;
const total = subtotal + tax;
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
const processPayment = () => {
if (cart.length === 0) return;
// Process payment logic here
console.log('Processing payment:', {
cart,
customerInfo,
paymentMethod,
total,
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
change: paymentMethod === 'cash' ? change : undefined,
});
// Clear cart after successful payment
setCart([]);
setCustomerInfo({ name: '', email: '', phone: '' });
setCashReceived('');
alert('Venta procesada exitosamente');
};
return (
<div className="p-6 h-screen flex flex-col">
<PageHeader
title="Punto de Venta"
description="Sistema de ventas integrado"
/>
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
{/* Products Section */}
<div className="lg:col-span-2 space-y-6">
{/* Categories */}
<div className="flex space-x-2 overflow-x-auto">
{categories.map(category => (
<Button
key={category.id}
variant={selectedCategory === category.id ? 'default' : 'outline'}
onClick={() => setSelectedCategory(category.id)}
className="whitespace-nowrap"
>
{category.name}
</Button>
))}
</div>
{/* Products Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredProducts.map(product => (
<Card
key={product.id}
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => addToCart(product)}
>
<img
src={product.image}
alt={product.name}
className="w-full h-20 object-cover rounded mb-3"
/>
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
<p className="text-lg font-bold text-green-600">€{product.price.toFixed(2)}</p>
<p className="text-xs text-gray-500">Stock: {product.stock}</p>
</Card>
))}
</div>
</div>
{/* Cart and Checkout Section */}
<div className="space-y-6">
{/* Cart */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center">
<ShoppingCart className="w-5 h-5 mr-2" />
Carrito ({cart.length})
</h3>
{cart.length > 0 && (
<Button variant="outline" size="sm" onClick={clearCart}>
Limpiar
</Button>
)}
</div>
<div className="space-y-3 max-h-64 overflow-y-auto">
{cart.length === 0 ? (
<p className="text-gray-500 text-center py-8">Carrito vacío</p>
) : (
cart.map(item => (
<div key={item.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div className="flex-1">
<h4 className="text-sm font-medium">{item.name}</h4>
<p className="text-xs text-gray-500">€{item.price.toFixed(2)} c/u</p>
</div>
<div className="flex items-center space-x-2">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity - 1);
}}
>
<Minus className="w-3 h-3" />
</Button>
<span className="w-8 text-center text-sm">{item.quantity}</span>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
updateQuantity(item.id, item.quantity + 1);
}}
>
<Plus className="w-3 h-3" />
</Button>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium">€{(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
))
)}
</div>
{cart.length > 0 && (
<div className="mt-4 pt-4 border-t">
<div className="space-y-2">
<div className="flex justify-between">
<span>Subtotal:</span>
<span>€{subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>IVA (21%):</span>
<span>€{tax.toFixed(2)}</span>
</div>
<div className="flex justify-between text-lg font-bold border-t pt-2">
<span>Total:</span>
<span>€{total.toFixed(2)}</span>
</div>
</div>
</div>
)}
</Card>
{/* Customer Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<User className="w-5 h-5 mr-2" />
Cliente (Opcional)
</h3>
<div className="space-y-3">
<Input
placeholder="Nombre"
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
/>
<Input
placeholder="Email"
type="email"
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
/>
<Input
placeholder="Teléfono"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
/>
</div>
</Card>
{/* Payment */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Calculator className="w-5 h-5 mr-2" />
Método de Pago
</h3>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2">
<Button
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('cash')}
className="flex items-center justify-center"
>
<Banknote className="w-4 h-4 mr-1" />
Efectivo
</Button>
<Button
variant={paymentMethod === 'card' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('card')}
className="flex items-center justify-center"
>
<CreditCard className="w-4 h-4 mr-1" />
Tarjeta
</Button>
<Button
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
onClick={() => setPaymentMethod('transfer')}
className="flex items-center justify-center"
>
Transferencia
</Button>
</div>
{paymentMethod === 'cash' && (
<div className="space-y-2">
<Input
placeholder="Efectivo recibido"
type="number"
step="0.01"
value={cashReceived}
onChange={(e) => setCashReceived(e.target.value)}
/>
{cashReceived && parseFloat(cashReceived) >= total && (
<div className="p-2 bg-green-50 rounded text-center">
<p className="text-sm text-green-600">
Cambio: <span className="font-bold">€{change.toFixed(2)}</span>
</p>
</div>
)}
</div>
)}
<Button
onClick={processPayment}
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
className="w-full"
size="lg"
>
<Receipt className="w-5 h-5 mr-2" />
Procesar Venta - €{total.toFixed(2)}
</Button>
</div>
</Card>
</div>
</div>
</div>
);
};
export default POSPage;

View File

@@ -0,0 +1,449 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState('');
const mockPurchaseOrders = [
{
id: 'PO-2024-001',
supplier: 'Molinos del Sur',
status: 'pending',
orderDate: '2024-01-25',
deliveryDate: '2024-01-28',
totalAmount: 1250.00,
items: [
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
],
paymentStatus: 'pending',
notes: 'Entrega en horario de mañana',
},
{
id: 'PO-2024-002',
supplier: 'Levaduras SA',
status: 'delivered',
orderDate: '2024-01-20',
deliveryDate: '2024-01-23',
totalAmount: 425.50,
items: [
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
],
paymentStatus: 'paid',
notes: '',
},
{
id: 'PO-2024-003',
supplier: 'Lácteos Frescos',
status: 'in_transit',
orderDate: '2024-01-24',
deliveryDate: '2024-01-26',
totalAmount: 320.75,
items: [
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
],
paymentStatus: 'pending',
notes: 'Producto refrigerado',
},
];
const mockSuppliers = [
{
id: '1',
name: 'Molinos del Sur',
contact: 'Juan Pérez',
email: 'juan@molinosdelsur.com',
phone: '+34 91 234 5678',
category: 'Harinas',
rating: 4.8,
totalOrders: 24,
totalSpent: 15600.00,
paymentTerms: '30 días',
leadTime: '2-3 días',
location: 'Sevilla',
status: 'active',
},
{
id: '2',
name: 'Levaduras SA',
contact: 'María González',
email: 'maria@levaduras.com',
phone: '+34 93 456 7890',
category: 'Levaduras',
rating: 4.6,
totalOrders: 18,
totalSpent: 8450.00,
paymentTerms: '15 días',
leadTime: '1-2 días',
location: 'Barcelona',
status: 'active',
},
{
id: '3',
name: 'Lácteos Frescos',
contact: 'Carlos Ruiz',
email: 'carlos@lacteosfrescos.com',
phone: '+34 96 789 0123',
category: 'Lácteos',
rating: 4.4,
totalOrders: 32,
totalSpent: 12300.00,
paymentTerms: '20 días',
leadTime: '1 día',
location: 'Valencia',
status: 'active',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
approved: { color: 'blue', text: 'Aprobado' },
in_transit: { color: 'purple', text: 'En Tránsito' },
delivered: { color: 'green', text: 'Entregado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
overdue: { color: 'red', text: 'Vencido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const stats = {
totalOrders: mockPurchaseOrders.length,
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Compras"
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Compra
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Totales</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalOrders}</p>
</div>
<ShoppingCart className="h-12 w-12 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Pendientes</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.pendingOrders}</p>
</div>
<Calendar className="h-12 w-12 text-[var(--color-primary)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Gasto Total</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{stats.totalSpent.toLocaleString()}</p>
</div>
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Proveedores Activos</p>
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
</div>
<Truck className="h-12 w-12 text-purple-600" />
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('orders')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'orders'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Órdenes de Compra
</button>
<button
onClick={() => setActiveTab('suppliers')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'suppliers'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Proveedores
</button>
<button
onClick={() => setActiveTab('analytics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'analytics'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Análisis
</button>
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Tab Content */}
{activeTab === 'orders' && (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Orden
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Proveedor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Fecha Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Monto Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockPurchaseOrders.map((order) => (
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-[var(--text-primary)]">{order.id}</div>
{order.notes && (
<div className="text-xs text-[var(--text-tertiary)]">{order.notes}</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{order.supplier}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--text-primary)]">
{order.totalAmount.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button variant="outline" size="sm">Editar</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{activeTab === 'suppliers' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockSuppliers.map((supplier) => (
<Card key={supplier.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{supplier.name}</h3>
<p className="text-sm text-[var(--text-secondary)]">{supplier.category}</p>
</div>
<Badge variant="green">Activo</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Contacto:</span>
<span className="font-medium">{supplier.contact}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Email:</span>
<span className="font-medium">{supplier.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Teléfono:</span>
<span className="font-medium">{supplier.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Ubicación:</span>
<span className="font-medium">{supplier.location}</span>
</div>
</div>
<div className="border-t pt-4">
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-[var(--text-secondary)]">Valoración</p>
<p className="text-sm font-medium flex items-center">
<span className="text-yellow-500"></span>
<span className="ml-1">{supplier.rating}</span>
</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Pedidos</p>
<p className="text-sm font-medium">{supplier.totalOrders}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-[var(--text-secondary)]">Total Gastado</p>
<p className="text-sm font-medium">{supplier.totalSpent.toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Tiempo Entrega</p>
<p className="text-sm font-medium">{supplier.leadTime}</p>
</div>
</div>
<div className="mb-4">
<p className="text-xs text-[var(--text-secondary)]">Condiciones de Pago</p>
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Detalles
</Button>
<Button size="sm" className="flex-1">
Nuevo Pedido
</Button>
</div>
</div>
</Card>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Mes</h3>
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">Gráfico de gastos mensuales</p>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Top Proveedores</h3>
<div className="space-y-3">
{mockSuppliers
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 5)
.map((supplier, index) => (
<div key={supplier.id} className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-sm font-medium text-[var(--text-tertiary)] w-4">
{index + 1}.
</span>
<span className="ml-3 text-sm text-[var(--text-primary)]">{supplier.name}</span>
</div>
<span className="text-sm font-medium text-[var(--text-primary)]">
{supplier.totalSpent.toLocaleString()}
</span>
</div>
))}
</div>
</Card>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Categoría</h3>
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">Gráfico de gastos por categoría</p>
</div>
</Card>
</div>
)}
</div>
);
};
export default ProcurementPage;

View File

@@ -0,0 +1,449 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState('');
const mockPurchaseOrders = [
{
id: 'PO-2024-001',
supplier: 'Molinos del Sur',
status: 'pending',
orderDate: '2024-01-25',
deliveryDate: '2024-01-28',
totalAmount: 1250.00,
items: [
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
],
paymentStatus: 'pending',
notes: 'Entrega en horario de mañana',
},
{
id: 'PO-2024-002',
supplier: 'Levaduras SA',
status: 'delivered',
orderDate: '2024-01-20',
deliveryDate: '2024-01-23',
totalAmount: 425.50,
items: [
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
],
paymentStatus: 'paid',
notes: '',
},
{
id: 'PO-2024-003',
supplier: 'Lácteos Frescos',
status: 'in_transit',
orderDate: '2024-01-24',
deliveryDate: '2024-01-26',
totalAmount: 320.75,
items: [
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
],
paymentStatus: 'pending',
notes: 'Producto refrigerado',
},
];
const mockSuppliers = [
{
id: '1',
name: 'Molinos del Sur',
contact: 'Juan Pérez',
email: 'juan@molinosdelsur.com',
phone: '+34 91 234 5678',
category: 'Harinas',
rating: 4.8,
totalOrders: 24,
totalSpent: 15600.00,
paymentTerms: '30 días',
leadTime: '2-3 días',
location: 'Sevilla',
status: 'active',
},
{
id: '2',
name: 'Levaduras SA',
contact: 'María González',
email: 'maria@levaduras.com',
phone: '+34 93 456 7890',
category: 'Levaduras',
rating: 4.6,
totalOrders: 18,
totalSpent: 8450.00,
paymentTerms: '15 días',
leadTime: '1-2 días',
location: 'Barcelona',
status: 'active',
},
{
id: '3',
name: 'Lácteos Frescos',
contact: 'Carlos Ruiz',
email: 'carlos@lacteosfrescos.com',
phone: '+34 96 789 0123',
category: 'Lácteos',
rating: 4.4,
totalOrders: 32,
totalSpent: 12300.00,
paymentTerms: '20 días',
leadTime: '1 día',
location: 'Valencia',
status: 'active',
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
approved: { color: 'blue', text: 'Aprobado' },
in_transit: { color: 'purple', text: 'En Tránsito' },
delivered: { color: 'green', text: 'Entregado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
paid: { color: 'green', text: 'Pagado' },
overdue: { color: 'red', text: 'Vencido' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
};
const stats = {
totalOrders: mockPurchaseOrders.length,
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Compras"
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Compra
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Órdenes Totales</p>
<p className="text-3xl font-bold text-gray-900">{stats.totalOrders}</p>
</div>
<ShoppingCart className="h-12 w-12 text-blue-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Órdenes Pendientes</p>
<p className="text-3xl font-bold text-orange-600">{stats.pendingOrders}</p>
</div>
<Calendar className="h-12 w-12 text-orange-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Gasto Total</p>
<p className="text-3xl font-bold text-green-600">€{stats.totalSpent.toLocaleString()}</p>
</div>
<DollarSign className="h-12 w-12 text-green-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Proveedores Activos</p>
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
</div>
<Truck className="h-12 w-12 text-purple-600" />
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('orders')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'orders'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Órdenes de Compra
</button>
<button
onClick={() => setActiveTab('suppliers')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'suppliers'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Proveedores
</button>
<button
onClick={() => setActiveTab('analytics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'analytics'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Análisis
</button>
</nav>
</div>
{/* Search and Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
</Card>
{/* Tab Content */}
{activeTab === 'orders' && (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Orden
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Proveedor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Pedido
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Entrega
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Monto Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pago
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockPurchaseOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.id}</div>
{order.notes && (
<div className="text-xs text-gray-500">{order.notes}</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{order.supplier}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.orderDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
€{order.totalAmount.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPaymentStatusBadge(order.paymentStatus)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button variant="outline" size="sm">Editar</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{activeTab === 'suppliers' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockSuppliers.map((supplier) => (
<Card key={supplier.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">{supplier.name}</h3>
<p className="text-sm text-gray-600">{supplier.category}</p>
</div>
<Badge variant="green">Activo</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Contacto:</span>
<span className="font-medium">{supplier.contact}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Email:</span>
<span className="font-medium">{supplier.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Teléfono:</span>
<span className="font-medium">{supplier.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Ubicación:</span>
<span className="font-medium">{supplier.location}</span>
</div>
</div>
<div className="border-t pt-4">
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-600">Valoración</p>
<p className="text-sm font-medium flex items-center">
<span className="text-yellow-500">★</span>
<span className="ml-1">{supplier.rating}</span>
</p>
</div>
<div>
<p className="text-xs text-gray-600">Pedidos</p>
<p className="text-sm font-medium">{supplier.totalOrders}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-600">Total Gastado</p>
<p className="text-sm font-medium">€{supplier.totalSpent.toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-gray-600">Tiempo Entrega</p>
<p className="text-sm font-medium">{supplier.leadTime}</p>
</div>
</div>
<div className="mb-4">
<p className="text-xs text-gray-600">Condiciones de Pago</p>
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Detalles
</Button>
<Button size="sm" className="flex-1">
Nuevo Pedido
</Button>
</div>
</div>
</Card>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Mes</h3>
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<p className="text-gray-500">Gráfico de gastos mensuales</p>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Top Proveedores</h3>
<div className="space-y-3">
{mockSuppliers
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 5)
.map((supplier, index) => (
<div key={supplier.id} className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-sm font-medium text-gray-500 w-4">
{index + 1}.
</span>
<span className="ml-3 text-sm text-gray-900">{supplier.name}</span>
</div>
<span className="text-sm font-medium text-gray-900">
€{supplier.totalSpent.toLocaleString()}
</span>
</div>
))}
</div>
</Card>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Categoría</h3>
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<p className="text-gray-500">Gráfico de gastos por categoría</p>
</div>
</Card>
</div>
)}
</div>
);
};
export default ProcurementPage;

View File

@@ -0,0 +1,315 @@
import React, { useState } from 'react';
import { Plus, Calendar, Clock, Users, AlertCircle } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
const mockProductionStats = {
dailyTarget: 150,
completed: 85,
inProgress: 12,
pending: 53,
efficiency: 78,
quality: 94,
};
const mockProductionOrders = [
{
id: '1',
recipeName: 'Pan de Molde Integral',
quantity: 20,
status: 'in_progress',
priority: 'high',
assignedTo: 'Juan Panadero',
startTime: '2024-01-26T06:00:00Z',
estimatedCompletion: '2024-01-26T10:00:00Z',
progress: 65,
},
{
id: '2',
recipeName: 'Croissants de Mantequilla',
quantity: 50,
status: 'pending',
priority: 'medium',
assignedTo: 'María González',
startTime: '2024-01-26T08:00:00Z',
estimatedCompletion: '2024-01-26T12:00:00Z',
progress: 0,
},
{
id: '3',
recipeName: 'Baguettes Francesas',
quantity: 30,
status: 'completed',
priority: 'medium',
assignedTo: 'Carlos Ruiz',
startTime: '2024-01-26T04:00:00Z',
estimatedCompletion: '2024-01-26T08:00:00Z',
progress: 100,
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
medium: { color: 'yellow', text: 'Media' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
}
/>
{/* Production Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Meta Diaria</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{mockProductionStats.dailyTarget}</p>
</div>
<Calendar className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
<p className="text-2xl font-bold text-[var(--color-success)]">{mockProductionStats.completed}</p>
</div>
<div className="h-8 w-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-[var(--color-success)]" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">En Proceso</p>
<p className="text-2xl font-bold text-[var(--color-info)]">{mockProductionStats.inProgress}</p>
</div>
<Clock className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pendiente</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">{mockProductionStats.pending}</p>
</div>
<AlertCircle className="h-8 w-8 text-[var(--color-primary)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Eficiencia</p>
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad</p>
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('schedule')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'schedule'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Programación
</button>
<button
onClick={() => setActiveTab('batches')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'batches'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Lotes de Producción
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Control de Calidad
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'schedule' && (
<Card>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-[var(--text-primary)]">Órdenes de Producción</h3>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
Filtros
</Button>
<Button variant="outline" size="sm">
Vista Calendario
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Cantidad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Asignado a
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Progreso
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Tiempo Estimado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockProductionOrders.map((order) => (
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-[var(--text-primary)]">{order.recipeName}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{order.quantity} unidades
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Users className="h-4 w-4 text-[var(--text-tertiary)] mr-2" />
<span className="text-sm text-[var(--text-primary)]">{order.assignedTo}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${order.progress}%` }}
></div>
</div>
<span className="text-sm text-[var(--text-primary)]">{order.progress}%</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button variant="outline" size="sm" className="mr-2">
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
)}
{activeTab === 'batches' && (
<BatchTracker />
)}
{activeTab === 'quality' && (
<QualityControl />
)}
</div>
);
};
export default ProductionPage;

View File

@@ -0,0 +1,315 @@
import React, { useState } from 'react';
import { Plus, Calendar, Clock, Users, AlertCircle } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
const mockProductionStats = {
dailyTarget: 150,
completed: 85,
inProgress: 12,
pending: 53,
efficiency: 78,
quality: 94,
};
const mockProductionOrders = [
{
id: '1',
recipeName: 'Pan de Molde Integral',
quantity: 20,
status: 'in_progress',
priority: 'high',
assignedTo: 'Juan Panadero',
startTime: '2024-01-26T06:00:00Z',
estimatedCompletion: '2024-01-26T10:00:00Z',
progress: 65,
},
{
id: '2',
recipeName: 'Croissants de Mantequilla',
quantity: 50,
status: 'pending',
priority: 'medium',
assignedTo: 'María González',
startTime: '2024-01-26T08:00:00Z',
estimatedCompletion: '2024-01-26T12:00:00Z',
progress: 0,
},
{
id: '3',
recipeName: 'Baguettes Francesas',
quantity: 30,
status: 'completed',
priority: 'medium',
assignedTo: 'Carlos Ruiz',
startTime: '2024-01-26T04:00:00Z',
estimatedCompletion: '2024-01-26T08:00:00Z',
progress: 100,
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
medium: { color: 'yellow', text: 'Media' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
}
/>
{/* Production Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Meta Diaria</p>
<p className="text-2xl font-bold text-gray-900">{mockProductionStats.dailyTarget}</p>
</div>
<Calendar className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completado</p>
<p className="text-2xl font-bold text-green-600">{mockProductionStats.completed}</p>
</div>
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">En Proceso</p>
<p className="text-2xl font-bold text-blue-600">{mockProductionStats.inProgress}</p>
</div>
<Clock className="h-8 w-8 text-blue-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendiente</p>
<p className="text-2xl font-bold text-orange-600">{mockProductionStats.pending}</p>
</div>
<AlertCircle className="h-8 w-8 text-orange-600" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Eficiencia</p>
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Calidad</p>
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
</div>
{/* Tabs Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('schedule')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'schedule'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Programación
</button>
<button
onClick={() => setActiveTab('batches')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'batches'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Lotes de Producción
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Control de Calidad
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'schedule' && (
<Card>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-gray-900">Órdenes de Producción</h3>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
Filtros
</Button>
<Button variant="outline" size="sm">
Vista Calendario
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cantidad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prioridad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Asignado a
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Progreso
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tiempo Estimado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{mockProductionOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{order.recipeName}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{order.quantity} unidades
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getPriorityBadge(order.priority)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Users className="h-4 w-4 text-gray-400 mr-2" />
<span className="text-sm text-gray-900">{order.assignedTo}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-full bg-gray-200 rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${order.progress}%` }}
></div>
</div>
<span className="text-sm text-gray-900">{order.progress}%</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button variant="outline" size="sm" className="mr-2">
Ver
</Button>
<Button variant="outline" size="sm">
Editar
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
)}
{activeTab === 'batches' && (
<BatchTracker />
)}
{activeTab === 'quality' && (
<QualityControl />
)}
</div>
);
};
export default ProductionPage;

View File

@@ -0,0 +1 @@
export { default as ProductionPage } from './ProductionPage';

View File

@@ -0,0 +1,412 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const mockRecipes = [
{
id: '1',
name: 'Pan de Molde Integral',
category: 'bread',
difficulty: 'medium',
prepTime: 120,
bakingTime: 35,
yield: 1,
rating: 4.8,
cost: 2.50,
price: 4.50,
profit: 2.00,
image: '/api/placeholder/300/200',
tags: ['integral', 'saludable', 'artesanal'],
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
ingredients: [
{ name: 'Harina integral', quantity: 500, unit: 'g' },
{ name: 'Agua', quantity: 300, unit: 'ml' },
{ name: 'Levadura', quantity: 10, unit: 'g' },
{ name: 'Sal', quantity: 8, unit: 'g' },
],
},
{
id: '2',
name: 'Croissants de Mantequilla',
category: 'pastry',
difficulty: 'hard',
prepTime: 480,
bakingTime: 20,
yield: 12,
rating: 4.9,
cost: 8.50,
price: 18.00,
profit: 9.50,
image: '/api/placeholder/300/200',
tags: ['francés', 'mantequilla', 'hojaldrado'],
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
ingredients: [
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
{ name: 'Leche', quantity: 150, unit: 'ml' },
{ name: 'Azúcar', quantity: 50, unit: 'g' },
],
},
{
id: '3',
name: 'Tarta de Manzana',
category: 'cake',
difficulty: 'easy',
prepTime: 45,
bakingTime: 40,
yield: 8,
rating: 4.6,
cost: 4.20,
price: 12.00,
profit: 7.80,
image: '/api/placeholder/300/200',
tags: ['frutal', 'casera', 'temporada'],
description: 'Tarta casera de manzana con canela y masa quebrada.',
ingredients: [
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
{ name: 'Harina', quantity: 250, unit: 'g' },
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
{ name: 'Azúcar', quantity: 100, unit: 'g' },
],
},
];
const categories = [
{ value: 'all', label: 'Todas las categorías' },
{ value: 'bread', label: 'Panes' },
{ value: 'pastry', label: 'Bollería' },
{ value: 'cake', label: 'Tartas' },
{ value: 'cookie', label: 'Galletas' },
{ value: 'other', label: 'Otros' },
];
const difficulties = [
{ value: 'all', label: 'Todas las dificultades' },
{ value: 'easy', label: 'Fácil' },
{ value: 'medium', label: 'Medio' },
{ value: 'hard', label: 'Difícil' },
];
const getCategoryBadge = (category: string) => {
const categoryConfig = {
bread: { color: 'brown', text: 'Pan' },
pastry: { color: 'yellow', text: 'Bollería' },
cake: { color: 'pink', text: 'Tarta' },
cookie: { color: 'orange', text: 'Galleta' },
other: { color: 'gray', text: 'Otro' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'green', text: 'Fácil' },
medium: { color: 'yellow', text: 'Medio' },
hard: { color: 'red', text: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const filteredRecipes = mockRecipes.filter(recipe => {
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
return matchesSearch && matchesCategory && matchesDifficulty;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Recetas"
description="Administra y organiza todas las recetas de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Receta
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Recetas</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{mockRecipes.length}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Más Populares</p>
<p className="text-3xl font-bold text-yellow-600">
{mockRecipes.filter(r => r.rating > 4.7).length}
</p>
</div>
<Star className="h-12 w-12 text-yellow-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Costo Promedio</p>
<p className="text-3xl font-bold text-[var(--color-success)]">
{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Margen Promedio</p>
<p className="text-3xl font-bold text-purple-600">
{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{categories.map(cat => (
<option key={cat.value} value={cat.value}>{cat.label}</option>
))}
</select>
<select
value={selectedDifficulty}
onChange={(e) => setSelectedDifficulty(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
{difficulties.map(diff => (
<option key={diff.value} value={diff.value}>{diff.label}</option>
))}
</select>
<Button
variant="outline"
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
>
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
</Button>
</div>
</div>
</Card>
{/* Recipes Grid/List */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-w-16 aspect-h-9">
<img
src={recipe.image}
alt={recipe.name}
className="w-full h-48 object-cover"
/>
</div>
<div className="p-6">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)] line-clamp-1">
{recipe.name}
</h3>
<div className="flex items-center ml-2">
<Star className="h-4 w-4 text-yellow-400 fill-current" />
<span className="text-sm text-[var(--text-secondary)] ml-1">{recipe.rating}</span>
</div>
</div>
<p className="text-[var(--text-secondary)] text-sm mb-3 line-clamp-2">
{recipe.description}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-[var(--text-secondary)]">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 mr-1" />
<span>{recipe.yield} porciones</span>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<div className="text-sm">
<span className="text-[var(--text-secondary)]">Costo: </span>
<span className="font-medium">{recipe.cost.toFixed(2)}</span>
</div>
<div className="text-sm">
<span className="text-[var(--text-secondary)]">Precio: </span>
<span className="font-medium text-[var(--color-success)]">{recipe.price.toFixed(2)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Receta
</Button>
<Button size="sm" className="flex-1">
Producir
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Categoría
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Dificultad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Tiempo Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Rendimiento
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Costo
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Precio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Margen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecipes.map((recipe) => (
<tr key={recipe.id} className="hover:bg-[var(--bg-secondary)]">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
src={recipe.image}
alt={recipe.name}
className="h-10 w-10 rounded-full mr-4"
/>
<div>
<div className="text-sm font-medium text-[var(--text-primary)]">{recipe.name}</div>
<div className="flex items-center">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs text-[var(--text-tertiary)] ml-1">{recipe.rating}</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getCategoryBadge(recipe.category)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getDifficultyBadge(recipe.difficulty)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{recipe.yield} porciones
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{recipe.cost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--color-success)] font-medium">
{recipe.price.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
{recipe.profit.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button size="sm">Producir</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
};
export default RecipesPage;

View File

@@ -0,0 +1,412 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const mockRecipes = [
{
id: '1',
name: 'Pan de Molde Integral',
category: 'bread',
difficulty: 'medium',
prepTime: 120,
bakingTime: 35,
yield: 1,
rating: 4.8,
cost: 2.50,
price: 4.50,
profit: 2.00,
image: '/api/placeholder/300/200',
tags: ['integral', 'saludable', 'artesanal'],
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
ingredients: [
{ name: 'Harina integral', quantity: 500, unit: 'g' },
{ name: 'Agua', quantity: 300, unit: 'ml' },
{ name: 'Levadura', quantity: 10, unit: 'g' },
{ name: 'Sal', quantity: 8, unit: 'g' },
],
},
{
id: '2',
name: 'Croissants de Mantequilla',
category: 'pastry',
difficulty: 'hard',
prepTime: 480,
bakingTime: 20,
yield: 12,
rating: 4.9,
cost: 8.50,
price: 18.00,
profit: 9.50,
image: '/api/placeholder/300/200',
tags: ['francés', 'mantequilla', 'hojaldrado'],
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
ingredients: [
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
{ name: 'Leche', quantity: 150, unit: 'ml' },
{ name: 'Azúcar', quantity: 50, unit: 'g' },
],
},
{
id: '3',
name: 'Tarta de Manzana',
category: 'cake',
difficulty: 'easy',
prepTime: 45,
bakingTime: 40,
yield: 8,
rating: 4.6,
cost: 4.20,
price: 12.00,
profit: 7.80,
image: '/api/placeholder/300/200',
tags: ['frutal', 'casera', 'temporada'],
description: 'Tarta casera de manzana con canela y masa quebrada.',
ingredients: [
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
{ name: 'Harina', quantity: 250, unit: 'g' },
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
{ name: 'Azúcar', quantity: 100, unit: 'g' },
],
},
];
const categories = [
{ value: 'all', label: 'Todas las categorías' },
{ value: 'bread', label: 'Panes' },
{ value: 'pastry', label: 'Bollería' },
{ value: 'cake', label: 'Tartas' },
{ value: 'cookie', label: 'Galletas' },
{ value: 'other', label: 'Otros' },
];
const difficulties = [
{ value: 'all', label: 'Todas las dificultades' },
{ value: 'easy', label: 'Fácil' },
{ value: 'medium', label: 'Medio' },
{ value: 'hard', label: 'Difícil' },
];
const getCategoryBadge = (category: string) => {
const categoryConfig = {
bread: { color: 'brown', text: 'Pan' },
pastry: { color: 'yellow', text: 'Bollería' },
cake: { color: 'pink', text: 'Tarta' },
cookie: { color: 'orange', text: 'Galleta' },
other: { color: 'gray', text: 'Otro' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'green', text: 'Fácil' },
medium: { color: 'yellow', text: 'Medio' },
hard: { color: 'red', text: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const filteredRecipes = mockRecipes.filter(recipe => {
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
return matchesSearch && matchesCategory && matchesDifficulty;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Recetas"
description="Administra y organiza todas las recetas de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Receta
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Recetas</p>
<p className="text-3xl font-bold text-gray-900">{mockRecipes.length}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Más Populares</p>
<p className="text-3xl font-bold text-yellow-600">
{mockRecipes.filter(r => r.rating > 4.7).length}
</p>
</div>
<Star className="h-12 w-12 text-yellow-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Costo Promedio</p>
<p className="text-3xl font-bold text-green-600">
€{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<DollarSign className="h-12 w-12 text-green-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Margen Promedio</p>
<p className="text-3xl font-bold text-purple-600">
€{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{categories.map(cat => (
<option key={cat.value} value={cat.value}>{cat.label}</option>
))}
</select>
<select
value={selectedDifficulty}
onChange={(e) => setSelectedDifficulty(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{difficulties.map(diff => (
<option key={diff.value} value={diff.value}>{diff.label}</option>
))}
</select>
<Button
variant="outline"
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
>
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
</Button>
</div>
</div>
</Card>
{/* Recipes Grid/List */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-w-16 aspect-h-9">
<img
src={recipe.image}
alt={recipe.name}
className="w-full h-48 object-cover"
/>
</div>
<div className="p-6">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
{recipe.name}
</h3>
<div className="flex items-center ml-2">
<Star className="h-4 w-4 text-yellow-400 fill-current" />
<span className="text-sm text-gray-600 ml-1">{recipe.rating}</span>
</div>
</div>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{recipe.description}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-gray-600">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 mr-1" />
<span>{recipe.yield} porciones</span>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<div className="text-sm">
<span className="text-gray-600">Costo: </span>
<span className="font-medium">€{recipe.cost.toFixed(2)}</span>
</div>
<div className="text-sm">
<span className="text-gray-600">Precio: </span>
<span className="font-medium text-green-600">€{recipe.price.toFixed(2)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Receta
</Button>
<Button size="sm" className="flex-1">
Producir
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Categoría
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dificultad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tiempo Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rendimiento
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Costo
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Precio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Margen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecipes.map((recipe) => (
<tr key={recipe.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
src={recipe.image}
alt={recipe.name}
className="h-10 w-10 rounded-full mr-4"
/>
<div>
<div className="text-sm font-medium text-gray-900">{recipe.name}</div>
<div className="flex items-center">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs text-gray-500 ml-1">{recipe.rating}</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getCategoryBadge(recipe.category)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getDifficultyBadge(recipe.difficulty)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{recipe.yield} porciones
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
€{recipe.cost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 font-medium">
€{recipe.price.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
€{recipe.profit.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button size="sm">Producir</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
};
export default RecipesPage;

View File

@@ -0,0 +1 @@
export { default as RecipesPage } from './RecipesPage';

View File

@@ -0,0 +1,481 @@
import React, { useState } from 'react';
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const BakeryConfigPage: React.FC = () => {
const [config, setConfig] = useState({
general: {
name: 'Panadería Artesanal San Miguel',
description: 'Panadería tradicional con más de 30 años de experiencia',
logo: '',
website: 'https://panaderiasanmiguel.com',
email: 'info@panaderiasanmiguel.com',
phone: '+34 912 345 678'
},
location: {
address: 'Calle Mayor 123',
city: 'Madrid',
postalCode: '28001',
country: 'España',
coordinates: {
lat: 40.4168,
lng: -3.7038
}
},
schedule: {
monday: { open: '07:00', close: '20:00', closed: false },
tuesday: { open: '07:00', close: '20:00', closed: false },
wednesday: { open: '07:00', close: '20:00', closed: false },
thursday: { open: '07:00', close: '20:00', closed: false },
friday: { open: '07:00', close: '20:00', closed: false },
saturday: { open: '08:00', close: '14:00', closed: false },
sunday: { open: '09:00', close: '13:00', closed: false }
},
business: {
taxId: 'B12345678',
registrationNumber: 'REG-2024-001',
licenseNumber: 'LIC-FOOD-2024',
currency: 'EUR',
timezone: 'Europe/Madrid',
language: 'es'
},
preferences: {
enableOnlineOrders: true,
enableReservations: false,
enableDelivery: true,
deliveryRadius: 5,
minimumOrderAmount: 15.00,
enableLoyaltyProgram: true,
autoBackup: true,
emailNotifications: true,
smsNotifications: false
}
});
const [hasChanges, setHasChanges] = useState(false);
const [activeTab, setActiveTab] = useState('general');
const tabs = [
{ id: 'general', label: 'General', icon: Store },
{ id: 'location', label: 'Ubicación', icon: MapPin },
{ id: 'schedule', label: 'Horarios', icon: Clock },
{ id: 'business', label: 'Empresa', icon: Globe }
];
const daysOfWeek = [
{ key: 'monday', label: 'Lunes' },
{ key: 'tuesday', label: 'Martes' },
{ key: 'wednesday', label: 'Miércoles' },
{ key: 'thursday', label: 'Jueves' },
{ key: 'friday', label: 'Viernes' },
{ key: 'saturday', label: 'Sábado' },
{ key: 'sunday', label: 'Domingo' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
setHasChanges(true);
};
const handleScheduleChange = (day: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
schedule: {
...prev.schedule,
[day]: {
...prev.schedule[day as keyof typeof prev.schedule],
[field]: value
}
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving bakery config:', config);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Configuración de Panadería"
description="Configura los datos básicos y preferencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar */}
<div className="w-full lg:w-64">
<Card className="p-4">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
<tab.icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
))}
</nav>
</Card>
</div>
{/* Content */}
<div className="flex-1">
{activeTab === 'general' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Información General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nombre de la Panadería
</label>
<Input
value={config.general.name}
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
placeholder="Nombre de tu panadería"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Sitio Web
</label>
<Input
value={config.general.website}
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
placeholder="https://tu-panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Descripción
</label>
<textarea
value={config.general.description}
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="Describe tu panadería..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Email de Contacto
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
value={config.general.email}
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
className="pl-10"
type="email"
placeholder="contacto@panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Teléfono
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
value={config.general.phone}
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
className="pl-10"
type="tel"
placeholder="+34 912 345 678"
/>
</div>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'location' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Ubicación</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Dirección
</label>
<Input
value={config.location.address}
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
placeholder="Calle, número, etc."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Ciudad
</label>
<Input
value={config.location.city}
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
placeholder="Ciudad"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Código Postal
</label>
<Input
value={config.location.postalCode}
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
placeholder="28001"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
País
</label>
<Input
value={config.location.country}
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
placeholder="España"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Latitud
</label>
<Input
value={config.location.coordinates.lat}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lat: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="40.4168"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Longitud
</label>
<Input
value={config.location.coordinates.lng}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lng: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="-3.7038"
/>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'schedule' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Horarios de Apertura</h3>
<div className="space-y-4">
{daysOfWeek.map((day) => {
const schedule = config.schedule[day.key as keyof typeof config.schedule];
return (
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
<div className="w-20">
<span className="text-sm font-medium text-[var(--text-secondary)]">{day.label}</span>
</div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={schedule.closed}
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm text-[var(--text-secondary)]">Cerrado</span>
</label>
{!schedule.closed && (
<>
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Apertura</label>
<input
type="time"
value={schedule.open}
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Cierre</label>
<input
type="time"
value={schedule.close}
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
</>
)}
</div>
);
})}
</div>
</Card>
)}
{activeTab === 'business' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Datos de Empresa</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
NIF/CIF
</label>
<Input
value={config.business.taxId}
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
placeholder="B12345678"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Número de Registro
</label>
<Input
value={config.business.registrationNumber}
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
placeholder="REG-2024-001"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Licencia Sanitaria
</label>
<Input
value={config.business.licenseNumber}
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
placeholder="LIC-FOOD-2024"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Moneda
</label>
<select
value={config.business.currency}
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="EUR">EUR ()</option>
<option value="USD">USD ($)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Zona Horaria
</label>
<select
value={config.business.timezone}
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="Europe/Madrid">Madrid (GMT+1)</option>
<option value="Europe/London">Londres (GMT)</option>
<option value="America/New_York">Nueva York (GMT-5)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Idioma
</label>
<select
value={config.business.language}
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
</div>
</div>
</Card>
)}
</div>
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default BakeryConfigPage;

View File

@@ -0,0 +1,481 @@
import React, { useState } from 'react';
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
import { Button, Card, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const BakeryConfigPage: React.FC = () => {
const [config, setConfig] = useState({
general: {
name: 'Panadería Artesanal San Miguel',
description: 'Panadería tradicional con más de 30 años de experiencia',
logo: '',
website: 'https://panaderiasanmiguel.com',
email: 'info@panaderiasanmiguel.com',
phone: '+34 912 345 678'
},
location: {
address: 'Calle Mayor 123',
city: 'Madrid',
postalCode: '28001',
country: 'España',
coordinates: {
lat: 40.4168,
lng: -3.7038
}
},
schedule: {
monday: { open: '07:00', close: '20:00', closed: false },
tuesday: { open: '07:00', close: '20:00', closed: false },
wednesday: { open: '07:00', close: '20:00', closed: false },
thursday: { open: '07:00', close: '20:00', closed: false },
friday: { open: '07:00', close: '20:00', closed: false },
saturday: { open: '08:00', close: '14:00', closed: false },
sunday: { open: '09:00', close: '13:00', closed: false }
},
business: {
taxId: 'B12345678',
registrationNumber: 'REG-2024-001',
licenseNumber: 'LIC-FOOD-2024',
currency: 'EUR',
timezone: 'Europe/Madrid',
language: 'es'
},
preferences: {
enableOnlineOrders: true,
enableReservations: false,
enableDelivery: true,
deliveryRadius: 5,
minimumOrderAmount: 15.00,
enableLoyaltyProgram: true,
autoBackup: true,
emailNotifications: true,
smsNotifications: false
}
});
const [hasChanges, setHasChanges] = useState(false);
const [activeTab, setActiveTab] = useState('general');
const tabs = [
{ id: 'general', label: 'General', icon: Store },
{ id: 'location', label: 'Ubicación', icon: MapPin },
{ id: 'schedule', label: 'Horarios', icon: Clock },
{ id: 'business', label: 'Empresa', icon: Globe }
];
const daysOfWeek = [
{ key: 'monday', label: 'Lunes' },
{ key: 'tuesday', label: 'Martes' },
{ key: 'wednesday', label: 'Miércoles' },
{ key: 'thursday', label: 'Jueves' },
{ key: 'friday', label: 'Viernes' },
{ key: 'saturday', label: 'Sábado' },
{ key: 'sunday', label: 'Domingo' }
];
const handleInputChange = (section: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
setHasChanges(true);
};
const handleScheduleChange = (day: string, field: string, value: any) => {
setConfig(prev => ({
...prev,
schedule: {
...prev.schedule,
[day]: {
...prev.schedule[day as keyof typeof prev.schedule],
[field]: value
}
}
}));
setHasChanges(true);
};
const handleSave = () => {
// Handle save logic
console.log('Saving bakery config:', config);
setHasChanges(false);
};
const handleReset = () => {
// Reset to defaults
setHasChanges(false);
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Configuración de Panadería"
description="Configura los datos básicos y preferencias de tu panadería"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar */}
<div className="w-full lg:w-64">
<Card className="p-4">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<tab.icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
))}
</nav>
</Card>
</div>
{/* Content */}
<div className="flex-1">
{activeTab === 'general' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Información General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la Panadería
</label>
<Input
value={config.general.name}
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
placeholder="Nombre de tu panadería"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sitio Web
</label>
<Input
value={config.general.website}
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
placeholder="https://tu-panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Descripción
</label>
<textarea
value={config.general.description}
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="Describe tu panadería..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email de Contacto
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
value={config.general.email}
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
className="pl-10"
type="email"
placeholder="contacto@panaderia.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Teléfono
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
value={config.general.phone}
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
className="pl-10"
type="tel"
placeholder="+34 912 345 678"
/>
</div>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'location' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Ubicación</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dirección
</label>
<Input
value={config.location.address}
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
placeholder="Calle, número, etc."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ciudad
</label>
<Input
value={config.location.city}
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
placeholder="Ciudad"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Código Postal
</label>
<Input
value={config.location.postalCode}
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
placeholder="28001"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
País
</label>
<Input
value={config.location.country}
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
placeholder="España"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Latitud
</label>
<Input
value={config.location.coordinates.lat}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lat: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="40.4168"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Longitud
</label>
<Input
value={config.location.coordinates.lng}
onChange={(e) => handleInputChange('location', 'coordinates', {
...config.location.coordinates,
lng: parseFloat(e.target.value) || 0
})}
type="number"
step="0.000001"
placeholder="-3.7038"
/>
</div>
</div>
</div>
</Card>
)}
{activeTab === 'schedule' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Horarios de Apertura</h3>
<div className="space-y-4">
{daysOfWeek.map((day) => {
const schedule = config.schedule[day.key as keyof typeof config.schedule];
return (
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
<div className="w-20">
<span className="text-sm font-medium text-gray-700">{day.label}</span>
</div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={schedule.closed}
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-600">Cerrado</span>
</label>
{!schedule.closed && (
<>
<div>
<label className="block text-xs text-gray-500 mb-1">Apertura</label>
<input
type="time"
value={schedule.open}
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Cierre</label>
<input
type="time"
value={schedule.close}
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
/>
</div>
</>
)}
</div>
);
})}
</div>
</Card>
)}
{activeTab === 'business' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">Datos de Empresa</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
NIF/CIF
</label>
<Input
value={config.business.taxId}
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
placeholder="B12345678"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Número de Registro
</label>
<Input
value={config.business.registrationNumber}
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
placeholder="REG-2024-001"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Licencia Sanitaria
</label>
<Input
value={config.business.licenseNumber}
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
placeholder="LIC-FOOD-2024"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Moneda
</label>
<select
value={config.business.currency}
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="EUR">EUR (€)</option>
<option value="USD">USD ($)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Zona Horaria
</label>
<select
value={config.business.timezone}
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="Europe/Madrid">Madrid (GMT+1)</option>
<option value="Europe/London">Londres (GMT)</option>
<option value="America/New_York">Nueva York (GMT-5)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Idioma
</label>
<select
value={config.business.language}
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
</div>
</div>
</Card>
)}
</div>
</div>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default BakeryConfigPage;

View File

@@ -0,0 +1 @@
export { default as BakeryConfigPage } from './BakeryConfigPage';

View File

@@ -0,0 +1,591 @@
import React, { useState } from 'react';
import { Settings, Shield, Database, Bell, Wifi, HardDrive, Activity, Save, RotateCcw, AlertTriangle } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const SystemSettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('general');
const [hasChanges, setHasChanges] = useState(false);
const [settings, setSettings] = useState({
general: {
systemName: 'Bakery-IA Sistema',
version: '2.1.0',
environment: 'production',
timezone: 'Europe/Madrid',
language: 'es',
currency: 'EUR',
dateFormat: 'dd/mm/yyyy',
autoUpdates: true,
maintenanceMode: false
},
security: {
sessionTimeout: 120,
maxLoginAttempts: 5,
passwordComplexity: true,
twoFactorAuth: false,
ipWhitelist: '',
sslEnabled: true,
encryptionLevel: 'AES256',
auditLogging: true,
dataRetention: 365
},
database: {
host: 'localhost',
port: 5432,
name: 'bakery_ia_db',
backupFrequency: 'daily',
backupRetention: 30,
maintenanceWindow: '02:00-04:00',
connectionPool: 20,
slowQueryLogging: true,
performanceMonitoring: true
},
notifications: {
emailEnabled: true,
smsEnabled: false,
pushEnabled: true,
slackIntegration: false,
webhookUrl: '',
alertThreshold: 'medium',
systemAlerts: true,
performanceAlerts: true,
securityAlerts: true
},
performance: {
cacheEnabled: true,
cacheTtl: 3600,
compressionEnabled: true,
cdnEnabled: false,
loadBalancing: false,
memoryLimit: '2GB',
cpuThreshold: 80,
diskSpaceThreshold: 85,
logLevel: 'info'
}
});
const tabs = [
{ id: 'general', label: 'General', icon: Settings },
{ id: 'security', label: 'Seguridad', icon: Shield },
{ id: 'database', label: 'Base de Datos', icon: Database },
{ id: 'notifications', label: 'Notificaciones', icon: Bell },
{ id: 'performance', label: 'Rendimiento', icon: Activity }
];
const systemStats = {
uptime: '15 días, 7 horas',
memoryUsage: 68,
diskUsage: 42,
cpuUsage: 23,
activeUsers: 12,
lastBackup: '2024-01-26 02:15:00',
version: '2.1.0',
environment: 'Production'
};
const systemLogs = [
{
id: '1',
timestamp: '2024-01-26 10:30:00',
level: 'INFO',
category: 'System',
message: 'Backup automático completado exitosamente',
details: 'Database: bakery_ia_db, Size: 245MB, Duration: 3.2s'
},
{
id: '2',
timestamp: '2024-01-26 09:15:00',
level: 'WARN',
category: 'Performance',
message: 'Uso de CPU alto detectado',
details: 'CPU usage: 89% for 5 minutes, Process: data-processor'
},
{
id: '3',
timestamp: '2024-01-26 08:45:00',
level: 'INFO',
category: 'Security',
message: 'Usuario admin autenticado correctamente',
details: 'IP: 192.168.1.100, Session: sess_abc123'
},
{
id: '4',
timestamp: '2024-01-26 07:30:00',
level: 'ERROR',
category: 'Database',
message: 'Consulta lenta detectada',
details: 'Query duration: 5.8s, Table: sales_analytics'
}
];
const handleSettingChange = (section: string, field: string, value: any) => {
setSettings(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
console.log('Saving system settings:', settings);
setHasChanges(false);
};
const handleReset = () => {
setHasChanges(false);
};
const getLevelColor = (level: string) => {
switch (level) {
case 'ERROR': return 'red';
case 'WARN': return 'yellow';
case 'INFO': return 'blue';
default: return 'gray';
}
};
const getUsageColor = (usage: number) => {
if (usage >= 80) return 'text-[var(--color-error)]';
if (usage >= 60) return 'text-yellow-600';
return 'text-[var(--color-success)]';
};
const renderTabContent = () => {
switch (activeTab) {
case 'general':
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nombre del Sistema
</label>
<input
type="text"
value={settings.general.systemName}
onChange={(e) => handleSettingChange('general', 'systemName', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Zona Horaria
</label>
<select
value={settings.general.timezone}
onChange={(e) => handleSettingChange('general', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="Europe/Madrid">Madrid (GMT+1)</option>
<option value="Europe/London">Londres (GMT)</option>
<option value="America/New_York">Nueva York (GMT-5)</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Idioma del Sistema
</label>
<select
value={settings.general.language}
onChange={(e) => handleSettingChange('general', 'language', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Formato de Fecha
</label>
<select
value={settings.general.dateFormat}
onChange={(e) => handleSettingChange('general', 'dateFormat', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="dd/mm/yyyy">DD/MM/YYYY</option>
<option value="mm/dd/yyyy">MM/DD/YYYY</option>
<option value="yyyy-mm-dd">YYYY-MM-DD</option>
</select>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.general.autoUpdates}
onChange={(e) => handleSettingChange('general', 'autoUpdates', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<div>
<span className="text-sm font-medium text-[var(--text-secondary)]">Actualizaciones Automáticas</span>
<p className="text-xs text-[var(--text-tertiary)]">Instalar actualizaciones de seguridad automáticamente</p>
</div>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.general.maintenanceMode}
onChange={(e) => handleSettingChange('general', 'maintenanceMode', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<div>
<span className="text-sm font-medium text-[var(--text-secondary)]">Modo Mantenimiento</span>
<p className="text-xs text-[var(--text-tertiary)]">Deshabilitar acceso durante mantenimiento</p>
</div>
</label>
</div>
</div>
);
case 'security':
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Tiempo de Sesión (minutos)
</label>
<input
type="number"
value={settings.security.sessionTimeout}
onChange={(e) => handleSettingChange('security', 'sessionTimeout', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Intentos Máximos de Login
</label>
<input
type="number"
value={settings.security.maxLoginAttempts}
onChange={(e) => handleSettingChange('security', 'maxLoginAttempts', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nivel de Encriptación
</label>
<select
value={settings.security.encryptionLevel}
onChange={(e) => handleSettingChange('security', 'encryptionLevel', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="AES128">AES-128</option>
<option value="AES256">AES-256</option>
<option value="AES512">AES-512</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Retención de Datos (días)
</label>
<input
type="number"
value={settings.security.dataRetention}
onChange={(e) => handleSettingChange('security', 'dataRetention', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.passwordComplexity}
onChange={(e) => handleSettingChange('security', 'passwordComplexity', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Complejidad de Contraseñas</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.twoFactorAuth}
onChange={(e) => handleSettingChange('security', 'twoFactorAuth', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Autenticación de Dos Factores</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.auditLogging}
onChange={(e) => handleSettingChange('security', 'auditLogging', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Registro de Auditoría</span>
</label>
</div>
</div>
);
case 'database':
return (
<div className="space-y-6">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center">
<AlertTriangle className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Configuración Avanzada</p>
<p className="text-sm text-yellow-700">Cambios incorrectos pueden afectar el rendimiento del sistema</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Frecuencia de Backup
</label>
<select
value={settings.database.backupFrequency}
onChange={(e) => handleSettingChange('database', 'backupFrequency', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="hourly">Cada Hora</option>
<option value="daily">Diario</option>
<option value="weekly">Semanal</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Retención de Backups (días)
</label>
<input
type="number"
value={settings.database.backupRetention}
onChange={(e) => handleSettingChange('database', 'backupRetention', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Ventana de Mantenimiento
</label>
<input
type="text"
value={settings.database.maintenanceWindow}
onChange={(e) => handleSettingChange('database', 'maintenanceWindow', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="02:00-04:00"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Pool de Conexiones
</label>
<input
type="number"
value={settings.database.connectionPool}
onChange={(e) => handleSettingChange('database', 'connectionPool', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
/>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.database.slowQueryLogging}
onChange={(e) => handleSettingChange('database', 'slowQueryLogging', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Registro de Consultas Lentas</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.database.performanceMonitoring}
onChange={(e) => handleSettingChange('database', 'performanceMonitoring', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Monitoreo de Rendimiento</span>
</label>
</div>
</div>
);
default:
return <div>Contenido no disponible</div>;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Configuración del Sistema"
description="Administra la configuración técnica y seguridad del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* System Status */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Activo</p>
<p className="text-lg font-bold text-[var(--color-success)]">{systemStats.uptime}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Uso de Memoria</p>
<p className={`text-lg font-bold ${getUsageColor(systemStats.memoryUsage)}`}>
{systemStats.memoryUsage}%
</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<HardDrive className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Uso de CPU</p>
<p className={`text-lg font-bold ${getUsageColor(systemStats.cpuUsage)}`}>
{systemStats.cpuUsage}%
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Usuarios Activos</p>
<p className="text-lg font-bold text-[var(--color-primary)]">{systemStats.activeUsers}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Wifi className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Settings Tabs */}
<div>
<Card className="p-4">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
<tab.icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
))}
</nav>
</Card>
</div>
{/* Settings Content */}
<div className="lg:col-span-2">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
{tabs.find(tab => tab.id === activeTab)?.label}
</h3>
{renderTabContent()}
</Card>
</div>
</div>
{/* System Logs */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Registro del Sistema</h3>
<div className="space-y-3">
{systemLogs.map((log) => (
<div key={log.id} className="flex items-start space-x-4 p-3 border rounded-lg">
<Badge variant={getLevelColor(log.level)} className="mt-1">
{log.level}
</Badge>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-1">
<span className="text-sm font-medium text-[var(--text-primary)]">{log.message}</span>
<span className="text-xs text-[var(--text-tertiary)]">{log.category}</span>
</div>
<p className="text-xs text-[var(--text-secondary)]">{log.details}</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">{log.timestamp}</p>
</div>
</div>
))}
</div>
</Card>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default SystemSettingsPage;

View File

@@ -0,0 +1,591 @@
import React, { useState } from 'react';
import { Settings, Shield, Database, Bell, Wifi, HardDrive, Activity, Save, RotateCcw, AlertTriangle } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const SystemSettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('general');
const [hasChanges, setHasChanges] = useState(false);
const [settings, setSettings] = useState({
general: {
systemName: 'Bakery-IA Sistema',
version: '2.1.0',
environment: 'production',
timezone: 'Europe/Madrid',
language: 'es',
currency: 'EUR',
dateFormat: 'dd/mm/yyyy',
autoUpdates: true,
maintenanceMode: false
},
security: {
sessionTimeout: 120,
maxLoginAttempts: 5,
passwordComplexity: true,
twoFactorAuth: false,
ipWhitelist: '',
sslEnabled: true,
encryptionLevel: 'AES256',
auditLogging: true,
dataRetention: 365
},
database: {
host: 'localhost',
port: 5432,
name: 'bakery_ia_db',
backupFrequency: 'daily',
backupRetention: 30,
maintenanceWindow: '02:00-04:00',
connectionPool: 20,
slowQueryLogging: true,
performanceMonitoring: true
},
notifications: {
emailEnabled: true,
smsEnabled: false,
pushEnabled: true,
slackIntegration: false,
webhookUrl: '',
alertThreshold: 'medium',
systemAlerts: true,
performanceAlerts: true,
securityAlerts: true
},
performance: {
cacheEnabled: true,
cacheTtl: 3600,
compressionEnabled: true,
cdnEnabled: false,
loadBalancing: false,
memoryLimit: '2GB',
cpuThreshold: 80,
diskSpaceThreshold: 85,
logLevel: 'info'
}
});
const tabs = [
{ id: 'general', label: 'General', icon: Settings },
{ id: 'security', label: 'Seguridad', icon: Shield },
{ id: 'database', label: 'Base de Datos', icon: Database },
{ id: 'notifications', label: 'Notificaciones', icon: Bell },
{ id: 'performance', label: 'Rendimiento', icon: Activity }
];
const systemStats = {
uptime: '15 días, 7 horas',
memoryUsage: 68,
diskUsage: 42,
cpuUsage: 23,
activeUsers: 12,
lastBackup: '2024-01-26 02:15:00',
version: '2.1.0',
environment: 'Production'
};
const systemLogs = [
{
id: '1',
timestamp: '2024-01-26 10:30:00',
level: 'INFO',
category: 'System',
message: 'Backup automático completado exitosamente',
details: 'Database: bakery_ia_db, Size: 245MB, Duration: 3.2s'
},
{
id: '2',
timestamp: '2024-01-26 09:15:00',
level: 'WARN',
category: 'Performance',
message: 'Uso de CPU alto detectado',
details: 'CPU usage: 89% for 5 minutes, Process: data-processor'
},
{
id: '3',
timestamp: '2024-01-26 08:45:00',
level: 'INFO',
category: 'Security',
message: 'Usuario admin autenticado correctamente',
details: 'IP: 192.168.1.100, Session: sess_abc123'
},
{
id: '4',
timestamp: '2024-01-26 07:30:00',
level: 'ERROR',
category: 'Database',
message: 'Consulta lenta detectada',
details: 'Query duration: 5.8s, Table: sales_analytics'
}
];
const handleSettingChange = (section: string, field: string, value: any) => {
setSettings(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}));
setHasChanges(true);
};
const handleSave = () => {
console.log('Saving system settings:', settings);
setHasChanges(false);
};
const handleReset = () => {
setHasChanges(false);
};
const getLevelColor = (level: string) => {
switch (level) {
case 'ERROR': return 'red';
case 'WARN': return 'yellow';
case 'INFO': return 'blue';
default: return 'gray';
}
};
const getUsageColor = (usage: number) => {
if (usage >= 80) return 'text-red-600';
if (usage >= 60) return 'text-yellow-600';
return 'text-green-600';
};
const renderTabContent = () => {
switch (activeTab) {
case 'general':
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre del Sistema
</label>
<input
type="text"
value={settings.general.systemName}
onChange={(e) => handleSettingChange('general', 'systemName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Zona Horaria
</label>
<select
value={settings.general.timezone}
onChange={(e) => handleSettingChange('general', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="Europe/Madrid">Madrid (GMT+1)</option>
<option value="Europe/London">Londres (GMT)</option>
<option value="America/New_York">Nueva York (GMT-5)</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Idioma del Sistema
</label>
<select
value={settings.general.language}
onChange={(e) => handleSettingChange('general', 'language', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Formato de Fecha
</label>
<select
value={settings.general.dateFormat}
onChange={(e) => handleSettingChange('general', 'dateFormat', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="dd/mm/yyyy">DD/MM/YYYY</option>
<option value="mm/dd/yyyy">MM/DD/YYYY</option>
<option value="yyyy-mm-dd">YYYY-MM-DD</option>
</select>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.general.autoUpdates}
onChange={(e) => handleSettingChange('general', 'autoUpdates', e.target.checked)}
className="rounded border-gray-300"
/>
<div>
<span className="text-sm font-medium text-gray-700">Actualizaciones Automáticas</span>
<p className="text-xs text-gray-500">Instalar actualizaciones de seguridad automáticamente</p>
</div>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.general.maintenanceMode}
onChange={(e) => handleSettingChange('general', 'maintenanceMode', e.target.checked)}
className="rounded border-gray-300"
/>
<div>
<span className="text-sm font-medium text-gray-700">Modo Mantenimiento</span>
<p className="text-xs text-gray-500">Deshabilitar acceso durante mantenimiento</p>
</div>
</label>
</div>
</div>
);
case 'security':
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tiempo de Sesión (minutos)
</label>
<input
type="number"
value={settings.security.sessionTimeout}
onChange={(e) => handleSettingChange('security', 'sessionTimeout', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Intentos Máximos de Login
</label>
<input
type="number"
value={settings.security.maxLoginAttempts}
onChange={(e) => handleSettingChange('security', 'maxLoginAttempts', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nivel de Encriptación
</label>
<select
value={settings.security.encryptionLevel}
onChange={(e) => handleSettingChange('security', 'encryptionLevel', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="AES128">AES-128</option>
<option value="AES256">AES-256</option>
<option value="AES512">AES-512</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Retención de Datos (días)
</label>
<input
type="number"
value={settings.security.dataRetention}
onChange={(e) => handleSettingChange('security', 'dataRetention', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.passwordComplexity}
onChange={(e) => handleSettingChange('security', 'passwordComplexity', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Complejidad de Contraseñas</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.twoFactorAuth}
onChange={(e) => handleSettingChange('security', 'twoFactorAuth', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Autenticación de Dos Factores</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.security.auditLogging}
onChange={(e) => handleSettingChange('security', 'auditLogging', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Registro de Auditoría</span>
</label>
</div>
</div>
);
case 'database':
return (
<div className="space-y-6">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center">
<AlertTriangle className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Configuración Avanzada</p>
<p className="text-sm text-yellow-700">Cambios incorrectos pueden afectar el rendimiento del sistema</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Frecuencia de Backup
</label>
<select
value={settings.database.backupFrequency}
onChange={(e) => handleSettingChange('database', 'backupFrequency', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="hourly">Cada Hora</option>
<option value="daily">Diario</option>
<option value="weekly">Semanal</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Retención de Backups (días)
</label>
<input
type="number"
value={settings.database.backupRetention}
onChange={(e) => handleSettingChange('database', 'backupRetention', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ventana de Mantenimiento
</label>
<input
type="text"
value={settings.database.maintenanceWindow}
onChange={(e) => handleSettingChange('database', 'maintenanceWindow', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="02:00-04:00"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Pool de Conexiones
</label>
<input
type="number"
value={settings.database.connectionPool}
onChange={(e) => handleSettingChange('database', 'connectionPool', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div className="space-y-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.database.slowQueryLogging}
onChange={(e) => handleSettingChange('database', 'slowQueryLogging', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Registro de Consultas Lentas</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={settings.database.performanceMonitoring}
onChange={(e) => handleSettingChange('database', 'performanceMonitoring', e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm font-medium text-gray-700">Monitoreo de Rendimiento</span>
</label>
</div>
</div>
);
default:
return <div>Contenido no disponible</div>;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Configuración del Sistema"
description="Administra la configuración técnica y seguridad del sistema"
action={
<div className="flex space-x-2">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
}
/>
{/* System Status */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Tiempo Activo</p>
<p className="text-lg font-bold text-green-600">{systemStats.uptime}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Uso de Memoria</p>
<p className={`text-lg font-bold ${getUsageColor(systemStats.memoryUsage)}`}>
{systemStats.memoryUsage}%
</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<HardDrive className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Uso de CPU</p>
<p className={`text-lg font-bold ${getUsageColor(systemStats.cpuUsage)}`}>
{systemStats.cpuUsage}%
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Usuarios Activos</p>
<p className="text-lg font-bold text-orange-600">{systemStats.activeUsers}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Wifi className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Settings Tabs */}
<div>
<Card className="p-4">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<tab.icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
))}
</nav>
</Card>
</div>
{/* Settings Content */}
<div className="lg:col-span-2">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">
{tabs.find(tab => tab.id === activeTab)?.label}
</h3>
{renderTabContent()}
</Card>
</div>
</div>
{/* System Logs */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Registro del Sistema</h3>
<div className="space-y-3">
{systemLogs.map((log) => (
<div key={log.id} className="flex items-start space-x-4 p-3 border rounded-lg">
<Badge variant={getLevelColor(log.level)} className="mt-1">
{log.level}
</Badge>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-1">
<span className="text-sm font-medium text-gray-900">{log.message}</span>
<span className="text-xs text-gray-500">{log.category}</span>
</div>
<p className="text-xs text-gray-600">{log.details}</p>
<p className="text-xs text-gray-500 mt-1">{log.timestamp}</p>
</div>
</div>
))}
</div>
</Card>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
<span className="text-sm">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default SystemSettingsPage;

View File

@@ -0,0 +1 @@
export { default as SystemSettingsPage } from './SystemSettingsPage';

View File

@@ -0,0 +1,406 @@
import React, { useState } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState('all');
const [showForm, setShowForm] = useState(false);
const teamMembers = [
{
id: '1',
name: 'María González',
email: 'maria.gonzalez@panaderia.com',
phone: '+34 600 123 456',
role: 'manager',
department: 'Administración',
status: 'active',
joinDate: '2022-03-15',
lastLogin: '2024-01-26 09:30:00',
permissions: ['inventory', 'sales', 'reports', 'team'],
avatar: '/avatars/maria.jpg',
schedule: {
monday: '07:00-15:00',
tuesday: '07:00-15:00',
wednesday: '07:00-15:00',
thursday: '07:00-15:00',
friday: '07:00-15:00',
saturday: 'Libre',
sunday: 'Libre'
}
},
{
id: '2',
name: 'Carlos Rodríguez',
email: 'carlos.rodriguez@panaderia.com',
phone: '+34 600 234 567',
role: 'baker',
department: 'Producción',
status: 'active',
joinDate: '2021-09-20',
lastLogin: '2024-01-26 08:45:00',
permissions: ['production', 'inventory'],
avatar: '/avatars/carlos.jpg',
schedule: {
monday: '05:00-13:00',
tuesday: '05:00-13:00',
wednesday: '05:00-13:00',
thursday: '05:00-13:00',
friday: '05:00-13:00',
saturday: '05:00-11:00',
sunday: 'Libre'
}
},
{
id: '3',
name: 'Ana Martínez',
email: 'ana.martinez@panaderia.com',
phone: '+34 600 345 678',
role: 'cashier',
department: 'Ventas',
status: 'active',
joinDate: '2023-01-10',
lastLogin: '2024-01-26 10:15:00',
permissions: ['sales', 'pos'],
avatar: '/avatars/ana.jpg',
schedule: {
monday: '08:00-16:00',
tuesday: '08:00-16:00',
wednesday: 'Libre',
thursday: '08:00-16:00',
friday: '08:00-16:00',
saturday: '09:00-14:00',
sunday: '09:00-14:00'
}
},
{
id: '4',
name: 'Luis Fernández',
email: 'luis.fernandez@panaderia.com',
phone: '+34 600 456 789',
role: 'baker',
department: 'Producción',
status: 'inactive',
joinDate: '2020-11-05',
lastLogin: '2024-01-20 16:30:00',
permissions: ['production'],
avatar: '/avatars/luis.jpg',
schedule: {
monday: '13:00-21:00',
tuesday: '13:00-21:00',
wednesday: '13:00-21:00',
thursday: 'Libre',
friday: '13:00-21:00',
saturday: 'Libre',
sunday: '13:00-21:00'
}
},
{
id: '5',
name: 'Isabel Torres',
email: 'isabel.torres@panaderia.com',
phone: '+34 600 567 890',
role: 'assistant',
department: 'Ventas',
status: 'active',
joinDate: '2023-06-01',
lastLogin: '2024-01-25 18:20:00',
permissions: ['sales'],
avatar: '/avatars/isabel.jpg',
schedule: {
monday: 'Libre',
tuesday: '16:00-20:00',
wednesday: '16:00-20:00',
thursday: '16:00-20:00',
friday: '16:00-20:00',
saturday: '14:00-20:00',
sunday: '14:00-20:00'
}
}
];
const roles = [
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
];
const teamStats = {
total: teamMembers.length,
active: teamMembers.filter(m => m.status === 'active').length,
departments: {
production: teamMembers.filter(m => m.department === 'Producción').length,
sales: teamMembers.filter(m => m.department === 'Ventas').length,
admin: teamMembers.filter(m => m.department === 'Administración').length
}
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'manager': return 'purple';
case 'baker': return 'green';
case 'cashier': return 'blue';
case 'assistant': return 'yellow';
default: return 'gray';
}
};
const getStatusColor = (status: string) => {
return status === 'active' ? 'green' : 'red';
};
const getRoleLabel = (role: string) => {
switch (role) {
case 'manager': return 'Gerente';
case 'baker': return 'Panadero';
case 'cashier': return 'Cajero';
case 'assistant': return 'Asistente';
default: return role;
}
};
const filteredMembers = teamMembers.filter(member => {
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
return matchesRole && matchesSearch;
});
const formatLastLogin = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) {
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
} else if (diffInDays === 1) {
return 'Ayer';
} else {
return `hace ${diffInDays} días`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Equipo"
description="Administra los miembros del equipo, roles y permisos"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Miembro
</Button>
}
/>
{/* Team Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Equipo</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{teamStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Activos</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{teamStats.active}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<UserCheck className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.departments.production}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar miembros del equipo..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{roles.map((role) => (
<button
key={role.value}
onClick={() => setSelectedRole(role.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedRole === role.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{role.label} ({role.count})
</button>
))}
</div>
</div>
</Card>
{/* Team Members List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredMembers.map((member) => (
<Card key={member.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className="w-12 h-12 bg-[var(--bg-quaternary)] rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-[var(--text-tertiary)]" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.name}</h3>
<Badge variant={getStatusColor(member.status)}>
{member.status === 'active' ? 'Activo' : 'Inactivo'}
</Badge>
</div>
<div className="space-y-1 mb-3">
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Mail className="w-4 h-4 mr-2" />
{member.email}
</div>
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Phone className="w-4 h-4 mr-2" />
{member.phone}
</div>
</div>
<div className="flex items-center space-x-2 mb-3">
<Badge variant={getRoleBadgeColor(member.role)}>
{getRoleLabel(member.role)}
</Badge>
<Badge variant="gray">
{member.department}
</Badge>
</div>
<div className="text-sm text-[var(--text-tertiary)] mb-3">
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
</div>
{/* Permissions */}
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">Permisos:</p>
<div className="flex flex-wrap gap-1">
{member.permissions.map((permission, index) => (
<span
key={index}
className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full"
>
{permission}
</span>
))}
</div>
</div>
{/* Schedule Preview */}
<div className="text-xs text-[var(--text-tertiary)]">
<p className="font-medium mb-1">Horario esta semana:</p>
<div className="grid grid-cols-2 gap-1">
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
<span key={day}>
{day.charAt(0).toUpperCase()}: {hours}
</span>
))}
</div>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
className={member.status === 'active' ? 'text-[var(--color-error)] hover:text-[var(--color-error)]' : 'text-[var(--color-success)] hover:text-[var(--color-success)]'}
>
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
</Button>
</div>
</div>
</Card>
))}
</div>
{filteredMembers.length === 0 && (
<Card className="p-12 text-center">
<Users className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se encontraron miembros</h3>
<p className="text-[var(--text-secondary)]">
No hay miembros del equipo que coincidan con los filtros seleccionados.
</p>
</Card>
)}
{/* Add Member Modal Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Nuevo Miembro del Equipo</h3>
<p className="text-[var(--text-secondary)] mb-4">
Formulario para agregar un nuevo miembro del equipo.
</p>
<div className="flex space-x-2">
<Button size="sm" onClick={() => setShowForm(false)}>
Guardar
</Button>
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
Cancelar
</Button>
</div>
</Card>
</div>
)}
</div>
);
};
export default TeamPage;

View File

@@ -0,0 +1,406 @@
import React, { useState } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState('all');
const [showForm, setShowForm] = useState(false);
const teamMembers = [
{
id: '1',
name: 'María González',
email: 'maria.gonzalez@panaderia.com',
phone: '+34 600 123 456',
role: 'manager',
department: 'Administración',
status: 'active',
joinDate: '2022-03-15',
lastLogin: '2024-01-26 09:30:00',
permissions: ['inventory', 'sales', 'reports', 'team'],
avatar: '/avatars/maria.jpg',
schedule: {
monday: '07:00-15:00',
tuesday: '07:00-15:00',
wednesday: '07:00-15:00',
thursday: '07:00-15:00',
friday: '07:00-15:00',
saturday: 'Libre',
sunday: 'Libre'
}
},
{
id: '2',
name: 'Carlos Rodríguez',
email: 'carlos.rodriguez@panaderia.com',
phone: '+34 600 234 567',
role: 'baker',
department: 'Producción',
status: 'active',
joinDate: '2021-09-20',
lastLogin: '2024-01-26 08:45:00',
permissions: ['production', 'inventory'],
avatar: '/avatars/carlos.jpg',
schedule: {
monday: '05:00-13:00',
tuesday: '05:00-13:00',
wednesday: '05:00-13:00',
thursday: '05:00-13:00',
friday: '05:00-13:00',
saturday: '05:00-11:00',
sunday: 'Libre'
}
},
{
id: '3',
name: 'Ana Martínez',
email: 'ana.martinez@panaderia.com',
phone: '+34 600 345 678',
role: 'cashier',
department: 'Ventas',
status: 'active',
joinDate: '2023-01-10',
lastLogin: '2024-01-26 10:15:00',
permissions: ['sales', 'pos'],
avatar: '/avatars/ana.jpg',
schedule: {
monday: '08:00-16:00',
tuesday: '08:00-16:00',
wednesday: 'Libre',
thursday: '08:00-16:00',
friday: '08:00-16:00',
saturday: '09:00-14:00',
sunday: '09:00-14:00'
}
},
{
id: '4',
name: 'Luis Fernández',
email: 'luis.fernandez@panaderia.com',
phone: '+34 600 456 789',
role: 'baker',
department: 'Producción',
status: 'inactive',
joinDate: '2020-11-05',
lastLogin: '2024-01-20 16:30:00',
permissions: ['production'],
avatar: '/avatars/luis.jpg',
schedule: {
monday: '13:00-21:00',
tuesday: '13:00-21:00',
wednesday: '13:00-21:00',
thursday: 'Libre',
friday: '13:00-21:00',
saturday: 'Libre',
sunday: '13:00-21:00'
}
},
{
id: '5',
name: 'Isabel Torres',
email: 'isabel.torres@panaderia.com',
phone: '+34 600 567 890',
role: 'assistant',
department: 'Ventas',
status: 'active',
joinDate: '2023-06-01',
lastLogin: '2024-01-25 18:20:00',
permissions: ['sales'],
avatar: '/avatars/isabel.jpg',
schedule: {
monday: 'Libre',
tuesday: '16:00-20:00',
wednesday: '16:00-20:00',
thursday: '16:00-20:00',
friday: '16:00-20:00',
saturday: '14:00-20:00',
sunday: '14:00-20:00'
}
}
];
const roles = [
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
];
const teamStats = {
total: teamMembers.length,
active: teamMembers.filter(m => m.status === 'active').length,
departments: {
production: teamMembers.filter(m => m.department === 'Producción').length,
sales: teamMembers.filter(m => m.department === 'Ventas').length,
admin: teamMembers.filter(m => m.department === 'Administración').length
}
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'manager': return 'purple';
case 'baker': return 'green';
case 'cashier': return 'blue';
case 'assistant': return 'yellow';
default: return 'gray';
}
};
const getStatusColor = (status: string) => {
return status === 'active' ? 'green' : 'red';
};
const getRoleLabel = (role: string) => {
switch (role) {
case 'manager': return 'Gerente';
case 'baker': return 'Panadero';
case 'cashier': return 'Cajero';
case 'assistant': return 'Asistente';
default: return role;
}
};
const filteredMembers = teamMembers.filter(member => {
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
return matchesRole && matchesSearch;
});
const formatLastLogin = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) {
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
} else if (diffInDays === 1) {
return 'Ayer';
} else {
return `hace ${diffInDays} días`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Equipo"
description="Administra los miembros del equipo, roles y permisos"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Miembro
</Button>
}
/>
{/* Team Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Equipo</p>
<p className="text-3xl font-bold text-gray-900">{teamStats.total}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Activos</p>
<p className="text-3xl font-bold text-green-600">{teamStats.active}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<UserCheck className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Producción</p>
<p className="text-3xl font-bold text-orange-600">{teamStats.departments.production}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar miembros del equipo..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{roles.map((role) => (
<button
key={role.value}
onClick={() => setSelectedRole(role.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedRole === role.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{role.label} ({role.count})
</button>
))}
</div>
</div>
</Card>
{/* Team Members List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredMembers.map((member) => (
<Card key={member.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-gray-500" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{member.name}</h3>
<Badge variant={getStatusColor(member.status)}>
{member.status === 'active' ? 'Activo' : 'Inactivo'}
</Badge>
</div>
<div className="space-y-1 mb-3">
<div className="flex items-center text-sm text-gray-600">
<Mail className="w-4 h-4 mr-2" />
{member.email}
</div>
<div className="flex items-center text-sm text-gray-600">
<Phone className="w-4 h-4 mr-2" />
{member.phone}
</div>
</div>
<div className="flex items-center space-x-2 mb-3">
<Badge variant={getRoleBadgeColor(member.role)}>
{getRoleLabel(member.role)}
</Badge>
<Badge variant="gray">
{member.department}
</Badge>
</div>
<div className="text-sm text-gray-500 mb-3">
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
</div>
{/* Permissions */}
<div className="mb-3">
<p className="text-xs font-medium text-gray-700 mb-2">Permisos:</p>
<div className="flex flex-wrap gap-1">
{member.permissions.map((permission, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{permission}
</span>
))}
</div>
</div>
{/* Schedule Preview */}
<div className="text-xs text-gray-500">
<p className="font-medium mb-1">Horario esta semana:</p>
<div className="grid grid-cols-2 gap-1">
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
<span key={day}>
{day.charAt(0).toUpperCase()}: {hours}
</span>
))}
</div>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
className={member.status === 'active' ? 'text-red-600 hover:text-red-700' : 'text-green-600 hover:text-green-700'}
>
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
</Button>
</div>
</div>
</Card>
))}
</div>
{filteredMembers.length === 0 && (
<Card className="p-12 text-center">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron miembros</h3>
<p className="text-gray-600">
No hay miembros del equipo que coincidan con los filtros seleccionados.
</p>
</Card>
)}
{/* Add Member Modal Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nuevo Miembro del Equipo</h3>
<p className="text-gray-600 mb-4">
Formulario para agregar un nuevo miembro del equipo.
</p>
<div className="flex space-x-2">
<Button size="sm" onClick={() => setShowForm(false)}>
Guardar
</Button>
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
Cancelar
</Button>
</div>
</Card>
</div>
)}
</div>
);
};
export default TeamPage;

View File

@@ -0,0 +1 @@
export { default as TeamPage } from './TeamPage';

View File

@@ -0,0 +1,454 @@
import React, { useState } from 'react';
import { BookOpen, Play, CheckCircle, Clock, Users, Award, Download, Search } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TrainingPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const trainingModules = [
{
id: '1',
title: 'Fundamentos de Panadería',
description: 'Conceptos básicos de elaboración de pan y técnicas fundamentales',
category: 'basics',
duration: '2.5 horas',
lessons: 12,
difficulty: 'beginner',
progress: 100,
completed: true,
rating: 4.8,
instructor: 'Chef María González',
topics: ['Ingredientes básicos', 'Proceso de amasado', 'Fermentación', 'Horneado'],
thumbnail: '/training/bread-basics.jpg'
},
{
id: '2',
title: 'Técnicas Avanzadas de Bollería',
description: 'Elaboración de croissants, hojaldre y productos fermentados complejos',
category: 'advanced',
duration: '4 horas',
lessons: 18,
difficulty: 'advanced',
progress: 65,
completed: false,
rating: 4.9,
instructor: 'Chef Pierre Laurent',
topics: ['Masas laminadas', 'Temperaturas críticas', 'Técnicas de plegado', 'Control de calidad'],
thumbnail: '/training/pastry-advanced.jpg'
},
{
id: '3',
title: 'Seguridad e Higiene Alimentaria',
description: 'Protocolos de seguridad, HACCP y normativas sanitarias',
category: 'safety',
duration: '1.5 horas',
lessons: 8,
difficulty: 'beginner',
progress: 0,
completed: false,
rating: 4.7,
instructor: 'Dr. Ana Rodríguez',
topics: ['HACCP', 'Limpieza y desinfección', 'Control de temperaturas', 'Trazabilidad'],
thumbnail: '/training/food-safety.jpg'
},
{
id: '4',
title: 'Gestión de Inventarios',
description: 'Optimización de stock, control de mermas y gestión de proveedores',
category: 'management',
duration: '3 horas',
lessons: 15,
difficulty: 'intermediate',
progress: 30,
completed: false,
rating: 4.6,
instructor: 'Carlos Fernández',
topics: ['Rotación de stock', 'Punto de reorden', 'Análisis ABC', 'Negociación con proveedores'],
thumbnail: '/training/inventory-mgmt.jpg'
},
{
id: '5',
title: 'Atención al Cliente',
description: 'Técnicas de venta, resolución de quejas y fidelización',
category: 'sales',
duration: '2 horas',
lessons: 10,
difficulty: 'beginner',
progress: 85,
completed: false,
rating: 4.8,
instructor: 'Isabel Torres',
topics: ['Técnicas de venta', 'Comunicación efectiva', 'Manejo de quejas', 'Up-selling'],
thumbnail: '/training/customer-service.jpg'
},
{
id: '6',
title: 'Innovación en Productos',
description: 'Desarrollo de nuevos productos, tendencias y análisis de mercado',
category: 'innovation',
duration: '3.5 horas',
lessons: 16,
difficulty: 'intermediate',
progress: 0,
completed: false,
rating: 4.7,
instructor: 'Chef Daniel Ramos',
topics: ['Análisis de tendencias', 'Prototipado', 'Testing de mercado', 'Costos de producción'],
thumbnail: '/training/product-innovation.jpg'
}
];
const categories = [
{ value: 'all', label: 'Todos', count: trainingModules.length },
{ value: 'basics', label: 'Básicos', count: trainingModules.filter(m => m.category === 'basics').length },
{ value: 'advanced', label: 'Avanzado', count: trainingModules.filter(m => m.category === 'advanced').length },
{ value: 'safety', label: 'Seguridad', count: trainingModules.filter(m => m.category === 'safety').length },
{ value: 'management', label: 'Gestión', count: trainingModules.filter(m => m.category === 'management').length },
{ value: 'sales', label: 'Ventas', count: trainingModules.filter(m => m.category === 'sales').length },
{ value: 'innovation', label: 'Innovación', count: trainingModules.filter(m => m.category === 'innovation').length }
];
const teamProgress = [
{
name: 'María González',
role: 'Gerente',
completedModules: 4,
totalModules: 6,
currentModule: 'Gestión de Inventarios',
progress: 75,
certificates: 3
},
{
name: 'Carlos Rodríguez',
role: 'Panadero',
completedModules: 2,
totalModules: 4,
currentModule: 'Técnicas Avanzadas de Bollería',
progress: 65,
certificates: 2
},
{
name: 'Ana Martínez',
role: 'Cajera',
completedModules: 3,
totalModules: 4,
currentModule: 'Atención al Cliente',
progress: 85,
certificates: 2
}
];
const trainingStats = {
totalModules: trainingModules.length,
completedModules: trainingModules.filter(m => m.completed).length,
inProgress: trainingModules.filter(m => m.progress > 0 && !m.completed).length,
totalHours: trainingModules.reduce((sum, m) => sum + parseFloat(m.duration), 0),
avgRating: trainingModules.reduce((sum, m) => sum + m.rating, 0) / trainingModules.length
};
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'green';
case 'intermediate': return 'yellow';
case 'advanced': return 'red';
default: return 'gray';
}
};
const getDifficultyLabel = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'Principiante';
case 'intermediate': return 'Intermedio';
case 'advanced': return 'Avanzado';
default: return difficulty;
}
};
const filteredModules = trainingModules.filter(module => {
const matchesCategory = selectedCategory === 'all' || module.category === selectedCategory;
const matchesSearch = module.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
module.description.toLowerCase().includes(searchTerm.toLowerCase());
return matchesCategory && matchesSearch;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Centro de Formación"
description="Módulos de capacitación y desarrollo profesional para el equipo"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Certificados
</Button>
<Button>
Nuevo Módulo
</Button>
</div>
}
/>
{/* Training Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Módulos Totales</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{trainingStats.totalModules}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<BookOpen className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{trainingStats.completedModules}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<CheckCircle className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">En Progreso</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{trainingStats.inProgress}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Horas</p>
<p className="text-3xl font-bold text-purple-600">{trainingStats.totalHours}h</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Rating Promedio</p>
<p className="text-3xl font-bold text-yellow-600">{trainingStats.avgRating.toFixed(1)}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<Award className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar módulos de formación..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Training Modules */}
<div className="lg:col-span-2 space-y-4">
{filteredModules.map((module) => (
<Card key={module.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="w-16 h-16 bg-[var(--bg-quaternary)] rounded-lg flex items-center justify-center">
<BookOpen className="w-8 h-8 text-[var(--text-tertiary)]" />
</div>
<div className="flex-1">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{module.title}</h3>
<p className="text-sm text-[var(--text-secondary)] mb-2">{module.description}</p>
</div>
<div className="flex items-center space-x-2">
{module.completed ? (
<Badge variant="green">Completado</Badge>
) : module.progress > 0 ? (
<Badge variant="blue">En Progreso</Badge>
) : (
<Badge variant="gray">No Iniciado</Badge>
)}
</div>
</div>
<div className="flex items-center space-x-4 text-sm text-[var(--text-secondary)] mb-3">
<span className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{module.duration}
</span>
<span className="flex items-center">
<BookOpen className="w-4 h-4 mr-1" />
{module.lessons} lecciones
</span>
<span className="flex items-center">
<Users className="w-4 h-4 mr-1" />
{module.instructor}
</span>
</div>
<div className="flex items-center justify-between mb-3">
<Badge variant={getDifficultyColor(module.difficulty)}>
{getDifficultyLabel(module.difficulty)}
</Badge>
<div className="flex items-center space-x-1">
<Award className="w-4 h-4 text-yellow-500" />
<span className="text-sm font-medium text-[var(--text-secondary)]">{module.rating}</span>
</div>
</div>
{/* Progress Bar */}
<div className="mb-3">
<div className="flex justify-between text-sm text-[var(--text-secondary)] mb-1">
<span>Progreso</span>
<span>{module.progress}%</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${module.progress}%` }}
></div>
</div>
</div>
{/* Topics */}
<div className="mb-4">
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">Temas incluidos:</p>
<div className="flex flex-wrap gap-2">
{module.topics.map((topic, index) => (
<span
key={index}
className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded-full"
>
{topic}
</span>
))}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm">
<Play className="w-4 h-4 mr-2" />
{module.progress > 0 ? 'Continuar' : 'Comenzar'}
</Button>
<Button size="sm" variant="outline">
Ver Detalles
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
{/* Team Progress Sidebar */}
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso del Equipo</h3>
<div className="space-y-4">
{teamProgress.map((member, index) => (
<div key={index} className="border-b border-[var(--border-primary)] pb-4 last:border-b-0">
<div className="flex items-center justify-between mb-2">
<div>
<p className="font-medium text-[var(--text-primary)]">{member.name}</p>
<p className="text-sm text-[var(--text-tertiary)]">{member.role}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">
{member.completedModules}/{member.totalModules}
</p>
<div className="flex items-center space-x-1">
<Award className="w-3 h-3 text-yellow-500" />
<span className="text-xs text-[var(--text-tertiary)]">{member.certificates}</span>
</div>
</div>
</div>
<p className="text-xs text-[var(--text-secondary)] mb-2">
Actual: {member.currentModule}
</p>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-1.5">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{ width: `${member.progress}%` }}
></div>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Certificaciones</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-green-50 border border-green-200 rounded-lg">
<Award className="w-5 h-5 text-[var(--color-success)]" />
<div>
<p className="text-sm font-medium text-[var(--color-success)]">Certificado en Seguridad</p>
<p className="text-xs text-[var(--color-success)]">Válido hasta: Dic 2024</p>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
<Award className="w-5 h-5 text-[var(--color-info)]" />
<div>
<p className="text-sm font-medium text-[var(--color-info)]">Certificado Básico</p>
<p className="text-xs text-[var(--color-info)]">Completado: Ene 2024</p>
</div>
</div>
<Button size="sm" variant="outline" className="w-full">
Ver Todos los Certificados
</Button>
</div>
</Card>
</div>
</div>
</div>
);
};
export default TrainingPage;

View File

@@ -0,0 +1,454 @@
import React, { useState } from 'react';
import { BookOpen, Play, CheckCircle, Clock, Users, Award, Download, Search } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const TrainingPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const trainingModules = [
{
id: '1',
title: 'Fundamentos de Panadería',
description: 'Conceptos básicos de elaboración de pan y técnicas fundamentales',
category: 'basics',
duration: '2.5 horas',
lessons: 12,
difficulty: 'beginner',
progress: 100,
completed: true,
rating: 4.8,
instructor: 'Chef María González',
topics: ['Ingredientes básicos', 'Proceso de amasado', 'Fermentación', 'Horneado'],
thumbnail: '/training/bread-basics.jpg'
},
{
id: '2',
title: 'Técnicas Avanzadas de Bollería',
description: 'Elaboración de croissants, hojaldre y productos fermentados complejos',
category: 'advanced',
duration: '4 horas',
lessons: 18,
difficulty: 'advanced',
progress: 65,
completed: false,
rating: 4.9,
instructor: 'Chef Pierre Laurent',
topics: ['Masas laminadas', 'Temperaturas críticas', 'Técnicas de plegado', 'Control de calidad'],
thumbnail: '/training/pastry-advanced.jpg'
},
{
id: '3',
title: 'Seguridad e Higiene Alimentaria',
description: 'Protocolos de seguridad, HACCP y normativas sanitarias',
category: 'safety',
duration: '1.5 horas',
lessons: 8,
difficulty: 'beginner',
progress: 0,
completed: false,
rating: 4.7,
instructor: 'Dr. Ana Rodríguez',
topics: ['HACCP', 'Limpieza y desinfección', 'Control de temperaturas', 'Trazabilidad'],
thumbnail: '/training/food-safety.jpg'
},
{
id: '4',
title: 'Gestión de Inventarios',
description: 'Optimización de stock, control de mermas y gestión de proveedores',
category: 'management',
duration: '3 horas',
lessons: 15,
difficulty: 'intermediate',
progress: 30,
completed: false,
rating: 4.6,
instructor: 'Carlos Fernández',
topics: ['Rotación de stock', 'Punto de reorden', 'Análisis ABC', 'Negociación con proveedores'],
thumbnail: '/training/inventory-mgmt.jpg'
},
{
id: '5',
title: 'Atención al Cliente',
description: 'Técnicas de venta, resolución de quejas y fidelización',
category: 'sales',
duration: '2 horas',
lessons: 10,
difficulty: 'beginner',
progress: 85,
completed: false,
rating: 4.8,
instructor: 'Isabel Torres',
topics: ['Técnicas de venta', 'Comunicación efectiva', 'Manejo de quejas', 'Up-selling'],
thumbnail: '/training/customer-service.jpg'
},
{
id: '6',
title: 'Innovación en Productos',
description: 'Desarrollo de nuevos productos, tendencias y análisis de mercado',
category: 'innovation',
duration: '3.5 horas',
lessons: 16,
difficulty: 'intermediate',
progress: 0,
completed: false,
rating: 4.7,
instructor: 'Chef Daniel Ramos',
topics: ['Análisis de tendencias', 'Prototipado', 'Testing de mercado', 'Costos de producción'],
thumbnail: '/training/product-innovation.jpg'
}
];
const categories = [
{ value: 'all', label: 'Todos', count: trainingModules.length },
{ value: 'basics', label: 'Básicos', count: trainingModules.filter(m => m.category === 'basics').length },
{ value: 'advanced', label: 'Avanzado', count: trainingModules.filter(m => m.category === 'advanced').length },
{ value: 'safety', label: 'Seguridad', count: trainingModules.filter(m => m.category === 'safety').length },
{ value: 'management', label: 'Gestión', count: trainingModules.filter(m => m.category === 'management').length },
{ value: 'sales', label: 'Ventas', count: trainingModules.filter(m => m.category === 'sales').length },
{ value: 'innovation', label: 'Innovación', count: trainingModules.filter(m => m.category === 'innovation').length }
];
const teamProgress = [
{
name: 'María González',
role: 'Gerente',
completedModules: 4,
totalModules: 6,
currentModule: 'Gestión de Inventarios',
progress: 75,
certificates: 3
},
{
name: 'Carlos Rodríguez',
role: 'Panadero',
completedModules: 2,
totalModules: 4,
currentModule: 'Técnicas Avanzadas de Bollería',
progress: 65,
certificates: 2
},
{
name: 'Ana Martínez',
role: 'Cajera',
completedModules: 3,
totalModules: 4,
currentModule: 'Atención al Cliente',
progress: 85,
certificates: 2
}
];
const trainingStats = {
totalModules: trainingModules.length,
completedModules: trainingModules.filter(m => m.completed).length,
inProgress: trainingModules.filter(m => m.progress > 0 && !m.completed).length,
totalHours: trainingModules.reduce((sum, m) => sum + parseFloat(m.duration), 0),
avgRating: trainingModules.reduce((sum, m) => sum + m.rating, 0) / trainingModules.length
};
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'green';
case 'intermediate': return 'yellow';
case 'advanced': return 'red';
default: return 'gray';
}
};
const getDifficultyLabel = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'Principiante';
case 'intermediate': return 'Intermedio';
case 'advanced': return 'Avanzado';
default: return difficulty;
}
};
const filteredModules = trainingModules.filter(module => {
const matchesCategory = selectedCategory === 'all' || module.category === selectedCategory;
const matchesSearch = module.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
module.description.toLowerCase().includes(searchTerm.toLowerCase());
return matchesCategory && matchesSearch;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Centro de Formación"
description="Módulos de capacitación y desarrollo profesional para el equipo"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Certificados
</Button>
<Button>
Nuevo Módulo
</Button>
</div>
}
/>
{/* Training Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Módulos Totales</p>
<p className="text-3xl font-bold text-blue-600">{trainingStats.totalModules}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<BookOpen className="h-6 w-6 text-blue-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completados</p>
<p className="text-3xl font-bold text-green-600">{trainingStats.completedModules}</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">En Progreso</p>
<p className="text-3xl font-bold text-orange-600">{trainingStats.inProgress}</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-orange-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Horas</p>
<p className="text-3xl font-bold text-purple-600">{trainingStats.totalHours}h</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Rating Promedio</p>
<p className="text-3xl font-bold text-yellow-600">{trainingStats.avgRating.toFixed(1)}</p>
</div>
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
<Award className="h-6 w-6 text-yellow-600" />
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar módulos de formación..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Training Modules */}
<div className="lg:col-span-2 space-y-4">
{filteredModules.map((module) => (
<Card key={module.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center">
<BookOpen className="w-8 h-8 text-gray-500" />
</div>
<div className="flex-1">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-lg font-semibold text-gray-900">{module.title}</h3>
<p className="text-sm text-gray-600 mb-2">{module.description}</p>
</div>
<div className="flex items-center space-x-2">
{module.completed ? (
<Badge variant="green">Completado</Badge>
) : module.progress > 0 ? (
<Badge variant="blue">En Progreso</Badge>
) : (
<Badge variant="gray">No Iniciado</Badge>
)}
</div>
</div>
<div className="flex items-center space-x-4 text-sm text-gray-600 mb-3">
<span className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{module.duration}
</span>
<span className="flex items-center">
<BookOpen className="w-4 h-4 mr-1" />
{module.lessons} lecciones
</span>
<span className="flex items-center">
<Users className="w-4 h-4 mr-1" />
{module.instructor}
</span>
</div>
<div className="flex items-center justify-between mb-3">
<Badge variant={getDifficultyColor(module.difficulty)}>
{getDifficultyLabel(module.difficulty)}
</Badge>
<div className="flex items-center space-x-1">
<Award className="w-4 h-4 text-yellow-500" />
<span className="text-sm font-medium text-gray-700">{module.rating}</span>
</div>
</div>
{/* Progress Bar */}
<div className="mb-3">
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>Progreso</span>
<span>{module.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${module.progress}%` }}
></div>
</div>
</div>
{/* Topics */}
<div className="mb-4">
<p className="text-sm font-medium text-gray-700 mb-2">Temas incluidos:</p>
<div className="flex flex-wrap gap-2">
{module.topics.map((topic, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full"
>
{topic}
</span>
))}
</div>
</div>
<div className="flex space-x-2">
<Button size="sm">
<Play className="w-4 h-4 mr-2" />
{module.progress > 0 ? 'Continuar' : 'Comenzar'}
</Button>
<Button size="sm" variant="outline">
Ver Detalles
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
{/* Team Progress Sidebar */}
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso del Equipo</h3>
<div className="space-y-4">
{teamProgress.map((member, index) => (
<div key={index} className="border-b border-gray-200 pb-4 last:border-b-0">
<div className="flex items-center justify-between mb-2">
<div>
<p className="font-medium text-gray-900">{member.name}</p>
<p className="text-sm text-gray-500">{member.role}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">
{member.completedModules}/{member.totalModules}
</p>
<div className="flex items-center space-x-1">
<Award className="w-3 h-3 text-yellow-500" />
<span className="text-xs text-gray-500">{member.certificates}</span>
</div>
</div>
</div>
<p className="text-xs text-gray-600 mb-2">
Actual: {member.currentModule}
</p>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{ width: `${member.progress}%` }}
></div>
</div>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Certificaciones</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-green-50 border border-green-200 rounded-lg">
<Award className="w-5 h-5 text-green-600" />
<div>
<p className="text-sm font-medium text-green-800">Certificado en Seguridad</p>
<p className="text-xs text-green-600">Válido hasta: Dic 2024</p>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<Award className="w-5 h-5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-800">Certificado Básico</p>
<p className="text-xs text-blue-600">Completado: Ene 2024</p>
</div>
</div>
<Button size="sm" variant="outline" className="w-full">
Ver Todos los Certificados
</Button>
</div>
</Card>
</div>
</div>
</div>
);
};
export default TrainingPage;

View File

@@ -0,0 +1 @@
export { default as TrainingPage } from './TrainingPage';

View File

@@ -1,295 +0,0 @@
import React, { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Eye, EyeOff, Loader2 } from 'lucide-react';
import toast from 'react-hot-toast';
import { loginSuccess } from '../../store/slices/authSlice';
import {
useAuth,
LoginRequest
} from '../../api';
interface LoginPageProps {
// No props needed with React Router
}
interface LoginForm {
email: string;
password: string;
}
const LoginPage: React.FC<LoginPageProps> = () => {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { login, isLoading, isAuthenticated } = useAuth();
// Get the intended destination from state, default to app
const from = (location.state as any)?.from?.pathname || '/app';
const [formData, setFormData] = useState<LoginForm>({
email: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<Partial<LoginForm>>({});
const validateForm = (): boolean => {
const newErrors: Partial<LoginForm> = {};
if (!formData.email) {
newErrors.email = 'El email es obligatorio';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'El email no es válido';
}
if (!formData.password) {
newErrors.password = 'La contraseña es obligatoria';
} else if (formData.password.length < 6) {
newErrors.password = 'La contraseña debe tener al menos 6 caracteres';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
try {
const loginData: LoginRequest = {
email: formData.email,
password: formData.password,
};
await login(loginData);
toast.success('¡Bienvenido a PanIA!');
const userData = localStorage.getItem('user_data');
const token = localStorage.getItem('auth_token');
if (userData && token) {
const user = JSON.parse(userData);
// Set auth state
dispatch(loginSuccess({ user, token }));
// Navigate to intended destination
navigate(from, { replace: true });
}
} catch (error: any) {
console.error('Login error:', error);
toast.error(error.message || 'Error al iniciar sesión');
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error when user starts typing
if (errors[name as keyof LoginForm]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Logo and Header */}
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<span className="text-white text-2xl font-bold">🥖</span>
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
PanIA
</h1>
<p className="text-gray-600 text-lg">
Inteligencia Artificial para tu Panadería
</p>
<p className="text-gray-500 text-sm mt-2">
Inicia sesión para acceder a tus predicciones
</p>
</div>
{/* Login Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<form className="space-y-6" onSubmit={handleSubmit}>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Correo electrónico
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.email
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Contraseña
</label>
<div className="relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
required
value={formData.password}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.password
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
Recordarme
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
>
¿Olvidaste tu contraseña?
</a>
</div>
</div>
{/* Submit Button */}
<div>
<button
type="submit"
disabled={isLoading}
className={`
group relative w-full flex justify-center py-3 px-4 border border-transparent
text-sm font-medium rounded-xl text-white transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
${isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 hover:shadow-lg transform hover:-translate-y-0.5'
}
`}
>
{isLoading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Iniciando sesión...
</>
) : (
'Iniciar sesión'
)}
</button>
</div>
</form>
{/* Register Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
¿No tienes una cuenta?{' '}
<Link
to="/register"
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
>
Regístrate gratis
</Link>
</p>
</div>
</div>
{/* Features Preview */}
<div className="text-center">
<p className="text-xs text-gray-500 mb-4">
Más de 500 panaderías en Madrid confían en PanIA
</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Predicciones precisas
</div>
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Reduce desperdicios
</div>
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Fácil de usar
</div>
</div>
</div>
</div>
</div>
);
};
export default LoginPage;

View File

@@ -1,774 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Eye, EyeOff, Loader2, Check, CreditCard, Shield, ArrowRight } from 'lucide-react';
import toast from 'react-hot-toast';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import {
useAuth,
RegisterRequest
} from '../../api';
// Development flags
const isDevelopment = import.meta.env.DEV;
const bypassPayment = import.meta.env.VITE_BYPASS_PAYMENT === 'true';
// Initialize Stripe with Spanish market configuration (only if not bypassing)
const stripePromise = !bypassPayment ? loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '', {
locale: 'es'
}) : null;
// Stripe card element options for Spanish market
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#374151',
'::placeholder': {
color: '#9CA3AF',
},
},
invalid: {
color: '#EF4444',
},
},
hidePostalCode: false, // Keep postal code for better fraud protection
};
// Subscription pricing (monthly)
const SUBSCRIPTION_PRICE_EUR = 29.99;
interface RegisterPageProps {
onLogin: (user: any, token: string) => void;
onNavigateToLogin: () => void;
}
interface RegisterForm {
fullName: string;
email: string;
confirmEmail: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
paymentCompleted: boolean;
}
interface RegisterFormErrors {
fullName?: string;
email?: string;
confirmEmail?: string;
password?: string;
confirmPassword?: string;
acceptTerms?: string;
paymentCompleted?: string;
}
const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => {
const { register, isLoading } = useAuth();
const [formData, setFormData] = useState<RegisterForm>({
fullName: '',
email: '',
confirmEmail: '',
password: '',
confirmPassword: '',
acceptTerms: false,
paymentCompleted: false
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<RegisterFormErrors>({});
const [passwordStrength, setPasswordStrength] = useState<{
score: number;
checks: { [key: string]: boolean };
message: string;
}>({ score: 0, checks: {}, message: '' });
const [paymentStep, setPaymentStep] = useState<'form' | 'payment' | 'processing' | 'completed'>('form');
const [paymentLoading, setPaymentLoading] = useState(false);
// Update password strength in real-time
useEffect(() => {
if (formData.password) {
const validation = validatePassword(formData.password);
setPasswordStrength(validation);
}
}, [formData.password]);
// Payment processing component
const PaymentForm: React.FC<{ onPaymentSuccess: () => void }> = ({ onPaymentSuccess }) => {
const stripe = useStripe();
const elements = useElements();
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
toast.error('Stripe no está cargado correctamente');
return;
}
const card = elements.getElement(CardElement);
if (!card) {
toast.error('Elemento de tarjeta no encontrado');
return;
}
setPaymentLoading(true);
try {
// Create payment method
const { error } = await stripe.createPaymentMethod({
type: 'card',
card,
billing_details: {
name: formData.fullName,
email: formData.email,
},
});
if (error) {
throw new Error(error.message);
}
// Here you would typically create the subscription via your backend
// For now, we'll simulate a successful payment
toast.success('¡Pago procesado correctamente!');
setFormData(prev => ({ ...prev, paymentCompleted: true }));
// Skip intermediate page and proceed directly to registration
onPaymentSuccess();
} catch (error) {
console.error('Payment error:', error);
toast.error(error instanceof Error ? error.message : 'Error procesando el pago');
} finally {
setPaymentLoading(false);
}
};
return (
<form onSubmit={handlePayment} className="space-y-6">
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6">
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center">
<CreditCard className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Suscripción PanIA Pro</h3>
<p className="text-sm text-gray-600">Facturación mensual</p>
</div>
<div className="ml-auto text-right">
<div className="text-2xl font-bold text-primary-600">{SUBSCRIPTION_PRICE_EUR}</div>
<div className="text-sm text-gray-500">/mes</div>
</div>
</div>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Predicciones de demanda ilimitadas
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Análisis de tendencias avanzado
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Soporte técnico prioritario
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Integración con sistemas de punto de venta
</div>
</div>
</div>
<div className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-gray-700 mb-2 block">
Información de la tarjeta
</span>
<div className="border border-gray-300 rounded-xl p-4 focus-within:ring-2 focus-within:ring-primary-500 focus-within:border-primary-500">
<CardElement options={cardElementOptions} />
</div>
</label>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<Shield className="w-4 h-4" />
<span>Pago seguro con encriptación SSL. Powered by Stripe.</span>
</div>
</div>
<button
type="submit"
disabled={!stripe || paymentLoading}
className="w-full bg-primary-500 text-white py-3 px-4 rounded-xl font-medium hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
>
{paymentLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Procesando pago...
</>
) : (
<>
<CreditCard className="w-5 h-5 mr-2" />
Pagar {SUBSCRIPTION_PRICE_EUR}/mes
</>
)}
</button>
</form>
);
};
// Password validation based on backend rules
const validatePassword = (password: string) => {
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
numbers: /\d/.test(password),
// symbols: /[!@#$%^&*(),.?":{}|<>]/.test(password) // Backend doesn't require symbols
};
const score = Object.values(checks).filter(Boolean).length;
let message = '';
if (score < 4) {
if (!checks.length) message += 'Mínimo 8 caracteres. ';
if (!checks.uppercase) message += 'Una mayúscula. ';
if (!checks.lowercase) message += 'Una minúscula. ';
if (!checks.numbers) message += 'Un número. ';
} else {
message = '¡Contraseña segura!';
}
return { score, checks, message: message.trim() };
};
const handleFormSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate form but exclude payment requirement for first step
const newErrors: RegisterFormErrors = {};
if (!formData.fullName.trim()) {
newErrors.fullName = 'El nombre completo es obligatorio';
} else if (formData.fullName.trim().length < 2) {
newErrors.fullName = 'El nombre debe tener al menos 2 caracteres';
}
if (!formData.email) {
newErrors.email = 'El email es obligatorio';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'El email no es válido';
}
if (!formData.confirmEmail) {
newErrors.confirmEmail = 'Confirma tu email';
} else if (formData.email !== formData.confirmEmail) {
newErrors.confirmEmail = 'Los emails no coinciden';
}
if (!formData.password) {
newErrors.password = 'La contraseña es obligatoria';
} else {
const passwordValidation = validatePassword(formData.password);
if (passwordValidation.score < 4) {
newErrors.password = passwordValidation.message;
}
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Las contraseñas no coinciden';
}
if (!formData.acceptTerms) {
newErrors.acceptTerms = 'Debes aceptar los términos y condiciones';
}
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return;
// Move to payment step, or bypass if in development mode
if (bypassPayment) {
// Development bypass: simulate payment completion and proceed directly to registration
setFormData(prev => ({ ...prev, paymentCompleted: true }));
toast.success('🚀 Modo desarrollo: Pago omitido');
// Proceed directly to registration without intermediate page
handleRegistrationComplete();
} else {
setPaymentStep('payment');
}
};
const handleRegistrationComplete = async () => {
if (!bypassPayment && !formData.paymentCompleted) {
toast.error('El pago debe completarse antes del registro');
return;
}
try {
const registerData: RegisterRequest = {
email: formData.email,
password: formData.password,
full_name: formData.fullName,
role: 'user' // Default role
};
await register(registerData);
toast.success('¡Registro exitoso! Bienvenido a PanIA');
// The useAuth hook handles auto-login after registration
// Get the user data from localStorage since useAuth auto-logs in
const userData = localStorage.getItem('user_data');
const token = localStorage.getItem('auth_token');
if (userData && token) {
onLogin(JSON.parse(userData), token);
}
} catch (error) {
console.error('Registration error:', error);
toast.error(error instanceof Error ? error.message : 'Error en el registro');
// Reset payment if registration fails
setFormData(prev => ({ ...prev, paymentCompleted: false }));
setPaymentStep('payment');
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
// Clear error when user starts typing
if (errors[name as keyof RegisterFormErrors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
};
// Render different content based on payment step
if (paymentStep === 'payment' && !bypassPayment) {
return (
<Elements stripe={stripePromise}>
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Logo and Header */}
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<span className="text-white text-2xl font-bold">🥖</span>
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
Finalizar Registro
</h1>
<p className="text-gray-600 text-lg">
Solo un paso más para comenzar
</p>
<p className="text-gray-500 text-sm mt-2">
Suscripción segura con Stripe
</p>
</div>
{/* Payment Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<PaymentForm onPaymentSuccess={handleRegistrationComplete} />
<button
onClick={() => setPaymentStep('form')}
className="w-full mt-4 text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
Volver al formulario
</button>
</div>
</div>
</div>
</Elements>
);
}
if (paymentStep === 'completed') {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-green-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<Check className="text-white text-2xl" />
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
¡Bienvenido a PanIA!
</h1>
<p className="text-gray-600 text-lg">
Tu cuenta ha sido creada exitosamente
</p>
<p className="text-gray-500 text-sm mt-2">
Redirigiendo al panel de control...
</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Logo and Header */}
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<span className="text-white text-2xl font-bold">🥖</span>
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
Únete a PanIA
</h1>
<p className="text-gray-600 text-lg">
Crea tu cuenta y transforma tu panadería
</p>
<p className="text-gray-500 text-sm mt-2">
Únete a más de 500 panaderías en Madrid
</p>
</div>
{/* Register Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<form className="space-y-6" onSubmit={handleFormSubmit}>
{/* Full Name Field */}
<div>
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
Nombre completo
</label>
<input
id="fullName"
name="fullName"
type="text"
autoComplete="name"
required
value={formData.fullName}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.fullName
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="Tu nombre completo"
/>
{errors.fullName && (
<p className="mt-1 text-sm text-red-600">{errors.fullName}</p>
)}
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Correo electrónico
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.email
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Confirm Email Field */}
<div>
<label htmlFor="confirmEmail" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar correo electrónico
</label>
<input
id="confirmEmail"
name="confirmEmail"
type="email"
autoComplete="email"
required
value={formData.confirmEmail}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.confirmEmail
? 'border-red-300 bg-red-50'
: formData.confirmEmail && formData.email === formData.confirmEmail
? 'border-green-300 bg-green-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{formData.confirmEmail && formData.email === formData.confirmEmail && (
<div className="absolute inset-y-0 right-3 flex items-center mt-8">
<Check className="h-5 w-5 text-green-500" />
</div>
)}
{errors.confirmEmail && (
<p className="mt-1 text-sm text-red-600">{errors.confirmEmail}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Contraseña
</label>
<div className="relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.password}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.password
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{/* Enhanced Password Strength Indicator */}
{formData.password && (
<div className="mt-2">
<div className="grid grid-cols-4 gap-1">
{Object.entries(passwordStrength.checks).map(([key, passed], index) => {
const labels = {
length: '8+ caracteres',
uppercase: 'Mayúscula',
lowercase: 'Minúscula',
numbers: 'Número'
};
return (
<div key={key} className={`text-xs p-1 rounded text-center ${
passed ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
}`}>
{passed ? '✓' : '○'} {labels[key as keyof typeof labels]}
</div>
);
})}
</div>
<p className={`text-xs mt-1 ${
passwordStrength.score === 4 ? 'text-green-600' : 'text-gray-600'
}`}>
{passwordStrength.message}
</p>
</div>
)}
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar contraseña
</label>
<div className="relative">
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.confirmPassword
? 'border-red-300 bg-red-50'
: formData.confirmPassword && formData.password === formData.confirmPassword
? 'border-green-300 bg-green-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
{formData.confirmPassword && formData.password === formData.confirmPassword && (
<div className="absolute inset-y-0 right-10 flex items-center">
<Check className="h-5 w-5 text-green-500" />
</div>
)}
</div>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
)}
</div>
{/* Terms and Conditions */}
<div>
<div className="flex items-start">
<input
id="acceptTerms"
name="acceptTerms"
type="checkbox"
checked={formData.acceptTerms}
onChange={handleInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded mt-0.5"
/>
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-700">
Acepto los{' '}
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
términos y condiciones
</a>{' '}
y la{' '}
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
política de privacidad
</a>
</label>
</div>
{errors.acceptTerms && (
<p className="mt-1 text-sm text-red-600">{errors.acceptTerms}</p>
)}
</div>
{/* Submit Button */}
<div>
<button
type="submit"
disabled={isLoading}
className={`
group relative w-full flex justify-center py-3 px-4 border border-transparent
text-sm font-medium rounded-xl text-white transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
${isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 hover:shadow-lg transform hover:-translate-y-0.5'
}
`}
>
{isLoading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Validando...
</>
) : (
<>
{bypassPayment ? 'Crear Cuenta (Dev)' : 'Continuar al Pago'}
<ArrowRight className="h-5 w-5 ml-2" />
</>
)}
</button>
</div>
</form>
{/* Login Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
¿Ya tienes una cuenta?{' '}
<button
onClick={onNavigateToLogin}
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
>
Inicia sesión aquí
</button>
</p>
</div>
</div>
{/* Benefits */}
<div className="text-center">
{bypassPayment && (
<div className="mb-4 p-2 bg-yellow-100 border border-yellow-300 rounded-lg">
<p className="text-xs text-yellow-800">
🚀 Modo Desarrollo: Pago omitido para pruebas
</p>
</div>
)}
<p className="text-xs text-gray-500 mb-4">
{bypassPayment
? 'Desarrollo • Pruebas • Sin pago requerido'
: 'Proceso seguro • Cancela en cualquier momento • Soporte 24/7'
}
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-gray-400">
<div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Predicciones IA
</div>
<div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Análisis de demanda
</div>
<div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Reduce desperdicios
</div>
</div>
</div>
</div>
</div>
);
};
export default RegisterPage;

View File

@@ -1,429 +0,0 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Eye, EyeOff, Loader2, User, Mail, Lock } from 'lucide-react';
import toast from 'react-hot-toast';
import { loginSuccess } from '../../store/slices/authSlice';
import { authService } from '../../api/services/auth.service';
import { onboardingService } from '../../api/services/onboarding.service';
import type { RegisterRequest } from '../../api/types/auth';
interface RegisterForm {
fullName: string;
email: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
}
interface RegisterFormErrors {
fullName?: string;
email?: string;
password?: string;
confirmPassword?: string;
acceptTerms?: string;
}
const RegisterPage: React.FC = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [formData, setFormData] = useState<RegisterForm>({
fullName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: false
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<RegisterFormErrors>({});
const validateForm = (): boolean => {
const newErrors: RegisterFormErrors = {};
if (!formData.fullName.trim()) {
newErrors.fullName = 'El nombre es obligatorio';
}
if (!formData.email) {
newErrors.email = 'El email es obligatorio';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'El email no es válido';
}
if (!formData.password) {
newErrors.password = 'La contraseña es obligatoria';
} else if (formData.password.length < 8) {
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Las contraseñas no coinciden';
}
if (!formData.acceptTerms) {
newErrors.acceptTerms = 'Debes aceptar los términos y condiciones';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
try {
// Prepare registration data
const registrationData: RegisterRequest = {
email: formData.email,
password: formData.password,
full_name: formData.fullName,
role: 'admin',
language: 'es'
};
// Call real authentication API
const response = await authService.register(registrationData);
// Extract user data from response
const userData = response.user;
if (!userData) {
throw new Error('No se recibieron datos del usuario');
}
// Convert API response to internal format
const user = {
id: userData.id,
email: userData.email,
fullName: userData.full_name,
role: (userData.role as "owner" | "admin" | "manager" | "worker") || 'admin',
isOnboardingComplete: false, // New users need onboarding
tenant_id: userData.tenant_id
};
// Store tokens in localStorage
localStorage.setItem('auth_token', response.access_token);
if (response.refresh_token) {
localStorage.setItem('refresh_token', response.refresh_token);
}
localStorage.setItem('user_data', JSON.stringify(user));
// Set auth state
dispatch(loginSuccess({ user, token: response.access_token }));
// Mark user_registered step as completed in onboarding
try {
await onboardingService.completeStep('user_registered', {
user_id: userData.id,
registration_completed_at: new Date().toISOString(),
registration_method: 'web_form'
});
console.log('✅ user_registered step marked as completed');
} catch (onboardingError) {
console.warn('Failed to mark user_registered step as completed:', onboardingError);
// Don't block the flow if onboarding step completion fails
}
toast.success('¡Cuenta creada exitosamente!');
// Navigate to onboarding
navigate('/app/onboarding');
} catch (error: any) {
console.error('Registration error:', error);
const errorMessage = error?.response?.data?.detail || error?.message || 'Error al crear la cuenta';
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
// Clear error when user starts typing
if (errors[name as keyof RegisterForm]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Logo and Header */}
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<span className="text-white text-2xl font-bold">🥖</span>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Únete a PanIA
</h1>
<p className="text-gray-600 text-lg">
Crea tu cuenta y comienza a optimizar tu panadería
</p>
</div>
{/* Registration Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<form className="space-y-6" onSubmit={handleSubmit}>
{/* Full Name Field */}
<div>
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
Nombre completo
</label>
<div className="relative">
<User className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
id="fullName"
name="fullName"
type="text"
required
value={formData.fullName}
onChange={handleInputChange}
className={`
appearance-none relative block w-full pl-10 pr-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.fullName
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="Juan Pérez"
/>
</div>
{errors.fullName && (
<p className="mt-1 text-sm text-red-600">{errors.fullName}</p>
)}
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Correo electrónico
</label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleInputChange}
className={`
appearance-none relative block w-full pl-10 pr-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.email
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
</div>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Contraseña
</label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.password}
onChange={handleInputChange}
className={`
appearance-none relative block w-full pl-10 pr-12 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.password
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar contraseña
</label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
className={`
appearance-none relative block w-full pl-10 pr-12 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.confirmPassword
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
)}
</div>
{/* Terms and Conditions */}
<div>
<div className="flex items-center">
<input
id="acceptTerms"
name="acceptTerms"
type="checkbox"
checked={formData.acceptTerms}
onChange={handleInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-700">
Acepto los{' '}
<a href="#" className="text-primary-600 hover:text-primary-500">
términos y condiciones
</a>{' '}
y la{' '}
<a href="#" className="text-primary-600 hover:text-primary-500">
política de privacidad
</a>
</label>
</div>
{errors.acceptTerms && (
<p className="mt-1 text-sm text-red-600">{errors.acceptTerms}</p>
)}
</div>
{/* Submit Button */}
<div>
<button
type="submit"
disabled={isLoading}
className={`
group relative w-full flex justify-center py-3 px-4 border border-transparent
text-sm font-medium rounded-xl text-white transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
${isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 hover:shadow-lg transform hover:-translate-y-0.5'
}
`}
>
{isLoading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Creando cuenta...
</>
) : (
'Crear cuenta'
)}
</button>
</div>
</form>
{/* Login Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
¿Ya tienes una cuenta?{' '}
<Link
to="/login"
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
>
Inicia sesión
</Link>
</p>
</div>
</div>
{/* Features Preview */}
<div className="text-center">
<p className="text-xs text-gray-500 mb-4">
Prueba gratuita de 14 días No se requiere tarjeta de crédito
</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Setup en 5 minutos
</div>
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Soporte incluido
</div>
<div className="flex items-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Cancela cuando quieras
</div>
</div>
</div>
</div>
</div>
);
};
export default RegisterPage;

View File

@@ -1,247 +0,0 @@
import React, { useState } from 'react';
import { useDashboard } from '../../hooks/useDashboard';
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
// Import simplified components for fallback
import TodayRevenue from '../../components/simple/TodayRevenue';
import TodayProduction from '../../components/simple/TodayProduction';
import QuickActions from '../../components/simple/QuickActions';
import QuickOverview from '../../components/simple/QuickOverview';
import OrderSuggestions from '../../components/simple/OrderSuggestions';
import WeatherContext from '../../components/simple/WeatherContext';
interface DashboardPageProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
onNavigateToProduction?: () => void;
onNavigateToInventory?: () => void;
onNavigateToRecipes?: () => void;
onNavigateToSales?: () => void;
}
const DashboardPage: React.FC<DashboardPageProps> = ({
onNavigateToOrders,
onNavigateToReports,
onNavigateToProduction,
onNavigateToInventory,
onNavigateToRecipes,
onNavigateToSales
}) => {
const {
weather,
tenantId,
isLoading,
error,
reload,
todayForecasts,
metrics
} = useDashboard();
// Use real API data for order suggestions - pass tenantId from dashboard
const {
dailyOrders: realDailyOrders,
weeklyOrders: realWeeklyOrders,
isLoading: ordersLoading,
error: ordersError
} = useOrderSuggestions(tenantId);
// Debug order suggestions
console.log('📈 Dashboard: OrderSuggestions data:', {
dailyOrders: realDailyOrders,
weeklyOrders: realWeeklyOrders,
isLoading: ordersLoading,
error: ordersError
});
// Transform forecast data for production component
const mockProduction = todayForecasts.map((forecast, index) => ({
id: `prod-${index}`,
product: forecast.product,
emoji: forecast.product.toLowerCase().includes('croissant') ? '🥐' :
forecast.product.toLowerCase().includes('pan') ? '🍞' :
forecast.product.toLowerCase().includes('magdalena') ? '🧁' : '🥖',
quantity: forecast.predicted,
status: 'pending' as const,
scheduledTime: index < 3 ? '06:00' : '14:00',
confidence: forecast.confidence === 'high' ? 0.9 :
forecast.confidence === 'medium' ? 0.7 : 0.5
}));
// Helper function for greeting
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return 'Buenos días';
if (hour < 18) return 'Buenas tardes';
return 'Buenas noches';
};
// Classic dashboard view
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Cargando datos de tu panadería...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h3 className="text-red-800 font-medium">Error al cargar datos</h3>
<p className="text-red-700 mt-1">{error}</p>
<button
onClick={() => reload()}
className="mt-4 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg transition-colors"
>
Reintentar
</button>
</div>
);
}
return (
<div className="p-4 md:p-6 space-y-6 bg-gray-50 min-h-screen">
{/* Welcome Header */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
{getGreeting()}! 👋
</h1>
<p className="text-gray-600 mt-1">
{new Date().toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</p>
</div>
<div className="mt-4 sm:mt-0 flex items-center space-x-4">
{weather && (
<div className="flex items-center text-sm text-gray-600 bg-gray-50 rounded-lg px-4 py-2">
<span className="text-lg mr-2">
{weather.precipitation && weather.precipitation > 0 ? '🌧️' : weather.temperature && weather.temperature > 20 ? '☀️' : '⛅'}
</span>
<div className="flex flex-col">
<span>{weather.temperature?.toFixed(1) || '--'}°C</span>
<span className="text-xs text-gray-500">AEMET</span>
</div>
</div>
)}
<div className="text-right">
<div className="text-sm font-medium text-gray-900">Estado del sistema</div>
<div className="text-xs text-green-600 flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
Operativo
</div>
</div>
</div>
</div>
</div>
{/* Critical Section - Always Visible */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Revenue - Most Important */}
<TodayRevenue
currentRevenue={metrics?.totalSales || 287.50}
previousRevenue={256.25}
dailyTarget={350}
/>
{/* Quick Actions - Easy Access */}
<QuickActions
onActionClick={(actionId) => {
console.log('Action clicked:', actionId);
// Handle quick actions
switch (actionId) {
case 'view_orders':
onNavigateToOrders?.();
break;
case 'view_sales':
onNavigateToReports?.();
break;
default:
// Handle other actions
break;
}
}}
/>
</div>
{/* Order Suggestions - Real AI-Powered Recommendations */}
{ordersLoading ? (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<span className="ml-3 text-gray-600">Cargando sugerencias de pedidos...</span>
</div>
</div>
) : ordersError ? (
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
<h3 className="text-red-800 font-medium">Error al cargar sugerencias</h3>
<p className="text-red-700 mt-1">{ordersError}</p>
</div>
) : (
<OrderSuggestions
dailyOrders={realDailyOrders}
weeklyOrders={realWeeklyOrders}
onUpdateQuantity={(orderId, quantity, type) => {
console.log('Update order quantity:', orderId, quantity, type);
// In real implementation, this would update the backend
}}
onCreateOrder={(items, type) => {
console.log('Create order:', type, items);
// Navigate to orders page to complete the order
onNavigateToOrders?.();
}}
onViewDetails={() => {
onNavigateToOrders?.();
}}
/>
)}
{/* Weather & Context - Comprehensive AEMET Data */}
<WeatherContext />
{/* Production Section - Core Operations */}
<TodayProduction
items={mockProduction}
onUpdateQuantity={(itemId: string, quantity: number) => {
console.log('Update quantity:', itemId, quantity);
}}
onUpdateStatus={(itemId: string, status: any) => {
console.log('Update status:', itemId, status);
}}
/>
{/* Quick Overview - Supporting Information */}
<QuickOverview
onNavigateToOrders={onNavigateToOrders}
onNavigateToReports={onNavigateToReports}
/>
{/* Success Message - When Everything is Good */}
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
<div className="text-4xl mb-2">🎉</div>
<h4 className="font-medium text-green-900">¡Todo bajo control!</h4>
<p className="text-green-700 text-sm mt-1">
Tu panadería está funcionando perfectamente.
</p>
</div>
</div>
);
};
export default DashboardPage;

View File

@@ -1,683 +0,0 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, Calendar, Cloud, AlertTriangle, Info, RefreshCw } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { useForecast } from '../../api/hooks/useForecast';
import { useInventoryProducts } from '../../api/hooks/useInventory';
import { useTenantId } from '../../hooks/useTenantId';
import type { ForecastResponse } from '../../api/types/forecasting';
interface ForecastData {
date: string;
product: string;
predicted: number;
confidence: 'high' | 'medium' | 'low';
factors: string[];
weatherImpact?: string;
inventory_product_id?: string;
confidence_lower?: number;
confidence_upper?: number;
}
interface WeatherAlert {
type: 'rain' | 'heat' | 'cold';
impact: string;
recommendation: string;
}
const ForecastPage: React.FC = () => {
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [selectedProduct, setSelectedProduct] = useState('all');
const [forecastData, setForecastData] = useState<ForecastData[]>([]);
const [weatherAlert, setWeatherAlert] = useState<WeatherAlert | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [inventoryItems, setInventoryItems] = useState<any[]>([]);
// Hooks
const { tenantId } = useTenantId();
const {
forecasts,
isLoading: forecastLoading,
error: forecastError,
createSingleForecast,
getForecasts,
getForecastAlerts,
exportForecasts
} = useForecast();
const {
getProductsList,
isLoading: inventoryLoading,
error: inventoryError
} = useInventoryProducts();
// Debug logging
if (process.env.NODE_ENV === 'development') {
console.log('ForecastPage - inventoryItems:', inventoryItems);
console.log('ForecastPage - inventoryLoading:', inventoryLoading);
console.log('ForecastPage - tenantId:', tenantId);
}
// Derived state
const isLoading = forecastLoading || inventoryLoading;
const products = (inventoryItems || []).map(item => ({
id: item.id,
name: item.name || 'Unknown Product'
}));
// Sample forecast data for the next 7 days - will be populated by real data
const [sampleForecastData, setSampleForecastData] = useState<any[]>(() => {
// Generate 7 days starting from today
const data = [];
for (let i = 0; i < 7; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
data.push({
date: date.toISOString().split('T')[0],
croissants: 0,
pan: 0,
cafe: 0
});
}
return data;
});
// Load inventory items on component mount
useEffect(() => {
const loadProducts = async () => {
if (tenantId) {
try {
const products = await getProductsList(tenantId);
// Always use real inventory products - no hardcoded fallbacks
if (products.length === 0) {
console.warn('⚠️ No finished products found in inventory for forecasting');
setInventoryItems([]);
} else {
// Map products to the expected format
setInventoryItems(products.map(p => ({
id: p.inventory_product_id,
name: p.name
})));
}
} catch (error) {
console.error('Failed to load products:', error);
// Don't use fake fallback products - show empty state instead
setInventoryItems([]);
}
}
};
loadProducts();
}, [tenantId, getProductsList]);
// Transform API forecasts to our local format
const transformForecastResponse = (forecast: ForecastResponse): ForecastData => {
// Find product name from inventory items
const inventoryItem = (inventoryItems || []).find(item => item.id === forecast.inventory_product_id);
const productName = inventoryItem?.name || 'Unknown Product';
// Determine confidence level based on confidence_level number
let confidence: 'high' | 'medium' | 'low' = 'medium';
if (forecast.confidence_level) {
if (forecast.confidence_level >= 0.8) confidence = 'high';
else if (forecast.confidence_level >= 0.6) confidence = 'medium';
else confidence = 'low';
}
// Extract factors from features_used or provide defaults
const factors = [];
if (forecast.features_used) {
if (forecast.features_used.is_weekend === false) factors.push('Día laboral');
else if (forecast.features_used.is_weekend === true) factors.push('Fin de semana');
if (forecast.features_used.is_holiday === false) factors.push('Sin eventos especiales');
else if (forecast.features_used.is_holiday === true) factors.push('Día festivo');
if (forecast.features_used.weather_description) factors.push(`Clima: ${forecast.features_used.weather_description}`);
else factors.push('Clima estable');
} else {
factors.push('Día laboral', 'Clima estable', 'Sin eventos especiales');
}
// Determine weather impact
let weatherImpact = 'Sin impacto significativo';
if (forecast.features_used?.temperature) {
const temp = forecast.features_used.temperature;
if (temp < 10) weatherImpact = 'Temperatura baja - posible aumento en bebidas calientes';
else if (temp > 25) weatherImpact = 'Temperatura alta - posible reducción en productos horneados';
}
return {
date: forecast.forecast_date.split('T')[0], // Convert to YYYY-MM-DD
product: productName,
predicted: Math.round(forecast.predicted_demand),
confidence,
factors,
weatherImpact,
inventory_product_id: forecast.inventory_product_id,
confidence_lower: forecast.confidence_lower,
confidence_upper: forecast.confidence_upper,
};
};
// Generate forecasts for available products
const generateForecasts = async () => {
if (!tenantId || !inventoryItems || inventoryItems.length === 0) return;
setIsGenerating(true);
try {
// Generate forecasts for products with trained models only
const productsToForecast = inventoryItems;
const chartData = [];
// Generate data for the next 7 days
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
const forecastDate = new Date();
forecastDate.setDate(forecastDate.getDate() + dayOffset);
const dateStr = forecastDate.toISOString().split('T')[0];
const dayData = {
date: dateStr,
croissants: 0,
pan: 0,
cafe: 0
};
// Generate forecasts for each product for this day
const dayForecasts = await Promise.all(
productsToForecast.map(async (item) => {
try {
const forecastResponses = await createSingleForecast(tenantId, {
inventory_product_id: item.id,
forecast_date: dateStr,
forecast_days: 1,
location: 'Madrid, Spain',
include_external_factors: true,
confidence_intervals: true,
});
return forecastResponses.map(transformForecastResponse);
} catch (error) {
console.error(`Failed to generate forecast for ${item.name} on ${dateStr}:`, error);
return [];
}
})
);
// Process forecasts for this day
const flatDayForecasts = dayForecasts.flat();
flatDayForecasts.forEach((forecast) => {
const key = forecast.product.toLowerCase();
if (key.includes('croissant')) dayData.croissants = forecast.predicted;
else if (key.includes('pan')) dayData.pan = forecast.predicted;
else if (key.includes('cafe')) dayData.cafe = forecast.predicted;
});
chartData.push(dayData);
// Store forecasts for selected date display
if (dateStr === selectedDate) {
setForecastData(flatDayForecasts);
}
}
// Update chart with 7 days of data
setSampleForecastData(chartData);
// Set a sample weather alert
setWeatherAlert({
type: 'rain',
impact: 'Condiciones climáticas estables para el día seleccionado',
recommendation: 'Mantener la producción según las predicciones'
});
} catch (error) {
console.error('Error generating forecasts:', error);
} finally {
setIsGenerating(false);
}
};
// Load existing forecasts when component mounts or date changes
useEffect(() => {
const loadExistingForecasts = async () => {
if (!tenantId) return;
try {
// Try to get existing forecasts first
const existingForecasts = await getForecasts(tenantId);
console.log('🔍 ForecastPage - existingForecasts:', existingForecasts);
console.log('🔍 ForecastPage - existingForecasts type:', typeof existingForecasts);
console.log('🔍 ForecastPage - existingForecasts isArray:', Array.isArray(existingForecasts));
if (Array.isArray(existingForecasts) && existingForecasts.length > 0) {
// Filter forecasts for selected date
const dateForecasts = existingForecasts
.filter(f => f.forecast_date && f.forecast_date.split('T')[0] === selectedDate)
.map(transformForecastResponse);
if (dateForecasts.length > 0) {
setForecastData(dateForecasts);
}
// Update 7-day chart with existing forecasts
const chartData = [];
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
const forecastDate = new Date();
forecastDate.setDate(forecastDate.getDate() + dayOffset);
const dateStr = forecastDate.toISOString().split('T')[0];
const dayData = {
date: dateStr,
croissants: 0,
pan: 0,
cafe: 0
};
// Find existing forecasts for this day
const dayForecasts = existingForecasts
.filter(f => f.forecast_date && f.forecast_date.split('T')[0] === dateStr)
.map(transformForecastResponse);
dayForecasts.forEach((forecast) => {
const key = forecast.product.toLowerCase();
if (key.includes('croissant')) dayData.croissants = forecast.predicted;
else if (key.includes('pan')) dayData.pan = forecast.predicted;
else if (key.includes('cafe')) dayData.cafe = forecast.predicted;
});
chartData.push(dayData);
}
setSampleForecastData(chartData);
} else {
console.log('🔍 ForecastPage - No existing forecasts found or invalid format');
}
// Load alerts
const alerts = await getForecastAlerts(tenantId);
if (Array.isArray(alerts) && alerts.length > 0) {
// Convert first alert to weather alert format
const alert = alerts[0];
setWeatherAlert({
type: 'rain', // Default type
impact: alert.message || 'Alert information not available',
recommendation: 'Revisa las recomendaciones del sistema'
});
}
} catch (error) {
console.error('Error loading existing forecasts:', error);
}
};
if (inventoryItems && inventoryItems.length > 0) {
loadExistingForecasts();
}
}, [tenantId, selectedDate, inventoryItems, getForecasts, getForecastAlerts]);
const getConfidenceColor = (confidence: string) => {
switch (confidence) {
case 'high':
return 'bg-success-100 text-success-800 border-success-200';
case 'medium':
return 'bg-warning-100 text-warning-800 border-warning-200';
case 'low':
return 'bg-danger-100 text-danger-800 border-danger-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getConfidenceLabel = (confidence: string) => {
switch (confidence) {
case 'high':
return 'Alta confianza';
case 'medium':
return 'Confianza media';
case 'low':
return 'Baja confianza';
default:
return 'N/A';
}
};
const filteredForecasts = selectedProduct === 'all'
? forecastData
: forecastData.filter(f => f.product.toLowerCase().includes(selectedProduct.toLowerCase()));
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="h-32 bg-gray-200 rounded-xl"></div>
<div className="h-32 bg-gray-200 rounded-xl"></div>
</div>
<div className="h-64 bg-gray-200 rounded-xl"></div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Predicciones IA</h1>
<p className="text-gray-600 mt-1">
Predicciones inteligentes para optimizar tu producción
</p>
</div>
</div>
{/* Weather Alert */}
{weatherAlert && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start">
<Cloud className="h-5 w-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" />
<div className="flex-1">
<h4 className="font-medium text-blue-900">Alerta Meteorológica</h4>
<p className="text-blue-800 text-sm mt-1">{weatherAlert.impact}</p>
<p className="text-blue-700 text-sm mt-2 font-medium">
💡 Recomendación: {weatherAlert.recommendation}
</p>
</div>
</div>
</div>
)}
{/* Controls */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Fecha de predicción
</label>
<div className="relative">
<Calendar className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
max={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Filtrar por producto
</label>
<select
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="all">Todos los productos</option>
{products.map(product => (
<option key={product.id} value={product.name.toLowerCase()}>{product.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Generar predicciones
</label>
<button
onClick={generateForecasts}
disabled={isGenerating || !tenantId || !(inventoryItems && inventoryItems.length > 0)}
className="w-full px-4 py-3 bg-primary-500 hover:bg-primary-600 disabled:bg-gray-300 text-white rounded-xl transition-colors flex items-center justify-center"
>
{isGenerating ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
Generando...
</>
) : (
<>
<TrendingUp className="h-4 w-4 mr-2" />
Generar Predicciones
</>
)}
</button>
</div>
</div>
{forecastError && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800 text-sm">Error: {forecastError}</span>
</div>
</div>
)}
{inventoryItems.length === 0 && !isLoading && !isGenerating && (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<span className="text-yellow-800 text-sm">
<strong>No hay productos disponibles para predicciones.</strong>
<br />Para usar esta funcionalidad, necesitas crear productos terminados (como pan, croissants, etc.) en tu inventario con tipo "Producto terminado".
</span>
</div>
</div>
)}
{inventoryItems.length > 0 && forecastData.length === 0 && !isLoading && !isGenerating && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center">
<Info className="h-5 w-5 text-blue-600 mr-2" />
<span className="text-blue-800 text-sm">
No hay predicciones para la fecha seleccionada. Haz clic en "Generar Predicciones" para crear nuevas predicciones.
</span>
</div>
</div>
)}
</div>
{/* Forecast Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredForecasts.map((forecast, index) => (
<div key={index} className="bg-white p-6 rounded-xl shadow-soft hover:shadow-medium transition-shadow">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">{forecast.product}</h3>
<span className={`px-3 py-1 rounded-lg text-xs font-medium border ${getConfidenceColor(forecast.confidence)}`}>
{getConfidenceLabel(forecast.confidence)}
</span>
</div>
<div className="mb-4">
<div className="flex items-baseline">
<span className="text-3xl font-bold text-gray-900">{forecast.predicted}</span>
<span className="text-gray-500 ml-2">unidades</span>
</div>
<p className="text-sm text-gray-600 mt-1">
Predicción para {new Date(forecast.date).toLocaleDateString('es-ES', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2 flex items-center">
<Info className="h-4 w-4 mr-1" />
Factores considerados
</h4>
<ul className="space-y-1">
{forecast.factors.map((factor, i) => (
<li key={i} className="text-xs text-gray-600 flex items-center">
<span className="w-1.5 h-1.5 bg-primary-500 rounded-full mr-2"></span>
{factor}
</li>
))}
</ul>
</div>
{forecast.weatherImpact && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-1 flex items-center">
<Cloud className="h-4 w-4 mr-1" />
Impacto del clima
</h4>
<p className="text-xs text-gray-600">{forecast.weatherImpact}</p>
</div>
)}
</div>
</div>
))}
</div>
{/* Trend Chart */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Tendencia de Predicciones (Próximos 7 Días)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={sampleForecastData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
stroke="#666"
fontSize={12}
tickFormatter={(value) => {
const date = new Date(value);
return `${date.getDate()}/${date.getMonth() + 1}`;
}}
/>
<YAxis stroke="#666" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
labelFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long'
});
}}
/>
<Line
type="monotone"
dataKey="croissants"
stroke="#f97316"
strokeWidth={3}
name="Croissants"
dot={{ fill: '#f97316', strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="pan"
stroke="#22c55e"
strokeWidth={3}
name="Pan de molde"
dot={{ fill: '#22c55e', strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="cafe"
stroke="#3b82f6"
strokeWidth={3}
name="Café"
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Recommendations */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Recomendaciones Inteligentes
</h3>
<div className="space-y-4">
<div className="flex items-start p-4 bg-success-50 rounded-lg border border-success-200">
<TrendingUp className="h-5 w-5 text-success-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h4 className="font-medium text-success-900">Oportunidad de Aumento</h4>
<p className="text-success-800 text-sm mt-1">
La demanda de café aumentará un 15% esta semana por las bajas temperaturas.
Considera aumentar el stock de café y bebidas calientes.
</p>
</div>
</div>
<div className="flex items-start p-4 bg-warning-50 rounded-lg border border-warning-200">
<AlertTriangle className="h-5 w-5 text-warning-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h4 className="font-medium text-warning-900">Ajuste Recomendado</h4>
<p className="text-warning-800 text-sm mt-1">
Las napolitanas muestran alta variabilidad. Considera reducir la producción
inicial y hornear más según demanda en tiempo real.
</p>
</div>
</div>
<div className="flex items-start p-4 bg-blue-50 rounded-lg border border-blue-200">
<Info className="h-5 w-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h4 className="font-medium text-blue-900">Optimización de Horarios</h4>
<p className="text-blue-800 text-sm mt-1">
El pico de demanda de croissants será entre 7:30-9:00 AM.
Asegúrate de tener suficiente stock listo para esas horas.
</p>
</div>
</div>
</div>
</div>
{/* Export Actions */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Acciones Rápidas
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<button
onClick={() => tenantId && exportForecasts(tenantId, 'csv')}
disabled={!tenantId || forecastData.length === 0}
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
>
<div className="font-medium text-gray-900">Exportar Predicciones</div>
<div className="text-sm text-gray-500 mt-1">Descargar en formato CSV</div>
</button>
<button
onClick={generateForecasts}
disabled={isGenerating || !tenantId || !(inventoryItems && inventoryItems.length > 0)}
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
>
<div className="font-medium text-gray-900">Actualizar Predicciones</div>
<div className="text-sm text-gray-500 mt-1">Generar nuevas predicciones</div>
</button>
<button
onClick={() => tenantId && getForecastAlerts(tenantId)}
disabled={!tenantId}
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
>
<div className="font-medium text-gray-900">Ver Alertas</div>
<div className="text-sm text-gray-500 mt-1">Revisar notificaciones del sistema</div>
</button>
</div>
</div>
</div>
);
};
export default ForecastPage;

View File

@@ -0,0 +1,7 @@
// Public pages
export * from './public';
// App pages
export { default as DashboardPage } from './app/DashboardPage';
export * from './app/operations';
export * from './app/analytics';

View File

@@ -1,514 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Search,
Plus,
Filter,
Download,
Upload,
LayoutGrid,
List,
Package,
TrendingDown,
AlertTriangle,
Loader,
RefreshCw,
BarChart3,
Calendar
} from 'lucide-react';
import toast from 'react-hot-toast';
import { useInventory } from '../../api/hooks/useInventory';
import {
InventorySearchParams,
ProductType,
CreateInventoryItemRequest,
UpdateInventoryItemRequest,
StockAdjustmentRequest,
InventoryItem
} from '../../api/services/inventory.service';
import InventoryItemCard from '../../components/inventory/InventoryItemCard';
type ViewMode = 'grid' | 'list';
interface FilterState {
search: string;
product_type?: ProductType;
category?: string;
is_active?: boolean;
low_stock_only?: boolean;
expiring_soon_only?: boolean;
sort_by?: 'name' | 'category' | 'stock_level' | 'last_movement' | 'created_at';
sort_order?: 'asc' | 'desc';
}
interface InventoryPageProps {
view?: string;
}
const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' }) => {
const {
items,
stockLevels,
dashboardData,
isLoading,
error,
pagination,
loadItems,
createItem,
updateItem,
deleteItem,
adjustStock,
refresh,
clearError
} = useInventory();
// Local state
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [filters, setFilters] = useState<FilterState>({
search: '',
sort_by: 'name',
sort_order: 'asc'
});
const [showFilters, setShowFilters] = useState(false);
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
// Load items when filters change
useEffect(() => {
const searchParams: InventorySearchParams = {
...filters,
page: 1,
limit: 20
};
// Remove empty values
Object.keys(searchParams).forEach(key => {
if (searchParams[key as keyof InventorySearchParams] === '' ||
searchParams[key as keyof InventorySearchParams] === undefined) {
delete searchParams[key as keyof InventorySearchParams];
}
});
loadItems(searchParams);
}, [filters, loadItems]);
// Handle search
const handleSearch = useCallback((value: string) => {
setFilters(prev => ({ ...prev, search: value }));
}, []);
// Handle filter changes
const handleFilterChange = useCallback((key: keyof FilterState, value: any) => {
setFilters(prev => ({ ...prev, [key]: value }));
}, []);
// Clear all filters
const clearFilters = useCallback(() => {
setFilters({
search: '',
sort_by: 'name',
sort_order: 'asc'
});
}, []);
// Handle item selection
const toggleItemSelection = (itemId: string) => {
const newSelection = new Set(selectedItems);
if (newSelection.has(itemId)) {
newSelection.delete(itemId);
} else {
newSelection.add(itemId);
}
setSelectedItems(newSelection);
};
// Handle stock adjustment
const handleStockAdjust = async (item: InventoryItem, adjustment: StockAdjustmentRequest) => {
const result = await adjustStock(item.id, adjustment);
if (result) {
// Refresh data to get updated stock levels
refresh();
}
};
// Handle item edit
const handleItemEdit = (item: InventoryItem) => {
// TODO: Open edit modal
console.log('Edit item:', item);
};
// Handle item view details
const handleItemViewDetails = (item: InventoryItem) => {
// TODO: Open details modal or navigate to details page
console.log('View details:', item);
};
// Handle view item by ID (for alerts)
const handleViewItemById = (itemId: string) => {
const item = items.find(item => item.id === itemId);
if (item) {
handleItemViewDetails(item);
}
};
// Get quick stats
const getQuickStats = () => {
const totalItems = items.length;
const lowStockItems = 0;
const expiringItems = 0;
const totalValue = dashboardData?.total_value || 0;
return { totalItems, lowStockItems, expiringItems, totalValue };
};
const stats = getQuickStats();
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="py-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gestión de Inventario</h1>
<p className="text-gray-600 mt-1">
Administra tus productos, stock y alertas
</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => refresh()}
disabled={isLoading}
className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-5 h-5 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
<Plus className="w-4 h-4" />
<span>Nuevo Producto</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Main Content */}
<div className="lg:col-span-4">
{/* Quick Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<Package className="w-8 h-8 text-blue-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Total Productos</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalItems}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<TrendingDown className="w-8 h-8 text-yellow-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Stock Bajo</p>
<p className="text-2xl font-bold text-gray-900">{stats.lowStockItems}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<Calendar className="w-8 h-8 text-red-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Por Vencer</p>
<p className="text-2xl font-bold text-gray-900">{stats.expiringItems}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<BarChart3 className="w-8 h-8 text-green-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Valor Total</p>
<p className="text-2xl font-bold text-gray-900">
{stats.totalValue.toLocaleString()}
</p>
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white rounded-lg border mb-6 p-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
{/* Search */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Buscar productos..."
value={filters.search}
onChange={(e) => handleSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* View Controls */}
<div className="flex items-center space-x-2">
<button
onClick={() => setShowFilters(!showFilters)}
className={`px-3 py-2 rounded-lg transition-colors flex items-center space-x-1 ${
showFilters ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Filter className="w-4 h-4" />
<span>Filtros</span>
</button>
<div className="flex rounded-lg border">
<button
onClick={() => setViewMode('grid')}
className={`p-2 ${
viewMode === 'grid'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 ${
viewMode === 'list'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Advanced Filters */}
{showFilters && (
<div className="mt-4 pt-4 border-t">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
{/* Product Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de Producto
</label>
<select
value={filters.product_type || ''}
onChange={(e) => handleFilterChange('product_type', e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Todos</option>
<option value="ingredient">Ingredientes</option>
<option value="finished_product">Productos Finales</option>
</select>
</div>
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Estado
</label>
<select
value={filters.is_active?.toString() || ''}
onChange={(e) => handleFilterChange('is_active',
e.target.value === '' ? undefined : e.target.value === 'true'
)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Todos</option>
<option value="true">Activos</option>
<option value="false">Inactivos</option>
</select>
</div>
{/* Stock Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Stock
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={filters.low_stock_only || false}
onChange={(e) => handleFilterChange('low_stock_only', e.target.checked || undefined)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Stock bajo</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={filters.expiring_soon_only || false}
onChange={(e) => handleFilterChange('expiring_soon_only', e.target.checked || undefined)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Por vencer</span>
</label>
</div>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ordenar por
</label>
<select
value={filters.sort_by || 'name'}
onChange={(e) => handleFilterChange('sort_by', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="name">Nombre</option>
<option value="category">Categoría</option>
<option value="stock_level">Nivel de Stock</option>
<option value="created_at">Fecha de Creación</option>
</select>
</div>
{/* Sort Order */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Orden
</label>
<select
value={filters.sort_order || 'asc'}
onChange={(e) => handleFilterChange('sort_order', e.target.value as 'asc' | 'desc')}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="asc">Ascendente</option>
<option value="desc">Descendente</option>
</select>
</div>
</div>
<div className="mt-4 flex justify-end">
<button
onClick={clearFilters}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800"
>
Limpiar filtros
</button>
</div>
</div>
)}
</div>
{/* Items Grid/List */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Cargando inventario...</span>
</div>
) : error ? (
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<AlertTriangle className="w-8 h-8 text-red-600 mx-auto mb-3" />
<h3 className="text-lg font-medium text-red-900 mb-2">Error al cargar inventario</h3>
<p className="text-red-700 mb-4">{error}</p>
<button
onClick={refresh}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Reintentar
</button>
</div>
) : items.length === 0 ? (
<div className="bg-white rounded-lg border p-12 text-center">
<Package className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{filters.search || Object.values(filters).some(v => v)
? 'No se encontraron productos'
: 'No tienes productos en tu inventario'
}
</h3>
<p className="text-gray-600 mb-6">
{filters.search || Object.values(filters).some(v => v)
? 'Prueba ajustando los filtros de búsqueda'
: 'Comienza agregando tu primer producto al inventario'
}
</p>
<button className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Agregar Producto
</button>
</div>
) : (
<div>
<div className={
viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6'
: 'space-y-4'
}>
{items.map((item) => (
<InventoryItemCard
key={item.id}
item={item}
stockLevel={stockLevels[item.id]}
compact={viewMode === 'list'}
onEdit={handleItemEdit}
onViewDetails={handleItemViewDetails}
onStockAdjust={handleStockAdjust}
/>
))}
</div>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="mt-8 flex items-center justify-between">
<div className="text-sm text-gray-600">
Mostrando {((pagination.page - 1) * pagination.limit) + 1} a{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} de{' '}
{pagination.total} productos
</div>
<div className="flex space-x-2">
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => {
const searchParams: InventorySearchParams = {
...filters,
page,
limit: pagination.limit
};
loadItems(searchParams);
}}
className={`px-3 py-2 rounded-lg ${
page === pagination.page
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50 border'
}`}
>
{page}
</button>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default InventoryPage;

View File

@@ -1,547 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import {
TrendingUp,
TrendingDown,
Package,
Clock,
Users,
Star,
ChevronRight,
CheckCircle,
BarChart3,
Shield,
Smartphone,
Play,
ArrowRight,
MapPin,
Quote
} from 'lucide-react';
interface LandingPageProps {
// No props needed with React Router
}
const LandingPage: React.FC<LandingPageProps> = () => {
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
const [currentTestimonial, setCurrentTestimonial] = useState(0);
const features = [
{
icon: TrendingUp,
title: "Predicciones Precisas",
description: "IA que aprende de tu negocio único para predecir demanda con 87% de precisión",
color: "bg-success-100 text-success-600"
},
{
icon: TrendingDown,
title: "Reduce Desperdicios",
description: "Disminuye hasta un 25% el desperdicio diario optimizando tu producción",
color: "bg-primary-100 text-primary-600"
},
{
icon: Clock,
title: "Ahorra Tiempo",
description: "30-45 minutos menos al día en planificación manual de producción",
color: "bg-blue-100 text-blue-600"
},
{
icon: Package,
title: "Gestión Inteligente",
description: "Pedidos automáticos y alertas de stock basados en predicciones",
color: "bg-purple-100 text-purple-600"
}
];
const testimonials = [
{
name: "María González",
business: "Panadería San Miguel",
location: "Chamberí, Madrid",
text: "Con PanIA reduje mis desperdicios un 20% en el primer mes. La IA realmente entiende mi negocio.",
rating: 5,
savings: "€280/mes"
},
{
name: "Carlos Ruiz",
business: "Obrador Central Goya",
location: "Salamanca, Madrid",
text: "Gestiono 4 puntos de venta y PanIA me ahorra 2 horas diarias de planificación. Imprescindible.",
rating: 5,
savings: "€450/mes"
},
{
name: "Ana Martín",
business: "Café & Pan Malasaña",
location: "Malasaña, Madrid",
text: "Las predicciones son increíblemente precisas. Ya no me quedo sin croissants en el desayuno.",
rating: 5,
savings: "€190/mes"
}
];
const stats = [
{ number: "500+", label: "Panaderías en Madrid" },
{ number: "87%", label: "Precisión en predicciones" },
{ number: "25%", label: "Reducción desperdicios" },
{ number: "€350", label: "Ahorro mensual promedio" }
];
const madridDistricts = [
"Centro", "Salamanca", "Chamberí", "Retiro", "Arganzuela",
"Moncloa", "Chamartín", "Hortaleza", "Fuencarral", "Tetuán"
];
useEffect(() => {
const interval = setInterval(() => {
setCurrentTestimonial((prev) => (prev + 1) % testimonials.length);
}, 5000);
return () => clearInterval(interval);
}, [testimonials.length]);
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<div className="flex items-center">
<div className="h-10 w-10 bg-primary-500 rounded-xl flex items-center justify-center mr-3">
<span className="text-white text-xl font-bold">🥖</span>
</div>
<span className="text-2xl font-bold text-gray-900">PanIA</span>
</div>
<nav className="hidden md:flex space-x-8">
<a href="#features" className="text-gray-600 hover:text-primary-600 transition-colors">Características</a>
<a href="#testimonials" className="text-gray-600 hover:text-primary-600 transition-colors">Testimonios</a>
<a href="#pricing" className="text-gray-600 hover:text-primary-600 transition-colors">Precios</a>
<a href="#contact" className="text-gray-600 hover:text-primary-600 transition-colors">Contacto</a>
</nav>
<div className="flex items-center space-x-4">
<Link
to="/login"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Iniciar sesión
</Link>
<Link
to="/register"
className="bg-primary-500 text-white px-6 py-2 rounded-lg hover:bg-primary-600 transition-all hover:shadow-lg"
>
Prueba gratis
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="bg-gradient-to-br from-primary-50 to-orange-100 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<div className="inline-flex items-center bg-primary-100 text-primary-800 px-4 py-2 rounded-full text-sm font-medium mb-6">
<Star className="h-4 w-4 mr-2" />
IA líder para panaderías en Madrid
</div>
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
La primera IA para
<span className="text-primary-600 block">tu panadería</span>
</h1>
<p className="text-xl text-gray-600 mb-8 leading-relaxed">
Transforma tus datos de ventas en predicciones precisas.
Reduce desperdicios, maximiza ganancias y optimiza tu producción
con inteligencia artificial diseñada para panaderías madrileñas.
</p>
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<Link
to="/register"
className="bg-primary-500 text-white px-8 py-4 rounded-xl font-semibold text-lg hover:bg-primary-600 transition-all hover:shadow-lg transform hover:-translate-y-1 flex items-center justify-center"
>
Comenzar gratis
<ArrowRight className="h-5 w-5 ml-2" />
</Link>
<button
onClick={() => setIsVideoModalOpen(true)}
className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-primary-500 hover:text-primary-600 transition-all flex items-center justify-center"
>
<Play className="h-5 w-5 mr-2" />
Ver demo
</button>
</div>
<div className="flex items-center text-sm text-gray-500">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
<span>30 días gratis Sin tarjeta de crédito Configuración en 5 minutos</span>
</div>
</div>
<div className="relative">
<div className="bg-white rounded-2xl shadow-2xl p-8 border">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">Predicciones para Hoy</h3>
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium">
87% precisión
</span>
</div>
<div className="space-y-4">
{[
{ product: "Croissants", predicted: 48, confidence: "high", change: 8 },
{ product: "Pan de molde", predicted: 35, confidence: "high", change: 3 },
{ product: "Café", predicted: 72, confidence: "medium", change: -5 }
].map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<div className="font-medium text-gray-900">{item.product}</div>
<div className="text-sm text-gray-500 flex items-center">
{item.change > 0 ? (
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
) : (
<TrendingDown className="h-3 w-3 text-red-500 mr-1" />
)}
{Math.abs(item.change)} vs ayer
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-gray-900">{item.predicted}</div>
<div className={`text-xs px-2 py-1 rounded ${
item.confidence === 'high'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{item.confidence === 'high' ? 'Alta confianza' : 'Media confianza'}
</div>
</div>
</div>
))}
</div>
</div>
{/* Floating stats */}
<div className="absolute -top-6 -right-6 bg-white rounded-xl shadow-lg p-4 border">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">-25%</div>
<div className="text-sm text-gray-600">Desperdicios</div>
</div>
</div>
<div className="absolute -bottom-6 -left-6 bg-white rounded-xl shadow-lg p-4 border">
<div className="text-center">
<div className="text-2xl font-bold text-primary-600">350</div>
<div className="text-sm text-gray-600">Ahorro/mes</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Resultados que hablan por solos
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Más de 500 panaderías en Madrid ya confían en PanIA para optimizar su producción
</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{stats.map((stat, index) => (
<div key={index} className="text-center">
<div className="text-4xl lg:text-5xl font-bold text-primary-600 mb-2">
{stat.number}
</div>
<div className="text-gray-600 font-medium">{stat.label}</div>
</div>
))}
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
IA diseñada para panaderías madrileñas
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Cada característica está pensada para resolver los desafíos específicos
de las panaderías en Madrid: desde el clima hasta los patrones de consumo locales
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<div key={index} className="bg-white p-8 rounded-2xl shadow-soft hover:shadow-medium transition-all">
<div className={`w-12 h-12 ${feature.color} rounded-xl flex items-center justify-center mb-6`}>
<Icon className="h-6 w-6" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">{feature.title}</h3>
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
</div>
);
})}
</div>
{/* Madrid-specific features */}
<div className="bg-white rounded-2xl p-8 shadow-soft">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">
Especializado en Madrid
</h3>
<p className="text-gray-600 mb-6">
PanIA conoce Madrid como ninguna otra IA. Integra datos del clima,
tráfico, eventos y patrones de consumo específicos de la capital.
</p>
<div className="space-y-3">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<span>Integración con datos meteorológicos de AEMET</span>
</div>
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<span>Análisis de eventos y festividades locales</span>
</div>
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<span>Patrones de tráfico peatonal por distrito</span>
</div>
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<span>Horarios de siesta y patrones españoles</span>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-primary-50 p-6 rounded-xl">
<div className="flex items-center mb-4">
<MapPin className="h-5 w-5 text-primary-600 mr-2" />
<h4 className="font-semibold text-gray-900">Distritos cubiertos</h4>
</div>
<div className="grid grid-cols-2 gap-2">
{madridDistricts.map((district, index) => (
<div key={index} className="text-sm text-gray-700 bg-white px-3 py-1 rounded-lg">
{district}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</section>
{/* Testimonials Section */}
<section id="testimonials" className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Lo que dicen nuestros clientes
</h2>
<p className="text-xl text-gray-600">
Panaderías reales, resultados reales en Madrid
</p>
</div>
<div className="relative">
<div className="bg-gradient-to-br from-primary-50 to-orange-50 rounded-2xl p-8 lg:p-12">
<div className="max-w-4xl mx-auto">
<div className="text-center">
<Quote className="h-12 w-12 text-primary-400 mx-auto mb-6" />
<blockquote className="text-2xl lg:text-3xl font-medium text-gray-900 mb-8 leading-relaxed">
"{testimonials[currentTestimonial].text}"
</blockquote>
<div className="flex items-center justify-center mb-6">
{[...Array(testimonials[currentTestimonial].rating)].map((_, i) => (
<Star key={i} className="h-5 w-5 text-yellow-400 fill-current" />
))}
</div>
<div className="text-center">
<div className="font-semibold text-gray-900 text-lg">
{testimonials[currentTestimonial].name}
</div>
<div className="text-primary-600 font-medium">
{testimonials[currentTestimonial].business}
</div>
<div className="text-gray-500 text-sm">
{testimonials[currentTestimonial].location}
</div>
<div className="inline-flex items-center bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium mt-2">
Ahorro: {testimonials[currentTestimonial].savings}
</div>
</div>
</div>
</div>
</div>
{/* Testimonial indicators */}
<div className="flex justify-center mt-8 space-x-2">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => setCurrentTestimonial(index)}
className={`w-3 h-3 rounded-full transition-all ${
index === currentTestimonial ? 'bg-primary-500' : 'bg-gray-300'
}`}
/>
))}
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20 bg-gradient-to-br from-primary-600 to-orange-600">
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8">
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-6">
¿Listo para transformar tu panadería?
</h2>
<p className="text-xl text-primary-100 mb-8 leading-relaxed">
Únete a más de 500 panaderías en Madrid que ya reducen desperdicios
y maximizan ganancias con PanIA
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-8">
<Link
to="/register"
className="bg-white text-primary-600 px-8 py-4 rounded-xl font-semibold text-lg hover:bg-gray-50 transition-all hover:shadow-lg transform hover:-translate-y-1"
>
Comenzar prueba gratuita
</Link>
<Link
to="/login"
className="border-2 border-white text-white px-8 py-4 rounded-xl font-semibold text-lg hover:bg-white hover:text-primary-600 transition-all"
>
Ya tengo cuenta
</Link>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-8 text-primary-100">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 mr-2" />
<span>30 días gratis</span>
</div>
<div className="flex items-center">
<CheckCircle className="h-5 w-5 mr-2" />
<span>Sin tarjeta de crédito</span>
</div>
<div className="flex items-center">
<CheckCircle className="h-5 w-5 mr-2" />
<span>Soporte en español</span>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div className="flex items-center mb-4">
<div className="h-8 w-8 bg-primary-500 rounded-lg flex items-center justify-center mr-3">
<span className="text-white text-lg font-bold">🥖</span>
</div>
<span className="text-xl font-bold">PanIA</span>
</div>
<p className="text-gray-400 mb-4">
Inteligencia Artificial para panaderías en Madrid
</p>
<div className="text-gray-400 text-sm">
<p>📍 Madrid, España</p>
<p>📧 hola@pania.es</p>
<p>📞 +34 900 123 456</p>
</div>
</div>
<div>
<h3 className="font-semibold mb-4">Producto</h3>
<ul className="space-y-2 text-gray-400">
<li><a href="#features" className="hover:text-white transition-colors">Características</a></li>
<li><a href="#pricing" className="hover:text-white transition-colors">Precios</a></li>
<li><a href="#" className="hover:text-white transition-colors">Demo</a></li>
<li><a href="#" className="hover:text-white transition-colors">API</a></li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4">Soporte</h3>
<ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white transition-colors">Centro de ayuda</a></li>
<li><a href="#" className="hover:text-white transition-colors">Documentación</a></li>
<li><a href="#contact" className="hover:text-white transition-colors">Contacto</a></li>
<li><a href="#" className="hover:text-white transition-colors">Estado del sistema</a></li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4">Legal</h3>
<ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white transition-colors">Privacidad</a></li>
<li><a href="#" className="hover:text-white transition-colors">Términos</a></li>
<li><a href="#" className="hover:text-white transition-colors">Cookies</a></li>
<li><a href="#" className="hover:text-white transition-colors">GDPR</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-12 pt-8 text-center text-gray-400">
<p>&copy; 2024 PanIA. Todos los derechos reservados. Hecho con en Madrid.</p>
</div>
</div>
</footer>
{/* Video Modal */}
{isVideoModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl p-6 max-w-4xl w-full max-h-[90vh] overflow-auto">
<div className="flex justify-between items-center mb-6">
<h3 className="text-2xl font-bold text-gray-900">Demo de PanIA</h3>
<button
onClick={() => setIsVideoModalOpen(false)}
className="text-gray-500 hover:text-gray-700 text-2xl"
>
×
</button>
</div>
<div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center">
<div className="text-center">
<Play className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">Video demo disponible próximamente</p>
<p className="text-sm text-gray-500 mt-2">
Mientras tanto, puedes comenzar tu prueba gratuita
</p>
<Link
to="/register"
onClick={() => setIsVideoModalOpen(false)}
className="mt-4 bg-primary-500 text-white px-6 py-2 rounded-lg hover:bg-primary-600 transition-colors inline-block"
>
Comenzar prueba gratis
</Link>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default LandingPage;

View File

@@ -1,168 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, TrendingUp, Clock, DollarSign, BarChart3 } from 'lucide-react';
const LandingPage: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-orange-100">
{/* Header */}
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6">
<div className="flex items-center">
<div className="h-8 w-8 bg-primary-500 rounded-lg flex items-center justify-center mr-3">
<span className="text-white text-sm font-bold">🥖</span>
</div>
<span className="text-xl font-bold text-gray-900">PanIA</span>
</div>
<div className="flex items-center space-x-4">
<Link
to="/login"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Iniciar sesión
</Link>
<Link
to="/register"
className="bg-primary-500 text-white px-6 py-2 rounded-lg hover:bg-primary-600 transition-all hover:shadow-lg"
>
Prueba gratis
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<div className="inline-flex items-center bg-primary-100 text-primary-800 px-4 py-2 rounded-full text-sm font-medium mb-6">
IA líder para panaderías en Madrid
</div>
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
La primera IA para
<span className="text-primary-600 block">tu panadería</span>
</h1>
<p className="text-xl text-gray-600 mb-8 leading-relaxed max-w-3xl mx-auto">
Transforma tus datos de ventas en predicciones precisas.
Reduce desperdicios, maximiza ganancias y optimiza tu producción
con inteligencia artificial diseñada para panaderías madrileñas.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-16">
<Link
to="/register"
className="w-full sm:w-auto bg-primary-500 text-white px-8 py-4 rounded-xl font-semibold text-lg hover:bg-primary-600 transition-all hover:shadow-xl hover:-translate-y-1 flex items-center justify-center group"
>
Empezar Gratis
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Link>
<Link
to="/login"
className="w-full sm:w-auto border-2 border-primary-500 text-primary-500 px-8 py-4 rounded-xl font-semibold text-lg hover:bg-primary-50 transition-all"
>
Iniciar Sesión
</Link>
</div>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Todo lo que necesitas para optimizar tu panadería
</h2>
<p className="text-xl text-gray-600">
Tecnología de vanguardia diseñada específicamente para panaderías
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{
icon: TrendingUp,
title: "Predicciones Precisas",
description: "IA que aprende de tu negocio único para predecir demanda con 87% de precisión",
color: "bg-green-100 text-green-600"
},
{
icon: Clock,
title: "Reduce Desperdicios",
description: "Disminuye hasta un 25% el desperdicio diario optimizando tu producción",
color: "bg-blue-100 text-blue-600"
},
{
icon: DollarSign,
title: "Ahorra Dinero",
description: "Ahorra hasta €500/mes reduciendo costos operativos y desperdicios",
color: "bg-purple-100 text-purple-600"
},
{
icon: BarChart3,
title: "Analytics Avanzados",
description: "Reportes detallados y insights que te ayudan a tomar mejores decisiones",
color: "bg-orange-100 text-orange-600"
}
].map((feature, index) => (
<div key={index} className="text-center">
<div className={`inline-flex h-16 w-16 items-center justify-center rounded-xl ${feature.color} mb-6`}>
<feature.icon className="h-8 w-8" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">{feature.title}</h3>
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-primary-500 py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-white mb-4">
¿Listo para revolucionar tu panadería?
</h2>
<p className="text-xl text-primary-100 mb-8 max-w-2xl mx-auto">
Únete a más de 500 panaderías en Madrid que ya confían en PanIA para optimizar su negocio
</p>
<Link
to="/register"
className="inline-flex items-center bg-white text-primary-600 px-8 py-4 rounded-xl font-semibold text-lg hover:bg-gray-50 transition-all hover:shadow-xl hover:-translate-y-1"
>
Empezar Prueba Gratuita
<ArrowRight className="ml-2 h-5 w-5" />
</Link>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<div className="flex items-center justify-center mb-4">
<div className="h-8 w-8 bg-primary-500 rounded-lg flex items-center justify-center mr-3">
<span className="text-white text-sm font-bold">🥖</span>
</div>
<span className="text-xl font-bold">PanIA</span>
</div>
<p className="text-gray-400 mb-4">
Inteligencia Artificial para panaderías madrileñas
</p>
<p className="text-sm text-gray-500">
© 2024 PanIA. Todos los derechos reservados.
</p>
</div>
</div>
</footer>
</div>
);
};
export default LandingPage;

File diff suppressed because it is too large Load Diff

View File

@@ -1,620 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock, BarChart3, TrendingUp, Euro, Settings } from 'lucide-react';
// Import complex components
import WhatIfPlanner from '../../components/ui/WhatIfPlanner';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
interface Order {
id: string;
supplier: string;
items: OrderItem[];
orderDate: string;
deliveryDate: string;
status: 'pending' | 'confirmed' | 'delivered' | 'cancelled';
total: number;
type: 'ingredients' | 'consumables';
}
interface OrderItem {
name: string;
quantity: number;
unit: string;
price: number;
suggested?: boolean;
}
interface OrdersPageProps {
view?: string;
}
const OrdersPage: React.FC<OrdersPageProps> = ({ view = 'incoming' }) => {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showNewOrder, setShowNewOrder] = useState(false);
const [activeTab, setActiveTab] = useState<'orders' | 'analytics' | 'forecasting' | 'suppliers'>('orders');
// Sample orders data
const sampleOrders: Order[] = [
{
id: '1',
supplier: 'Harinas Castellana',
items: [
{ name: 'Harina de trigo', quantity: 50, unit: 'kg', price: 0.85, suggested: true },
{ name: 'Levadura fresca', quantity: 2, unit: 'kg', price: 3.20 },
{ name: 'Sal marina', quantity: 5, unit: 'kg', price: 1.10 }
],
orderDate: '2024-11-03',
deliveryDate: '2024-11-05',
status: 'pending',
total: 52.50,
type: 'ingredients'
},
{
id: '2',
supplier: 'Distribuciones Madrid',
items: [
{ name: 'Vasos de café 250ml', quantity: 1000, unit: 'unidades', price: 0.08 },
{ name: 'Bolsas papel kraft', quantity: 500, unit: 'unidades', price: 0.12, suggested: true },
{ name: 'Servilletas', quantity: 20, unit: 'paquetes', price: 2.50 }
],
orderDate: '2024-11-02',
deliveryDate: '2024-11-04',
status: 'confirmed',
total: 190.00,
type: 'consumables'
},
{
id: '3',
supplier: 'Lácteos Frescos SA',
items: [
{ name: 'Leche entera', quantity: 20, unit: 'litros', price: 0.95 },
{ name: 'Mantequilla', quantity: 5, unit: 'kg', price: 4.20 },
{ name: 'Nata para montar', quantity: 3, unit: 'litros', price: 2.80 }
],
orderDate: '2024-11-01',
deliveryDate: '2024-11-03',
status: 'delivered',
total: 47.40,
type: 'ingredients'
}
];
useEffect(() => {
const loadOrders = async () => {
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 800));
setOrders(sampleOrders);
} catch (error) {
console.error('Error loading orders:', error);
} finally {
setIsLoading(false);
}
};
loadOrders();
}, []);
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'bg-warning-100 text-warning-800 border-warning-200';
case 'confirmed':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'delivered':
return 'bg-success-100 text-success-800 border-success-200';
case 'cancelled':
return 'bg-red-100 text-red-800 border-red-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'pending':
return 'Pendiente';
case 'confirmed':
return 'Confirmado';
case 'delivered':
return 'Entregado';
case 'cancelled':
return 'Cancelado';
default:
return status;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
return <Clock className="h-4 w-4" />;
case 'confirmed':
return <AlertCircle className="h-4 w-4" />;
case 'delivered':
return <CheckCircle className="h-4 w-4" />;
case 'cancelled':
return <AlertCircle className="h-4 w-4" />;
default:
return <Clock className="h-4 w-4" />;
}
};
// Sample data for complex components
const orderDemandHeatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Harina de trigo', demand: 50, confidence: 'high' as const },
{ name: 'Levadura fresca', demand: 2, confidence: 'high' as const },
{ name: 'Mantequilla', demand: 5, confidence: 'medium' as const },
{ name: 'Vasos café', demand: 1000, confidence: 'medium' as const },
]
},
{ date: '2024-11-05', demand: 165, isForecast: true },
{ date: '2024-11-06', demand: 195, isForecast: true },
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const baselineSupplyData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Harina de trigo', demand: 50, price: 0.85 },
{ name: 'Levadura fresca', demand: 2, price: 3.20 },
{ name: 'Mantequilla', demand: 5, price: 4.20 },
{ name: 'Leche entera', demand: 20, price: 0.95 },
{ name: 'Vasos café', demand: 1000, price: 0.08 },
]
};
const filteredOrders = orders.filter(order => {
if (activeTab === 'orders') return true;
return false;
});
const handleDeleteOrder = (orderId: string) => {
setOrders(prev => prev.filter(order => order.id !== orderId));
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-48 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gestión de Pedidos</h1>
<p className="text-gray-600 mt-1">
Administra tus pedidos de ingredientes y consumibles
</p>
</div>
<button
onClick={() => setShowNewOrder(true)}
className="mt-4 sm:mt-0 inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg"
>
<Plus className="h-5 w-5 mr-2" />
Nuevo Pedido
</button>
</div>
{/* Enhanced Tabs */}
<div className="bg-white rounded-xl shadow-soft p-1">
<div className="flex space-x-1">
{[
{ id: 'orders', label: 'Gestión de Pedidos', icon: Package, count: orders.length },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'forecasting', label: 'Simulaciones', icon: TrendingUp },
{ id: 'suppliers', label: 'Proveedores', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
{tab.count && (
<span className="ml-2 px-2 py-1 bg-gray-200 text-gray-700 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
</div>
{/* AI Suggestions */}
<div className="bg-gradient-to-r from-primary-50 to-orange-50 border border-primary-200 rounded-xl p-6">
<div className="flex items-start">
<div className="p-2 bg-primary-100 rounded-lg mr-4">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-primary-900 mb-2">Sugerencias Inteligentes de Pedidos</h3>
<div className="space-y-2 text-sm text-primary-800">
<p> <strong>Harina de trigo:</strong> Stock bajo detectado. Recomendamos pedir 50kg para cubrir 2 semanas.</p>
<p> <strong>Bolsas de papel:</strong> Aumento del 15% en takeaway. Considera aumentar el pedido habitual.</p>
<p> <strong>Café en grano:</strong> Predicción de alta demanda por temperaturas bajas. +20% recomendado.</p>
</div>
<button className="mt-3 text-primary-700 hover:text-primary-600 font-medium text-sm">
Ver todas las sugerencias
</button>
</div>
</div>
</div>
{/* Tab Content */}
{activeTab === 'orders' && (
<>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredOrders.map((order) => (
<div key={order.id} className="bg-white rounded-xl shadow-soft hover:shadow-medium transition-shadow">
{/* Order Header */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-900">{order.supplier}</h3>
<span className={`px-2 py-1 rounded-lg text-xs font-medium border flex items-center ${getStatusColor(order.status)}`}>
{getStatusIcon(order.status)}
<span className="ml-1">{getStatusLabel(order.status)}</span>
</span>
</div>
<div className="flex items-center text-sm text-gray-600 mb-2">
<Calendar className="h-4 w-4 mr-1" />
<span>Entrega: {new Date(order.deliveryDate).toLocaleDateString('es-ES')}</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs px-2 py-1 rounded ${
order.type === 'ingredients'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{order.type === 'ingredients' ? 'Ingredientes' : 'Consumibles'}
</span>
<span className="text-lg font-bold text-gray-900">{order.total.toFixed(2)}</span>
</div>
</div>
{/* Order Items */}
<div className="p-6">
<h4 className="text-sm font-medium text-gray-700 mb-3">Artículos ({order.items.length})</h4>
<div className="space-y-2 max-h-32 overflow-y-auto">
{order.items.map((item, index) => (
<div key={index} className="flex justify-between text-sm">
<div className="flex-1">
<span className="text-gray-900">{item.name}</span>
{item.suggested && (
<span className="ml-2 text-xs bg-primary-100 text-primary-700 px-1 py-0.5 rounded">
IA
</span>
)}
<div className="text-gray-500 text-xs">
{item.quantity} {item.unit} × {item.price.toFixed(2)}
</div>
</div>
<span className="text-gray-900 font-medium">
{(item.quantity * item.price).toFixed(2)}
</span>
</div>
))}
</div>
</div>
{/* Order Actions */}
<div className="px-6 pb-6">
<div className="flex space-x-2">
<button className="flex-1 py-2 px-3 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center justify-center">
<Edit className="h-4 w-4 mr-1" />
Editar
</button>
<button
onClick={() => handleDeleteOrder(order.id)}
className="py-2 px-3 text-sm bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<Package className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay pedidos</h3>
<p className="text-gray-600 mb-4">
{activeTab === 'orders'
? 'Aún no has creado ningún pedido'
: 'No hay datos disponibles para esta sección'
}
</p>
<button
onClick={() => setShowNewOrder(true)}
className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors"
>
<Plus className="h-5 w-5 mr-2" />
Crear primer pedido
</button>
</div>
)}
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-gray-900">{orders.length}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-warning-100 rounded-lg">
<Clock className="h-6 w-6 text-warning-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Pendientes</p>
<p className="text-2xl font-bold text-gray-900">
{orders.filter(o => o.status === 'pending' || o.status === 'confirmed').length}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-success-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Gasto Mensual</p>
<p className="text-2xl font-bold text-gray-900">
{orders.reduce((sum, order) => sum + order.total, 0).toFixed(0)}
</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Acciones Rápidas
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Pedido Automático</div>
<div className="text-sm text-gray-500 mt-1">Basado en predicciones IA</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Gestión de Proveedores</div>
<div className="text-sm text-gray-500 mt-1">Añadir o editar proveedores</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Historial de Gastos</div>
<div className="text-sm text-gray-500 mt-1">Ver análisis de costos</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Configurar Alertas</div>
<div className="text-sm text-gray-500 mt-1">Stock bajo y vencimientos</div>
</button>
</div>
</div>
</>
)}
{/* Analytics Tab */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={orderDemandHeatmapData}
selectedProduct="Ingredientes"
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Cost Analysis Chart */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Euro className="h-5 w-5 mr-2 text-primary-600" />
Análisis de Costos
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800 font-semibold">Ahorro Mensual</div>
<div className="text-2xl font-bold text-green-900">124.50</div>
<div className="text-sm text-green-700">vs mes anterior</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-blue-800 font-semibold">Gasto Promedio</div>
<div className="text-2xl font-bold text-blue-900">289.95</div>
<div className="text-sm text-blue-700">por pedido</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-purple-800 font-semibold">Eficiencia</div>
<div className="text-2xl font-bold text-purple-900">94.2%</div>
<div className="text-sm text-purple-700">predicción IA</div>
</div>
</div>
<div className="mt-6 h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de costos</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{/* Forecasting/Simulations Tab */}
{activeTab === 'forecasting' && (
<div className="space-y-6">
<WhatIfPlanner
baselineData={baselineSupplyData}
onScenarioRun={(scenario, result) => {
console.log('Scenario run:', scenario, result);
}}
/>
</div>
)}
{/* Suppliers Tab */}
{activeTab === 'suppliers' && (
<div className="space-y-6">
{/* Suppliers Management */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Settings className="h-5 w-5 mr-2 text-primary-600" />
Gestión de Proveedores
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
name: 'Harinas Castellana',
category: 'Ingredientes',
rating: 4.8,
reliability: 98,
nextDelivery: '2024-11-05',
status: 'active'
},
{
name: 'Distribuciones Madrid',
category: 'Consumibles',
rating: 4.5,
reliability: 95,
nextDelivery: '2024-11-04',
status: 'active'
},
{
name: 'Lácteos Frescos SA',
category: 'Ingredientes',
rating: 4.9,
reliability: 99,
nextDelivery: '2024-11-03',
status: 'active'
}
].map((supplier, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{supplier.name}</h4>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
</span>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Categoría:</span> {supplier.category}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Calificación:</span> {supplier.rating}/5
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Confiabilidad:</span> {supplier.reliability}%
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Próxima entrega:</span> {new Date(supplier.nextDelivery).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-4 flex space-x-2">
<button className="flex-1 px-3 py-2 text-sm bg-primary-100 text-primary-700 rounded-lg hover:bg-primary-200 transition-colors">
Editar
</button>
<button className="px-3 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
Contactar
</button>
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors">
<Plus className="h-4 w-4 mr-2" />
Añadir Proveedor
</button>
</div>
</div>
</div>
)}
{/* New Order Modal Placeholder */}
{showNewOrder && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nuevo Pedido</h3>
<p className="text-gray-600 mb-6">
Esta funcionalidad estará disponible próximamente. PanIA analizará tus necesidades
y creará pedidos automáticos basados en las predicciones de demanda.
</p>
<div className="flex space-x-3">
<button
onClick={() => setShowNewOrder(false)}
className="flex-1 py-2 px-4 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors"
>
Cerrar
</button>
<button
onClick={() => setShowNewOrder(false)}
className="flex-1 py-2 px-4 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors"
>
Entendido
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default OrdersPage;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import { POSManagementPage } from '../../components/pos';
interface POSPageProps {
view?: 'integrations' | 'sync-status' | 'transactions';
}
const POSPage: React.FC<POSPageProps> = ({ view = 'integrations' }) => {
// For now, all views route to the main POS management page
// In the future, you can create separate components for different views
switch (view) {
case 'integrations':
return (
<div className="p-6">
<POSManagementPage />
</div>
);
case 'sync-status':
return (
<div className="p-6">
{/* Future: Create dedicated sync status view */}
<POSManagementPage />
</div>
);
case 'transactions':
return (
<div className="p-6">
{/* Future: Create dedicated transactions view */}
<POSManagementPage />
</div>
);
default:
return (
<div className="p-6">
<POSManagementPage />
</div>
);
}
};
export default POSPage;

View File

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

View File

@@ -0,0 +1,710 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui';
import { PublicLayout } from '../../components/layout';
import {
BarChart3,
TrendingUp,
Shield,
Zap,
Users,
Award,
ChevronRight,
Check,
Star,
ArrowRight,
Play,
Calendar,
Clock,
DollarSign,
Package,
PieChart,
Settings
} from 'lucide-react';
const LandingPage: React.FC = () => {
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<PublicLayout
variant="full-width"
contentPadding="none"
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
variant: "default",
navigationItems: [
{ id: 'features', label: 'Características', href: '#features' },
{ id: 'benefits', label: 'Beneficios', href: '#benefits' },
{ id: 'pricing', label: 'Precios', href: '#pricing' },
{ id: 'testimonials', label: 'Testimonios', href: '#testimonials' }
]
}}
>
{/* Hero Section */}
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<div className="mb-6">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
<Zap className="w-4 h-4 mr-2" />
IA Avanzada para Panaderías
</span>
</div>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
<span className="block">Revoluciona tu</span>
<span className="block text-[var(--color-primary)]">Panadería con IA</span>
</h1>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
Optimiza automáticamente tu producción, reduce desperdicios hasta un 35%,
predice demanda con precisión del 92% y aumenta tus ventas con inteligencia artificial.
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Link to="/register">
<Button size="lg" className="px-8 py-4 text-lg font-semibold bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200">
Comenzar Gratis 14 Días
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</Link>
<Button
variant="outline"
size="lg"
className="px-8 py-4 text-lg font-semibold border-2 border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white transition-all duration-200"
onClick={() => scrollToSection('demo')}
>
<Play className="mr-2 w-5 h-5" />
Ver Demo en Vivo
</Button>
</div>
<div className="mt-12 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Sin tarjeta de crédito
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Configuración en 5 minutos
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Soporte 24/7 en español
</div>
</div>
</div>
</div>
{/* Background decoration */}
<div className="absolute top-0 left-0 right-0 h-full overflow-hidden -z-10">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-[var(--color-primary)]/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-[var(--color-secondary)]/5 rounded-full blur-3xl"></div>
</div>
</section>
{/* Stats Section */}
<section className="py-16 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">500+</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Panaderías Activas</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">35%</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Reducción de Desperdicios</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">92%</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Precisión de Predicciones</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">4.8</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Satisfacción de Clientes</div>
</div>
</div>
</div>
</section>
{/* Main Features Section */}
<section id="features" className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)]">
Gestión Completa con
<span className="block text-[var(--color-primary)]">Inteligencia Artificial</span>
</h2>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)]">
Automatiza procesos, optimiza recursos y toma decisiones inteligentes basadas en datos reales de tu panadería.
</p>
</div>
<div className="mt-20 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* AI Forecasting */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-xl flex items-center justify-center shadow-lg">
<TrendingUp className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Predicción Inteligente</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Algoritmos de IA analizan patrones históricos, clima, eventos locales y tendencias para predecir la demanda exacta de cada producto.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-primary)]">
<Check className="w-4 h-4 mr-2" />
Precisión del 92% en predicciones
</div>
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Reduce desperdicios hasta 35%
</div>
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Aumenta ventas promedio 22%
</div>
</div>
</div>
</div>
{/* Smart Inventory */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] rounded-xl flex items-center justify-center shadow-lg">
<Package className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Inventario Inteligente</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-secondary)]">
<Check className="w-4 h-4 mr-2" />
Alertas automáticas de stock bajo
</div>
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Órdenes de compra automatizadas
</div>
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Optimización de costos de materias primas
</div>
</div>
</div>
</div>
{/* Production Planning */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-dark)] rounded-xl flex items-center justify-center shadow-lg">
<Calendar className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Planificación de Producción</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-accent)]">
<Check className="w-4 h-4 mr-2" />
Programación automática de horneado
</div>
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
<Check className="w-4 h-4 mr-2" />
Optimización de uso de hornos
</div>
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
<Check className="w-4 h-4 mr-2" />
Gestión de personal y turnos
</div>
</div>
</div>
</div>
</div>
{/* Additional Features Grid */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Analytics Avanzado</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Dashboards en tiempo real con métricas clave</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<DollarSign className="w-6 h-6 text-[var(--color-secondary)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">POS Integrado</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Sistema de ventas completo y fácil de usar</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-accent)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<Shield className="w-6 h-6 text-[var(--color-accent)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Control de Calidad</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Trazabilidad completa y gestión HACCP</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-info)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<Settings className="w-6 h-6 text-[var(--color-info)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Automatización</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Procesos automáticos que ahorran tiempo</p>
</div>
</div>
</div>
</section>
{/* Benefits Section */}
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-2 lg:gap-16 items-center">
<div>
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Resultados Comprobados
<span className="block text-[var(--color-primary)]">en Cientos de Panaderías</span>
</h2>
<p className="mt-6 text-lg text-[var(--text-secondary)]">
Nuestros clientes han logrado transformaciones significativas en sus operaciones,
mejorando rentabilidad y reduciendo desperdicios desde el primer mes.
</p>
<div className="mt-10 space-y-8">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-[var(--color-success)]" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Aumenta Ventas 22% Promedio</h4>
<p className="text-[var(--text-secondary)]">Optimización de producción y mejor disponibilidad de productos populares</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Shield className="w-5 h-5 text-[var(--color-info)]" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Reduce Desperdicios 35%</h4>
<p className="text-[var(--text-secondary)]">Predicciones precisas evitan sobreproducción y productos vencidos</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="w-5 h-5 text-purple-600" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Ahorra 8 Horas Semanales</h4>
<p className="text-[var(--text-secondary)]">Automatización de tareas administrativas y de planificación</p>
</div>
</div>
</div>
</div>
<div className="mt-12 lg:mt-0">
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-2xl p-8">
<div className="grid grid-cols-2 gap-8">
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">127k</div>
<div className="text-sm text-[var(--text-secondary)]">Ahorro promedio anual por panadería</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-secondary)]">98%</div>
<div className="text-sm text-[var(--text-secondary)]">Satisfacción de clientes</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-accent)]">2.3x</div>
<div className="text-sm text-[var(--text-secondary)]">ROI promedio en 12 meses</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-info)]">24/7</div>
<div className="text-sm text-[var(--text-secondary)]">Soporte técnico especializado</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Testimonials Section */}
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Lo que Dicen Nuestros Clientes
</h2>
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
Panaderías de toda España han transformado sus negocios con nuestra plataforma
</p>
</div>
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Testimonial 1 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"Desde que implementamos Panadería IA, nuestros desperdicios se redujeron un 40% y las ventas aumentaron un 28%.
La predicción de demanda es increíblemente precisa."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold">
M
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">María González</div>
<div className="text-sm text-[var(--text-secondary)]">Panadería Santa María, Madrid</div>
</div>
</div>
</div>
{/* Testimonial 2 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"El sistema nos ahorra 10 horas semanales en planificación. Ahora puedo enfocarme en mejorar nuestros productos
mientras la IA maneja la logística."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-secondary)] rounded-full flex items-center justify-center text-white font-bold">
C
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">Carlos Ruiz</div>
<div className="text-sm text-[var(--text-secondary)]">Horno de Oro, Valencia</div>
</div>
</div>
</div>
{/* Testimonial 3 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"Increíble cómo predice exactamente cuántos panes necesitamos cada día. Nuestros clientes siempre encuentran
sus productos favoritos disponibles."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-accent)] rounded-full flex items-center justify-center text-white font-bold">
A
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">Ana Martínez</div>
<div className="text-sm text-[var(--text-secondary)]">Pan & Tradición, Sevilla</div>
</div>
</div>
</div>
</div>
{/* Trust indicators */}
<div className="mt-16 text-center">
<p className="text-sm text-[var(--text-tertiary)] mb-8">Confiado por más de 500 panaderías en España</p>
<div className="flex items-center justify-center space-x-8 opacity-60">
<div className="font-semibold text-[var(--text-secondary)]">Panadería Real</div>
<div className="font-semibold text-[var(--text-secondary)]">Horno Artesanal</div>
<div className="font-semibold text-[var(--text-secondary)]">Pan de Casa</div>
<div className="font-semibold text-[var(--text-secondary)]">Dulce Tradición</div>
</div>
</div>
</div>
</section>
{/* Pricing Section */}
<section id="pricing" className="py-24 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Planes que se Adaptan a tu Negocio
</h2>
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
Sin costos ocultos, sin compromisos largos. Comienza gratis y escala según crezcas.
</p>
</div>
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Starter Plan */}
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Starter</h3>
<p className="mt-2 text-sm text-[var(--text-secondary)]">Perfecto para panaderías pequeñas</p>
<div className="mt-6">
<span className="text-3xl font-bold text-[var(--text-primary)]">49</span>
<span className="text-[var(--text-secondary)]">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Hasta 50 productos</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Predicción básica de demanda</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Control de inventario</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Reportes básicos</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Soporte por email</span>
</div>
</div>
<Button className="w-full mt-8" variant="outline">
Comenzar Gratis
</Button>
</div>
{/* Professional Plan - Highlighted */}
<div className="bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-2xl p-8 relative shadow-2xl">
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<span className="bg-[var(--color-secondary)] text-white px-4 py-1 rounded-full text-sm font-semibold">
Más Popular
</span>
</div>
<h3 className="text-lg font-semibold text-white">Professional</h3>
<p className="mt-2 text-sm text-white/80">Para panaderías en crecimiento</p>
<div className="mt-6">
<span className="text-3xl font-bold text-white">149</span>
<span className="text-white/80">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Productos ilimitados</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">IA avanzada con 92% precisión</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Gestión completa de producción</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">POS integrado</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Analytics avanzado</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Soporte prioritario 24/7</span>
</div>
</div>
<Button className="w-full mt-8 bg-white text-[var(--color-primary)] hover:bg-[var(--bg-tertiary)]">
Comenzar Prueba Gratuita
</Button>
</div>
{/* Enterprise Plan */}
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Enterprise</h3>
<p className="mt-2 text-sm text-[var(--text-secondary)]">Para cadenas y grandes operaciones</p>
<div className="mt-6">
<span className="text-3xl font-bold text-[var(--text-primary)]">399</span>
<span className="text-[var(--text-secondary)]">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Multi-locación ilimitada</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">IA personalizada por ubicación</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">API personalizada</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Integración ERPs</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Manager dedicado</span>
</div>
</div>
<Button className="w-full mt-8" variant="outline">
Contactar Ventas
</Button>
</div>
</div>
<div className="mt-16 text-center">
<p className="text-sm text-[var(--text-tertiary)]">
🔒 Todos los planes incluyen cifrado de datos, backups automáticos y cumplimiento RGPD
</p>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Preguntas Frecuentes
</h2>
<p className="mt-4 text-lg text-[var(--text-secondary)]">
Todo lo que necesitas saber sobre Panadería IA
</p>
</div>
<div className="mt-16 space-y-8">
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Qué tan precisa es la predicción de demanda?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo
histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente
con más datos de tu panadería.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Cuánto tiempo toma implementar el sistema?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas.
La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Se integra con mi sistema POS actual?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado
para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Qué soporte técnico ofrecen?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones
de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Mis datos están seguros?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías
de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.
</p>
</div>
</div>
</div>
</section>
{/* Final CTA Section */}
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
</div>
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
Transforma tu Panadería
<span className="block text-white/90">Comenzando Hoy</span>
</h2>
<p className="mt-6 text-lg text-white/80 max-w-2xl mx-auto">
Únete a más de 500 panaderías que ya están reduciendo desperdicios, aumentando ventas y
optimizando operaciones con inteligencia artificial.
</p>
<div className="mt-12 flex flex-col sm:flex-row gap-6 justify-center">
<Link to="/register">
<Button
size="lg"
className="px-10 py-4 text-lg font-semibold bg-white text-[var(--color-primary)] hover:bg-[var(--bg-tertiary)] shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
>
Comenzar Prueba Gratuita 14 Días
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</Link>
<Button
size="lg"
variant="outline"
className="px-10 py-4 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200"
onClick={() => scrollToSection('demo')}
>
<Play className="mr-2 w-5 h-5" />
Ver Demo
</Button>
</div>
<div className="mt-12 grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
<div>
<div className="text-2xl font-bold text-white">14 días</div>
<div className="text-white/70 text-sm">Prueba gratuita</div>
</div>
<div>
<div className="text-2xl font-bold text-white">5 min</div>
<div className="text-white/70 text-sm">Configuración</div>
</div>
<div>
<div className="text-2xl font-bold text-white">24/7</div>
<div className="text-white/70 text-sm">Soporte incluido</div>
</div>
</div>
</div>
</section>
</PublicLayout>
);
};
export default LandingPage;

View File

@@ -0,0 +1,710 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui';
import { PublicLayout } from '../../components/layout';
import {
BarChart3,
TrendingUp,
Shield,
Zap,
Users,
Award,
ChevronRight,
Check,
Star,
ArrowRight,
Play,
Calendar,
Clock,
DollarSign,
Package,
PieChart,
Settings
} from 'lucide-react';
const LandingPage: React.FC = () => {
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<PublicLayout
variant="full-width"
contentPadding="none"
headerProps={{
showThemeToggle: true,
showAuthButtons: true,
variant: "default",
navigationItems: [
{ id: 'features', label: 'Características', href: '#features' },
{ id: 'benefits', label: 'Beneficios', href: '#benefits' },
{ id: 'pricing', label: 'Precios', href: '#pricing' },
{ id: 'testimonials', label: 'Testimonios', href: '#testimonials' }
]
}}
>
{/* Hero Section */}
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<div className="mb-6">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
<Zap className="w-4 h-4 mr-2" />
IA Avanzada para Panaderías
</span>
</div>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
<span className="block">Revoluciona tu</span>
<span className="block text-[var(--color-primary)]">Panadería con IA</span>
</h1>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
Optimiza automáticamente tu producción, reduce desperdicios hasta un 35%,
predice demanda con precisión del 92% y aumenta tus ventas con inteligencia artificial.
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Link to="/register">
<Button size="lg" className="px-8 py-4 text-lg font-semibold bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200">
Comenzar Gratis 14 Días
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</Link>
<Button
variant="outline"
size="lg"
className="px-8 py-4 text-lg font-semibold border-2 border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white transition-all duration-200"
onClick={() => scrollToSection('demo')}
>
<Play className="mr-2 w-5 h-5" />
Ver Demo en Vivo
</Button>
</div>
<div className="mt-12 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Sin tarjeta de crédito
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Configuración en 5 minutos
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Soporte 24/7 en español
</div>
</div>
</div>
</div>
{/* Background decoration */}
<div className="absolute top-0 left-0 right-0 h-full overflow-hidden -z-10">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-[var(--color-primary)]/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-[var(--color-secondary)]/5 rounded-full blur-3xl"></div>
</div>
</section>
{/* Stats Section */}
<section className="py-16 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">500+</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Panaderías Activas</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">35%</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Reducción de Desperdicios</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">92%</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Precisión de Predicciones</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">4.8★</div>
<div className="mt-2 text-sm text-[var(--text-secondary)]">Satisfacción de Clientes</div>
</div>
</div>
</div>
</section>
{/* Main Features Section */}
<section id="features" className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)]">
Gestión Completa con
<span className="block text-[var(--color-primary)]">Inteligencia Artificial</span>
</h2>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)]">
Automatiza procesos, optimiza recursos y toma decisiones inteligentes basadas en datos reales de tu panadería.
</p>
</div>
<div className="mt-20 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* AI Forecasting */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-xl flex items-center justify-center shadow-lg">
<TrendingUp className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Predicción Inteligente</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Algoritmos de IA analizan patrones históricos, clima, eventos locales y tendencias para predecir la demanda exacta de cada producto.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-primary)]">
<Check className="w-4 h-4 mr-2" />
Precisión del 92% en predicciones
</div>
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Reduce desperdicios hasta 35%
</div>
<div className="flex items-center text-sm text-[var(--color-primary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Aumenta ventas promedio 22%
</div>
</div>
</div>
</div>
{/* Smart Inventory */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] rounded-xl flex items-center justify-center shadow-lg">
<Package className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Inventario Inteligente</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-secondary)]">
<Check className="w-4 h-4 mr-2" />
Alertas automáticas de stock bajo
</div>
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Órdenes de compra automatizadas
</div>
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
<Check className="w-4 h-4 mr-2" />
Optimización de costos de materias primas
</div>
</div>
</div>
</div>
{/* Production Planning */}
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
<div className="absolute -top-4 left-8">
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-dark)] rounded-xl flex items-center justify-center shadow-lg">
<Calendar className="w-6 h-6 text-white" />
</div>
</div>
<div className="mt-6">
<h3 className="text-xl font-bold text-[var(--text-primary)]">Planificación de Producción</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.
</p>
<div className="mt-6">
<div className="flex items-center text-sm text-[var(--color-accent)]">
<Check className="w-4 h-4 mr-2" />
Programación automática de horneado
</div>
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
<Check className="w-4 h-4 mr-2" />
Optimización de uso de hornos
</div>
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
<Check className="w-4 h-4 mr-2" />
Gestión de personal y turnos
</div>
</div>
</div>
</div>
</div>
{/* Additional Features Grid */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Analytics Avanzado</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Dashboards en tiempo real con métricas clave</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<DollarSign className="w-6 h-6 text-[var(--color-secondary)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">POS Integrado</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Sistema de ventas completo y fácil de usar</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-accent)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<Shield className="w-6 h-6 text-[var(--color-accent)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Control de Calidad</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Trazabilidad completa y gestión HACCP</p>
</div>
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
<div className="w-12 h-12 bg-[var(--color-info)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
<Settings className="w-6 h-6 text-[var(--color-info)]" />
</div>
<h4 className="font-semibold text-[var(--text-primary)]">Automatización</h4>
<p className="text-sm text-[var(--text-secondary)] mt-2">Procesos automáticos que ahorran tiempo</p>
</div>
</div>
</div>
</section>
{/* Benefits Section */}
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-2 lg:gap-16 items-center">
<div>
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Resultados Comprobados
<span className="block text-[var(--color-primary)]">en Cientos de Panaderías</span>
</h2>
<p className="mt-6 text-lg text-[var(--text-secondary)]">
Nuestros clientes han logrado transformaciones significativas en sus operaciones,
mejorando rentabilidad y reduciendo desperdicios desde el primer mes.
</p>
<div className="mt-10 space-y-8">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Aumenta Ventas 22% Promedio</h4>
<p className="text-[var(--text-secondary)]">Optimización de producción y mejor disponibilidad de productos populares</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<Shield className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Reduce Desperdicios 35%</h4>
<p className="text-[var(--text-secondary)]">Predicciones precisas evitan sobreproducción y productos vencidos</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="w-5 h-5 text-purple-600" />
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Ahorra 8 Horas Semanales</h4>
<p className="text-[var(--text-secondary)]">Automatización de tareas administrativas y de planificación</p>
</div>
</div>
</div>
</div>
<div className="mt-12 lg:mt-0">
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-2xl p-8">
<div className="grid grid-cols-2 gap-8">
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-primary)]">€127k</div>
<div className="text-sm text-[var(--text-secondary)]">Ahorro promedio anual por panadería</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-secondary)]">98%</div>
<div className="text-sm text-[var(--text-secondary)]">Satisfacción de clientes</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-accent)]">2.3x</div>
<div className="text-sm text-[var(--text-secondary)]">ROI promedio en 12 meses</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-info)]">24/7</div>
<div className="text-sm text-[var(--text-secondary)]">Soporte técnico especializado</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Testimonials Section */}
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Lo que Dicen Nuestros Clientes
</h2>
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
Panaderías de toda España han transformado sus negocios con nuestra plataforma
</p>
</div>
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Testimonial 1 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"Desde que implementamos Panadería IA, nuestros desperdicios se redujeron un 40% y las ventas aumentaron un 28%.
La predicción de demanda es increíblemente precisa."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold">
M
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">María González</div>
<div className="text-sm text-[var(--text-secondary)]">Panadería Santa María, Madrid</div>
</div>
</div>
</div>
{/* Testimonial 2 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"El sistema nos ahorra 10 horas semanales en planificación. Ahora puedo enfocarme en mejorar nuestros productos
mientras la IA maneja la logística."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-secondary)] rounded-full flex items-center justify-center text-white font-bold">
C
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">Carlos Ruiz</div>
<div className="text-sm text-[var(--text-secondary)]">Horno de Oro, Valencia</div>
</div>
</div>
</div>
{/* Testimonial 3 */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
<div className="flex items-center mb-4">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
))}
</div>
<blockquote className="text-[var(--text-secondary)] italic">
"Increíble cómo predice exactamente cuántos panes necesitamos cada día. Nuestros clientes siempre encuentran
sus productos favoritos disponibles."
</blockquote>
<div className="mt-6 flex items-center">
<div className="w-12 h-12 bg-[var(--color-accent)] rounded-full flex items-center justify-center text-white font-bold">
A
</div>
<div className="ml-4">
<div className="font-semibold text-[var(--text-primary)]">Ana Martínez</div>
<div className="text-sm text-[var(--text-secondary)]">Pan & Tradición, Sevilla</div>
</div>
</div>
</div>
</div>
{/* Trust indicators */}
<div className="mt-16 text-center">
<p className="text-sm text-[var(--text-tertiary)] mb-8">Confiado por más de 500 panaderías en España</p>
<div className="flex items-center justify-center space-x-8 opacity-60">
<div className="font-semibold text-[var(--text-secondary)]">Panadería Real</div>
<div className="font-semibold text-[var(--text-secondary)]">Horno Artesanal</div>
<div className="font-semibold text-[var(--text-secondary)]">Pan de Casa</div>
<div className="font-semibold text-[var(--text-secondary)]">Dulce Tradición</div>
</div>
</div>
</div>
</section>
{/* Pricing Section */}
<section id="pricing" className="py-24 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Planes que se Adaptan a tu Negocio
</h2>
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
Sin costos ocultos, sin compromisos largos. Comienza gratis y escala según crezcas.
</p>
</div>
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Starter Plan */}
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Starter</h3>
<p className="mt-2 text-sm text-[var(--text-secondary)]">Perfecto para panaderías pequeñas</p>
<div className="mt-6">
<span className="text-3xl font-bold text-[var(--text-primary)]">€49</span>
<span className="text-[var(--text-secondary)]">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Hasta 50 productos</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Predicción básica de demanda</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Control de inventario</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Reportes básicos</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Soporte por email</span>
</div>
</div>
<Button className="w-full mt-8" variant="outline">
Comenzar Gratis
</Button>
</div>
{/* Professional Plan - Highlighted */}
<div className="bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-2xl p-8 relative shadow-2xl">
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<span className="bg-[var(--color-secondary)] text-white px-4 py-1 rounded-full text-sm font-semibold">
Más Popular
</span>
</div>
<h3 className="text-lg font-semibold text-white">Professional</h3>
<p className="mt-2 text-sm text-white/80">Para panaderías en crecimiento</p>
<div className="mt-6">
<span className="text-3xl font-bold text-white">€149</span>
<span className="text-white/80">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Productos ilimitados</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">IA avanzada con 92% precisión</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Gestión completa de producción</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">POS integrado</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Analytics avanzado</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-white mr-3" />
<span className="text-sm text-white">Soporte prioritario 24/7</span>
</div>
</div>
<Button className="w-full mt-8 bg-white text-[var(--color-primary)] hover:bg-gray-100">
Comenzar Prueba Gratuita
</Button>
</div>
{/* Enterprise Plan */}
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Enterprise</h3>
<p className="mt-2 text-sm text-[var(--text-secondary)]">Para cadenas y grandes operaciones</p>
<div className="mt-6">
<span className="text-3xl font-bold text-[var(--text-primary)]">€399</span>
<span className="text-[var(--text-secondary)]">/mes</span>
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Multi-locación ilimitada</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">IA personalizada por ubicación</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">API personalizada</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Integración ERPs</span>
</div>
<div className="flex items-center">
<Check className="w-5 h-5 text-green-500 mr-3" />
<span className="text-sm text-[var(--text-secondary)]">Manager dedicado</span>
</div>
</div>
<Button className="w-full mt-8" variant="outline">
Contactar Ventas
</Button>
</div>
</div>
<div className="mt-16 text-center">
<p className="text-sm text-[var(--text-tertiary)]">
🔒 Todos los planes incluyen cifrado de datos, backups automáticos y cumplimiento RGPD
</p>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-24 bg-[var(--bg-secondary)]">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
Preguntas Frecuentes
</h2>
<p className="mt-4 text-lg text-[var(--text-secondary)]">
Todo lo que necesitas saber sobre Panadería IA
</p>
</div>
<div className="mt-16 space-y-8">
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Qué tan precisa es la predicción de demanda?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo
histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente
con más datos de tu panadería.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Cuánto tiempo toma implementar el sistema?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas.
La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Se integra con mi sistema POS actual?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Sí, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado
para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Qué soporte técnico ofrecen?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones
de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.
</p>
</div>
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
¿Mis datos están seguros?
</h3>
<p className="mt-4 text-[var(--text-secondary)]">
Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías
de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.
</p>
</div>
</div>
</div>
</section>
{/* Final CTA Section */}
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
</div>
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
Transforma tu Panadería
<span className="block text-white/90">Comenzando Hoy</span>
</h2>
<p className="mt-6 text-lg text-white/80 max-w-2xl mx-auto">
Únete a más de 500 panaderías que ya están reduciendo desperdicios, aumentando ventas y
optimizando operaciones con inteligencia artificial.
</p>
<div className="mt-12 flex flex-col sm:flex-row gap-6 justify-center">
<Link to="/register">
<Button
size="lg"
className="px-10 py-4 text-lg font-semibold bg-white text-[var(--color-primary)] hover:bg-gray-100 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
>
Comenzar Prueba Gratuita 14 Días
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</Link>
<Button
size="lg"
variant="outline"
className="px-10 py-4 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200"
onClick={() => scrollToSection('demo')}
>
<Play className="mr-2 w-5 h-5" />
Ver Demo
</Button>
</div>
<div className="mt-12 grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
<div>
<div className="text-2xl font-bold text-white">14 días</div>
<div className="text-white/70 text-sm">Prueba gratuita</div>
</div>
<div>
<div className="text-2xl font-bold text-white">5 min</div>
<div className="text-white/70 text-sm">Configuración</div>
</div>
<div>
<div className="text-2xl font-bold text-white">24/7</div>
<div className="text-white/70 text-sm">Soporte incluido</div>
</div>
</div>
</div>
</section>
</PublicLayout>
);
};
export default LandingPage;

View File

@@ -0,0 +1,201 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuthActions, useAuthError, useAuthLoading, useIsAuthenticated } from '../../stores';
import { Button, Input, Card } from '../../components/ui';
import { PublicLayout } from '../../components/layout';
const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuthActions();
const error = useAuthError();
const loading = useAuthLoading();
const isAuthenticated = useIsAuthenticated();
const from = (location.state as any)?.from?.pathname || '/app';
useEffect(() => {
if (isAuthenticated && !loading) {
// Add a small delay to ensure the auth state has fully settled
setTimeout(() => {
navigate(from, { replace: true });
}, 100);
}
}, [isAuthenticated, loading, navigate, from]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) return;
try {
await login(email, password);
} catch (err) {
// Error is handled by the store
}
};
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
variant: "minimal"
}}
>
<div className="w-full max-w-md mx-auto space-y-8">
<div>
<div className="flex justify-center">
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
PI
</div>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
Inicia sesión en tu cuenta
</h2>
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
O{' '}
<Link
to="/register"
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
>
regístrate para comenzar tu prueba gratuita
</Link>
</p>
</div>
<Card className="p-8">
<form className="space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-[var(--color-error)]" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-[var(--color-error)]">
Error de autenticación
</h3>
<div className="mt-2 text-sm text-[var(--color-error)]">
{error}
</div>
</div>
</div>
</div>
)}
<div>
<label htmlFor="email" className="sr-only">
Correo electrónico
</label>
<Input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="Correo electrónico"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Contraseña
</label>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
placeholder="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-primary)] rounded"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-[var(--text-primary)]">
Recordarme
</label>
</div>
<div className="text-sm">
<a href="#" className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
¿Olvidaste tu contraseña?
</a>
</div>
</div>
<div>
<Button
type="submit"
className="w-full flex justify-center"
disabled={loading}
>
{loading ? 'Iniciando sesión...' : 'Iniciar sesión'}
</Button>
</div>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-[var(--border-primary)]" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-[var(--bg-primary)] text-[var(--text-tertiary)]">Demo</span>
</div>
</div>
<div className="mt-6">
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => {
// TODO: Handle demo login
console.log('Demo login');
}}
>
Usar cuenta de demo
</Button>
</div>
</div>
</form>
<div className="mt-6 text-center text-xs text-[var(--text-tertiary)]">
Al iniciar sesión, aceptas nuestros{' '}
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
Términos de Servicio
</a>
{' '}y{' '}
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
Política de Privacidad
</a>
</div>
</Card>
</div>
</PublicLayout>
);
};
export default LoginPage;

View File

@@ -0,0 +1,376 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Button, Input, Card, Select } from '../../components/ui';
import { PublicLayout } from '../../components/layout';
const RegisterPage: React.FC = () => {
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
// Personal info
firstName: '',
lastName: '',
email: '',
phone: '',
// Company info
companyName: '',
companyType: '',
employeeCount: '',
// Account info
password: '',
confirmPassword: '',
acceptTerms: false,
acceptMarketing: false,
});
const navigate = useNavigate();
const handleInputChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleNextStep = () => {
setStep(prev => prev + 1);
};
const handlePrevStep = () => {
setStep(prev => prev - 1);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Redirect to onboarding
navigate('/onboarding');
} catch (error) {
console.error('Registration failed:', error);
} finally {
setLoading(false);
}
};
const isStep1Valid = formData.firstName && formData.lastName && formData.email && formData.phone;
const isStep2Valid = formData.companyName && formData.companyType && formData.employeeCount;
const isStep3Valid = formData.password && formData.confirmPassword &&
formData.password === formData.confirmPassword && formData.acceptTerms;
return (
<PublicLayout
variant="centered"
maxWidth="md"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
variant: "minimal"
}}
>
<div className="w-full max-w-md mx-auto space-y-8">
<div>
<div className="flex justify-center">
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
PI
</div>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
Crea tu cuenta
</h2>
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
O{' '}
<Link
to="/login"
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
>
inicia sesión si ya tienes una cuenta
</Link>
</p>
</div>
<Card className="p-8">
{/* Progress indicator */}
<div className="mb-8">
<div className="flex items-center">
{[1, 2, 3].map((stepNumber) => (
<div key={stepNumber} className="flex items-center">
<div
className={`flex items-center justify-center w-8 h-8 rounded-full ${
step >= stepNumber
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)]'
}`}
>
{stepNumber}
</div>
{stepNumber < 3 && (
<div
className={`flex-1 h-0.5 mx-4 ${
step > stepNumber ? 'bg-[var(--color-primary)]' : 'bg-[var(--bg-quaternary)]'
}`}
/>
)}
</div>
))}
</div>
<div className="flex justify-between mt-2 text-xs text-[var(--text-secondary)]">
<span>Datos personales</span>
<span>Información empresarial</span>
<span>Crear cuenta</span>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{step === 1 && (
<>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--text-primary)]">
Nombre *
</label>
<Input
id="firstName"
type="text"
required
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Tu nombre"
/>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--text-primary)]">
Apellido *
</label>
<Input
id="lastName"
type="text"
required
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Tu apellido"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)]">
Correo electrónico *
</label>
<Input
id="email"
type="email"
required
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="tu@email.com"
/>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-[var(--text-primary)]">
Teléfono *
</label>
<Input
id="phone"
type="tel"
required
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="+34 600 000 000"
/>
</div>
</div>
<Button
type="button"
onClick={handleNextStep}
disabled={!isStep1Valid}
className="w-full"
>
Continuar
</Button>
</>
)}
{step === 2 && (
<>
<div className="space-y-4">
<div>
<label htmlFor="companyName" className="block text-sm font-medium text-[var(--text-primary)]">
Nombre de la panadería *
</label>
<Input
id="companyName"
type="text"
required
value={formData.companyName}
onChange={(e) => handleInputChange('companyName', e.target.value)}
placeholder="Panadería San Miguel"
/>
</div>
<div>
<label htmlFor="companyType" className="block text-sm font-medium text-[var(--text-primary)]">
Tipo de negocio *
</label>
<Select
value={formData.companyType}
onValueChange={(value) => handleInputChange('companyType', value)}
>
<option value="">Selecciona el tipo</option>
<option value="traditional">Panadería tradicional</option>
<option value="artisan">Panadería artesanal</option>
<option value="industrial">Panadería industrial</option>
<option value="bakery-cafe">Panadería-cafetería</option>
<option value="specialty">Panadería especializada</option>
</Select>
</div>
<div>
<label htmlFor="employeeCount" className="block text-sm font-medium text-[var(--text-primary)]">
Número de empleados *
</label>
<Select
value={formData.employeeCount}
onValueChange={(value) => handleInputChange('employeeCount', value)}
>
<option value="">Selecciona el rango</option>
<option value="1">Solo yo</option>
<option value="2-5">2-5 empleados</option>
<option value="6-15">6-15 empleados</option>
<option value="16-50">16-50 empleados</option>
<option value="51+">Más de 50 empleados</option>
</Select>
</div>
</div>
<div className="flex space-x-4">
<Button
type="button"
variant="outline"
onClick={handlePrevStep}
className="flex-1"
>
Atrás
</Button>
<Button
type="button"
onClick={handleNextStep}
disabled={!isStep2Valid}
className="flex-1"
>
Continuar
</Button>
</div>
</>
)}
{step === 3 && (
<>
<div className="space-y-4">
<div>
<label htmlFor="password" className="block text-sm font-medium text-[var(--text-primary)]">
Contraseña *
</label>
<Input
id="password"
type="password"
required
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
placeholder="Mínimo 8 caracteres"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[var(--text-primary)]">
Confirmar contraseña *
</label>
<Input
id="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
placeholder="Repite la contraseña"
/>
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
<p className="mt-1 text-sm text-[var(--color-error)]">Las contraseñas no coinciden</p>
)}
</div>
</div>
<div className="space-y-4">
<div className="flex items-start">
<input
id="acceptTerms"
type="checkbox"
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
checked={formData.acceptTerms}
onChange={(e) => handleInputChange('acceptTerms', e.target.checked)}
/>
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-[var(--text-primary)]">
Acepto los{' '}
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
Términos de Servicio
</a>{' '}
y la{' '}
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
Política de Privacidad
</a>
</label>
</div>
<div className="flex items-start">
<input
id="acceptMarketing"
type="checkbox"
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
checked={formData.acceptMarketing}
onChange={(e) => handleInputChange('acceptMarketing', e.target.checked)}
/>
<label htmlFor="acceptMarketing" className="ml-2 block text-sm text-[var(--text-primary)]">
Quiero recibir newsletters y novedades sobre el producto (opcional)
</label>
</div>
</div>
<div className="flex space-x-4">
<Button
type="button"
variant="outline"
onClick={handlePrevStep}
className="flex-1"
>
Atrás
</Button>
<Button
type="submit"
disabled={!isStep3Valid || loading}
className="flex-1"
>
{loading ? 'Creando cuenta...' : 'Crear cuenta'}
</Button>
</div>
</>
)}
</form>
<div className="mt-6 text-center text-xs text-[var(--text-secondary)]">
¿Necesitas ayuda?{' '}
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
Contáctanos
</a>
</div>
</Card>
</div>
</PublicLayout>
);
};
export default RegisterPage;

View File

@@ -0,0 +1,3 @@
export { default as LandingPage } from './LandingPage';
export { default as LoginPage } from './LoginPage';
export { default as RegisterPage } from './RegisterPage';

View File

@@ -1,521 +0,0 @@
// frontend/src/pages/recipes/RecipesPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Search,
Plus,
Filter,
LayoutGrid,
List,
ChefHat,
TrendingUp,
AlertTriangle,
Loader,
RefreshCw,
BarChart3,
Star,
Calendar,
Download,
Upload
} from 'lucide-react';
import toast from 'react-hot-toast';
import { useRecipes } from '../../api/hooks/useRecipes';
import { Recipe, RecipeSearchParams } from '../../api/services/recipes.service';
import RecipeCard from '../../components/recipes/RecipeCard';
type ViewMode = 'grid' | 'list';
interface FilterState {
search: string;
status?: string;
category?: string;
is_seasonal?: boolean;
is_signature?: boolean;
difficulty_level?: number;
}
interface RecipesPageProps {
view?: string;
}
const RecipesPage: React.FC<RecipesPageProps> = ({ view }) => {
const {
recipes,
categories,
statistics,
isLoading,
isCreating,
error,
pagination,
loadRecipes,
createRecipe,
updateRecipe,
deleteRecipe,
duplicateRecipe,
activateRecipe,
checkFeasibility,
loadStatistics,
clearError,
refresh,
setPage
} = useRecipes();
// Local state
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [filters, setFilters] = useState<FilterState>({
search: ''
});
const [showFilters, setShowFilters] = useState(false);
const [selectedRecipes, setSelectedRecipes] = useState<Set<string>>(new Set());
const [feasibilityResults, setFeasibilityResults] = useState<Map<string, any>>(new Map());
// Load recipes when filters change
useEffect(() => {
const searchParams: RecipeSearchParams = {
search_term: filters.search || undefined,
status: filters.status || undefined,
category: filters.category || undefined,
is_seasonal: filters.is_seasonal,
is_signature: filters.is_signature,
difficulty_level: filters.difficulty_level,
limit: 20,
offset: (pagination.page - 1) * 20
};
// Remove undefined values
Object.keys(searchParams).forEach(key => {
if (searchParams[key as keyof RecipeSearchParams] === undefined) {
delete searchParams[key as keyof RecipeSearchParams];
}
});
loadRecipes(searchParams);
}, [filters, pagination.page, loadRecipes]);
// Handle search
const handleSearch = useCallback((value: string) => {
setFilters(prev => ({ ...prev, search: value }));
setPage(1); // Reset to first page
}, [setPage]);
// Handle filter changes
const handleFilterChange = useCallback((key: keyof FilterState, value: any) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPage(1); // Reset to first page
}, [setPage]);
// Clear all filters
const clearFilters = useCallback(() => {
setFilters({ search: '' });
setPage(1);
}, [setPage]);
// Handle recipe selection
const toggleRecipeSelection = (recipeId: string) => {
const newSelection = new Set(selectedRecipes);
if (newSelection.has(recipeId)) {
newSelection.delete(recipeId);
} else {
newSelection.add(recipeId);
}
setSelectedRecipes(newSelection);
};
// Handle recipe actions
const handleViewRecipe = (recipe: Recipe) => {
// TODO: Navigate to recipe details page or open modal
console.log('View recipe:', recipe);
};
const handleEditRecipe = (recipe: Recipe) => {
// TODO: Navigate to recipe edit page or open modal
console.log('Edit recipe:', recipe);
};
const handleDuplicateRecipe = async (recipe: Recipe) => {
const newName = prompt(`Enter name for duplicated recipe:`, `${recipe.name} (Copy)`);
if (newName && newName.trim()) {
const result = await duplicateRecipe(recipe.id, newName.trim());
if (result) {
toast.success('Recipe duplicated successfully');
}
}
};
const handleActivateRecipe = async (recipe: Recipe) => {
if (confirm(`Are you sure you want to activate "${recipe.name}"?`)) {
const result = await activateRecipe(recipe.id);
if (result) {
toast.success('Recipe activated successfully');
}
}
};
const handleCheckFeasibility = async (recipe: Recipe) => {
const result = await checkFeasibility(recipe.id, 1.0);
if (result) {
setFeasibilityResults(prev => new Map(prev.set(recipe.id, result)));
if (result.feasible) {
toast.success('Recipe can be produced with current inventory');
} else {
toast.error('Recipe cannot be produced - missing ingredients');
}
}
};
const handleDeleteRecipe = async (recipe: Recipe) => {
if (confirm(`Are you sure you want to delete "${recipe.name}"? This action cannot be undone.`)) {
const success = await deleteRecipe(recipe.id);
if (success) {
toast.success('Recipe deleted successfully');
}
}
};
// Get quick stats
const getQuickStats = () => {
if (!statistics) {
return {
totalRecipes: recipes.length,
activeRecipes: recipes.filter(r => r.status === 'active').length,
signatureRecipes: recipes.filter(r => r.is_signature_item).length,
seasonalRecipes: recipes.filter(r => r.is_seasonal).length
};
}
return {
totalRecipes: statistics.total_recipes,
activeRecipes: statistics.active_recipes,
signatureRecipes: statistics.signature_recipes,
seasonalRecipes: statistics.seasonal_recipes
};
};
const stats = getQuickStats();
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="py-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Recipe Management</h1>
<p className="text-gray-600 mt-1">
Create and manage your bakery recipes
</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => refresh()}
disabled={isLoading}
className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-5 h-5 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
<Plus className="w-4 h-4" />
<span>New Recipe</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Quick Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<ChefHat className="w-8 h-8 text-blue-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Total Recipes</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalRecipes}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<TrendingUp className="w-8 h-8 text-green-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Active Recipes</p>
<p className="text-2xl font-bold text-gray-900">{stats.activeRecipes}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<Star className="w-8 h-8 text-yellow-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Signature Items</p>
<p className="text-2xl font-bold text-gray-900">{stats.signatureRecipes}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center">
<Calendar className="w-8 h-8 text-purple-600" />
<div className="ml-3">
<p className="text-sm text-gray-600">Seasonal Items</p>
<p className="text-2xl font-bold text-gray-900">{stats.seasonalRecipes}</p>
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white rounded-lg border mb-6 p-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
{/* Search */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search recipes..."
value={filters.search}
onChange={(e) => handleSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* View Controls */}
<div className="flex items-center space-x-2">
<button
onClick={() => setShowFilters(!showFilters)}
className={`px-3 py-2 rounded-lg transition-colors flex items-center space-x-1 ${
showFilters ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Filter className="w-4 h-4" />
<span>Filters</span>
</button>
<div className="flex rounded-lg border">
<button
onClick={() => setViewMode('grid')}
className={`p-2 ${
viewMode === 'grid'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 ${
viewMode === 'list'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Advanced Filters */}
{showFilters && (
<div className="mt-4 pt-4 border-t">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
value={filters.status || ''}
onChange={(e) => handleFilterChange('status', e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All Statuses</option>
<option value="draft">Draft</option>
<option value="active">Active</option>
<option value="testing">Testing</option>
<option value="archived">Archived</option>
<option value="discontinued">Discontinued</option>
</select>
</div>
{/* Category */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
value={filters.category || ''}
onChange={(e) => handleFilterChange('category', e.target.value || undefined)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All Categories</option>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
{/* Difficulty */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Difficulty
</label>
<select
value={filters.difficulty_level || ''}
onChange={(e) => handleFilterChange('difficulty_level',
e.target.value ? parseInt(e.target.value) : undefined
)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All Levels</option>
<option value="1">Level 1 (Easy)</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5 (Hard)</option>
</select>
</div>
{/* Special Types */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Special Types
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={filters.is_signature || false}
onChange={(e) => handleFilterChange('is_signature', e.target.checked || undefined)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Signature items</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={filters.is_seasonal || false}
onChange={(e) => handleFilterChange('is_seasonal', e.target.checked || undefined)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Seasonal items</span>
</label>
</div>
</div>
{/* Clear Filters */}
<div className="flex items-end">
<button
onClick={clearFilters}
className="px-3 py-2 text-sm text-gray-600 hover:text-gray-800"
>
Clear filters
</button>
</div>
</div>
</div>
)}
</div>
{/* Recipes Grid/List */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading recipes...</span>
</div>
) : error ? (
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<AlertTriangle className="w-8 h-8 text-red-600 mx-auto mb-3" />
<h3 className="text-lg font-medium text-red-900 mb-2">Error loading recipes</h3>
<p className="text-red-700 mb-4">{error}</p>
<button
onClick={refresh}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Try Again
</button>
</div>
) : recipes.length === 0 ? (
<div className="bg-white rounded-lg border p-12 text-center">
<ChefHat className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{Object.values(filters).some(v => v)
? 'No recipes found'
: 'No recipes yet'
}
</h3>
<p className="text-gray-600 mb-6">
{Object.values(filters).some(v => v)
? 'Try adjusting your search and filter criteria'
: 'Create your first recipe to get started'
}
</p>
<button className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Create Recipe
</button>
</div>
) : (
<div>
<div className={
viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6'
: 'space-y-4'
}>
{recipes.map((recipe) => (
<RecipeCard
key={recipe.id}
recipe={recipe}
compact={viewMode === 'list'}
onView={handleViewRecipe}
onEdit={handleEditRecipe}
onDuplicate={handleDuplicateRecipe}
onActivate={handleActivateRecipe}
onCheckFeasibility={handleCheckFeasibility}
feasibility={feasibilityResults.get(recipe.id)}
/>
))}
</div>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="mt-8 flex items-center justify-between">
<div className="text-sm text-gray-600">
Showing {((pagination.page - 1) * pagination.limit) + 1} to{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
{pagination.total} recipes
</div>
<div className="flex space-x-2">
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setPage(page)}
className={`px-3 py-2 rounded-lg ${
page === pagination.page
? 'bg-blue-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50 border'
}`}
>
{page}
</button>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default RecipesPage;

View File

@@ -1,207 +0,0 @@
import React, { useState } from 'react';
import {
BarChart3,
ShoppingCart,
TrendingUp,
Package
} from 'lucide-react';
import { SalesAnalyticsDashboard, SalesManagementPage } from '../../components/sales';
import Button from '../../components/ui/Button';
type SalesPageView = 'overview' | 'analytics' | 'management';
interface SalesPageProps {
view?: string;
}
const SalesPage: React.FC<SalesPageProps> = ({ view = 'daily-sales' }) => {
const [activeView, setActiveView] = useState<SalesPageView>('overview');
const renderContent = () => {
switch (activeView) {
case 'analytics':
return <SalesAnalyticsDashboard />;
case 'management':
return <SalesManagementPage />;
case 'overview':
default:
return (
<div className="space-y-6">
{/* Overview Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-xl p-8 text-white">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Panel de Ventas</h1>
<p className="text-blue-100">
Gestiona, analiza y optimiza tus ventas con insights inteligentes
</p>
</div>
<div className="w-16 h-16 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<ShoppingCart className="w-8 h-8 text-white" />
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
onClick={() => setActiveView('analytics')}
className="bg-white rounded-xl p-6 border border-gray-200 hover:shadow-md transition-shadow cursor-pointer group"
>
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center group-hover:bg-blue-200 transition-colors">
<BarChart3 className="w-6 h-6 text-blue-600" />
</div>
<TrendingUp className="w-5 h-5 text-green-500" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Análisis de Ventas
</h3>
<p className="text-gray-600 text-sm mb-4">
Explora métricas detalladas, tendencias y insights de rendimiento
</p>
<div className="flex items-center text-blue-600 text-sm font-medium">
Ver Analytics
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
<div
onClick={() => setActiveView('management')}
className="bg-white rounded-xl p-6 border border-gray-200 hover:shadow-md transition-shadow cursor-pointer group"
>
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-200 transition-colors">
<Package className="w-6 h-6 text-green-600" />
</div>
<ShoppingCart className="w-5 h-5 text-blue-500" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Gestión de Ventas
</h3>
<p className="text-gray-600 text-sm mb-4">
Administra, filtra y exporta todos tus registros de ventas
</p>
<div className="flex items-center text-green-600 text-sm font-medium">
Gestionar Ventas
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
<div className="bg-white rounded-xl p-6 border border-gray-200 hover:shadow-md transition-shadow cursor-pointer group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center group-hover:bg-purple-200 transition-colors">
<TrendingUp className="w-6 h-6 text-purple-600" />
</div>
<div className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded-full">
Próximamente
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Predicciones IA
</h3>
<p className="text-gray-600 text-sm mb-4">
Predicciones inteligentes y recomendaciones de ventas
</p>
<div className="flex items-center text-purple-600 text-sm font-medium opacity-50">
En Desarrollo
</div>
</div>
</div>
{/* Quick Insights */}
<div className="bg-white rounded-xl p-6 border border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Insights Rápidos</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<TrendingUp className="w-8 h-8 text-green-600" />
</div>
<div className="text-2xl font-bold text-gray-900 mb-1">+12.5%</div>
<div className="text-sm text-gray-600">Crecimiento mensual</div>
</div>
<div className="text-center">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<ShoppingCart className="w-8 h-8 text-blue-600" />
</div>
<div className="text-2xl font-bold text-gray-900 mb-1">247</div>
<div className="text-sm text-gray-600">Pedidos este mes</div>
</div>
<div className="text-center">
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Package className="w-8 h-8 text-purple-600" />
</div>
<div className="text-2xl font-bold text-gray-900 mb-1">18.50</div>
<div className="text-sm text-gray-600">Valor promedio pedido</div>
</div>
<div className="text-center">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-3">
<BarChart3 className="w-8 h-8 text-orange-600" />
</div>
<div className="text-2xl font-bold text-gray-900 mb-1">4.2</div>
<div className="text-sm text-gray-600">Puntuación satisfacción</div>
</div>
</div>
</div>
{/* Getting Started */}
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-xl p-6 border border-indigo-200">
<h2 className="text-xl font-semibold text-gray-900 mb-4">¿Primera vez aquí?</h2>
<p className="text-gray-700 mb-6">
Comienza explorando tus análisis de ventas para descubrir insights valiosos
sobre el rendimiento de tu panadería.
</p>
<div className="flex flex-col sm:flex-row gap-3">
<Button
onClick={() => setActiveView('analytics')}
className="bg-indigo-600 hover:bg-indigo-700 text-white"
>
<BarChart3 className="w-4 h-4 mr-2" />
Ver Analytics
</Button>
<Button
variant="outline"
onClick={() => setActiveView('management')}
>
<Package className="w-4 h-4 mr-2" />
Gestionar Ventas
</Button>
</div>
</div>
</div>
);
}
};
return (
<div className="p-4 md:p-6 space-y-6">
{/* Navigation */}
{activeView !== 'overview' && (
<div className="flex items-center justify-between mb-6">
<nav className="flex items-center space-x-4">
<button
onClick={() => setActiveView('overview')}
className="text-gray-600 hover:text-gray-900 text-sm font-medium"
>
Volver al Panel
</button>
</nav>
</div>
)}
{/* Content */}
{renderContent()}
</div>
);
};
export default SalesPage;

Some files were not shown because too many files have changed in this diff Show More