Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

@@ -1,313 +0,0 @@
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

@@ -1,385 +0,0 @@
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

@@ -1,2 +1,4 @@
export * from './forecasting';
export * from './sales-analytics';
export * from './sales-analytics';
export * from './performance';
export * from './ai-insights';

View File

@@ -1,403 +0,0 @@
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

@@ -1,379 +0,0 @@
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

@@ -1,388 +0,0 @@
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

@@ -1,388 +0,0 @@
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

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

View File

@@ -1,312 +0,0 @@
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

@@ -1,336 +0,0 @@
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

@@ -1,423 +0,0 @@
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

@@ -1,232 +0,0 @@
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

@@ -1,405 +0,0 @@
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

@@ -1,368 +0,0 @@
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

@@ -1,449 +0,0 @@
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

@@ -1,315 +0,0 @@
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

@@ -1,412 +0,0 @@
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

@@ -3,7 +3,7 @@ import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Se
import { Button, Card, Input, Select, Modal, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useToast } from '../../../../hooks/ui/useToast';
import { posService, POSConfiguration } from '../../../../services/api/pos.service';
import { posService, POSConfiguration } from '../../../../api/services/pos.service';
interface BakeryConfig {
// General Info

View File

@@ -1,481 +0,0 @@
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

@@ -3,4 +3,5 @@ export { default as ProfilePage } from './profile';
export { default as BakeryConfigPage } from './bakery-config';
export { default as TeamPage } from './team';
export { default as SubscriptionPage } from './subscription';
export { default as PreferencesPage } from './preferences';
export { default as PreferencesPage } from './preferences';
export { default as PreferencesPage } from './PreferencesPage';

View File

@@ -1,388 +0,0 @@
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

@@ -37,7 +37,7 @@ import {
subscriptionService,
type UsageSummary,
type AvailablePlans
} from '../../../../services/api';
} from '../../../../api/services';
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
interface PlanComparisonProps {

View File

@@ -37,7 +37,7 @@ import {
subscriptionService,
type UsageSummary,
type AvailablePlans
} from '../../../../services/api';
} from '../../../../api/services';
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
interface PlanComparisonProps {

View File

@@ -1,406 +0,0 @@
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;