ADD new frontend
This commit is contained in:
312
frontend/src/pages/app/data/events/EventsPage.tsx
Normal file
312
frontend/src/pages/app/data/events/EventsPage.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar, Activity, Filter, Download, Eye, BarChart3 } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const EventsPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-26 10:30:00',
|
||||
category: 'sales',
|
||||
type: 'order_completed',
|
||||
title: 'Pedido Completado',
|
||||
description: 'Pedido #ORD-456 completado por €127.50',
|
||||
metadata: {
|
||||
orderId: 'ORD-456',
|
||||
amount: 127.50,
|
||||
customer: 'María González',
|
||||
items: 8
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-26 09:15:00',
|
||||
category: 'production',
|
||||
type: 'batch_started',
|
||||
title: 'Lote Iniciado',
|
||||
description: 'Iniciado lote de croissants CR-024',
|
||||
metadata: {
|
||||
batchId: 'CR-024',
|
||||
product: 'Croissants',
|
||||
quantity: 48,
|
||||
expectedDuration: '2.5h'
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-26 08:45:00',
|
||||
category: 'inventory',
|
||||
type: 'stock_updated',
|
||||
title: 'Stock Actualizado',
|
||||
description: 'Repuesto stock de harina - Nivel: 50kg',
|
||||
metadata: {
|
||||
item: 'Harina de Trigo',
|
||||
previousLevel: '5kg',
|
||||
newLevel: '50kg',
|
||||
supplier: 'Molinos del Sur'
|
||||
},
|
||||
severity: 'success'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: '2024-01-26 07:30:00',
|
||||
category: 'system',
|
||||
type: 'user_login',
|
||||
title: 'Inicio de Sesión',
|
||||
description: 'Usuario admin ha iniciado sesión',
|
||||
metadata: {
|
||||
userId: 'admin',
|
||||
ipAddress: '192.168.1.100',
|
||||
userAgent: 'Chrome/120.0',
|
||||
location: 'Madrid, ES'
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: '2024-01-25 19:20:00',
|
||||
category: 'sales',
|
||||
type: 'payment_processed',
|
||||
title: 'Pago Procesado',
|
||||
description: 'Pago de €45.80 procesado exitosamente',
|
||||
metadata: {
|
||||
amount: 45.80,
|
||||
method: 'Tarjeta',
|
||||
reference: 'PAY-789',
|
||||
customer: 'Juan Pérez'
|
||||
},
|
||||
severity: 'success'
|
||||
}
|
||||
];
|
||||
|
||||
const eventStats = {
|
||||
total: events.length,
|
||||
today: events.filter(e =>
|
||||
new Date(e.timestamp).toDateString() === new Date().toDateString()
|
||||
).length,
|
||||
sales: events.filter(e => e.category === 'sales').length,
|
||||
production: events.filter(e => e.category === 'production').length,
|
||||
system: events.filter(e => e.category === 'system').length
|
||||
};
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todos', count: events.length },
|
||||
{ value: 'sales', label: 'Ventas', count: eventStats.sales },
|
||||
{ value: 'production', label: 'Producción', count: eventStats.production },
|
||||
{ value: 'inventory', label: 'Inventario', count: events.filter(e => e.category === 'inventory').length },
|
||||
{ value: 'system', label: 'Sistema', count: eventStats.system }
|
||||
];
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'success': return 'green';
|
||||
case 'warning': return 'yellow';
|
||||
case 'error': return 'red';
|
||||
default: return 'blue';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
switch (category) {
|
||||
case 'sales': return <BarChart3 {...iconProps} />;
|
||||
case 'production': return <Activity {...iconProps} />;
|
||||
case 'inventory': return <Calendar {...iconProps} />;
|
||||
default: return <Activity {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEvents = selectedCategory === 'all'
|
||||
? events
|
||||
: events.filter(event => event.category === selectedCategory);
|
||||
|
||||
const formatTimeAgo = (timestamp: string) => {
|
||||
const now = new Date();
|
||||
const eventTime = new Date(timestamp);
|
||||
const diffInMs = now.getTime() - eventTime.getTime();
|
||||
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
|
||||
if (diffInDays > 0) {
|
||||
return `hace ${diffInDays}d`;
|
||||
} else if (diffInHours > 0) {
|
||||
return `hace ${diffInHours}h`;
|
||||
} else {
|
||||
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Registro de Eventos"
|
||||
description="Seguimiento de todas las actividades y eventos del sistema"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros Avanzados
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Event Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Eventos</p>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{eventStats.total}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Hoy</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">{eventStats.today}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<Calendar className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<BarChart3 className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{eventStats.production}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="day">Hoy</option>
|
||||
<option value="week">Esta Semana</option>
|
||||
<option value="month">Este Mes</option>
|
||||
<option value="all">Todos</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.value}
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedCategory === category.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
|
||||
}`}
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Events List */}
|
||||
<div className="space-y-4">
|
||||
{filteredEvents.map((event) => (
|
||||
<Card key={event.id} className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
|
||||
{getCategoryIcon(event.category)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{event.title}</h3>
|
||||
<Badge variant={getSeverityColor(event.severity)}>
|
||||
{event.category}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-[var(--text-secondary)] mb-3">{event.description}</p>
|
||||
|
||||
{/* Event Metadata */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{Object.entries(event.metadata).map(([key, value]) => (
|
||||
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
|
||||
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider mb-1">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-[var(--text-tertiary)]">
|
||||
<span>{formatTimeAgo(event.timestamp)}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredEvents.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Activity className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay eventos</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
No se encontraron eventos para el período y categoría seleccionados.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventsPage;
|
||||
312
frontend/src/pages/app/data/events/EventsPage.tsx.backup
Normal file
312
frontend/src/pages/app/data/events/EventsPage.tsx.backup
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar, Activity, Filter, Download, Eye, BarChart3 } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const EventsPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-26 10:30:00',
|
||||
category: 'sales',
|
||||
type: 'order_completed',
|
||||
title: 'Pedido Completado',
|
||||
description: 'Pedido #ORD-456 completado por €127.50',
|
||||
metadata: {
|
||||
orderId: 'ORD-456',
|
||||
amount: 127.50,
|
||||
customer: 'María González',
|
||||
items: 8
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-26 09:15:00',
|
||||
category: 'production',
|
||||
type: 'batch_started',
|
||||
title: 'Lote Iniciado',
|
||||
description: 'Iniciado lote de croissants CR-024',
|
||||
metadata: {
|
||||
batchId: 'CR-024',
|
||||
product: 'Croissants',
|
||||
quantity: 48,
|
||||
expectedDuration: '2.5h'
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-26 08:45:00',
|
||||
category: 'inventory',
|
||||
type: 'stock_updated',
|
||||
title: 'Stock Actualizado',
|
||||
description: 'Repuesto stock de harina - Nivel: 50kg',
|
||||
metadata: {
|
||||
item: 'Harina de Trigo',
|
||||
previousLevel: '5kg',
|
||||
newLevel: '50kg',
|
||||
supplier: 'Molinos del Sur'
|
||||
},
|
||||
severity: 'success'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: '2024-01-26 07:30:00',
|
||||
category: 'system',
|
||||
type: 'user_login',
|
||||
title: 'Inicio de Sesión',
|
||||
description: 'Usuario admin ha iniciado sesión',
|
||||
metadata: {
|
||||
userId: 'admin',
|
||||
ipAddress: '192.168.1.100',
|
||||
userAgent: 'Chrome/120.0',
|
||||
location: 'Madrid, ES'
|
||||
},
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: '2024-01-25 19:20:00',
|
||||
category: 'sales',
|
||||
type: 'payment_processed',
|
||||
title: 'Pago Procesado',
|
||||
description: 'Pago de €45.80 procesado exitosamente',
|
||||
metadata: {
|
||||
amount: 45.80,
|
||||
method: 'Tarjeta',
|
||||
reference: 'PAY-789',
|
||||
customer: 'Juan Pérez'
|
||||
},
|
||||
severity: 'success'
|
||||
}
|
||||
];
|
||||
|
||||
const eventStats = {
|
||||
total: events.length,
|
||||
today: events.filter(e =>
|
||||
new Date(e.timestamp).toDateString() === new Date().toDateString()
|
||||
).length,
|
||||
sales: events.filter(e => e.category === 'sales').length,
|
||||
production: events.filter(e => e.category === 'production').length,
|
||||
system: events.filter(e => e.category === 'system').length
|
||||
};
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todos', count: events.length },
|
||||
{ value: 'sales', label: 'Ventas', count: eventStats.sales },
|
||||
{ value: 'production', label: 'Producción', count: eventStats.production },
|
||||
{ value: 'inventory', label: 'Inventario', count: events.filter(e => e.category === 'inventory').length },
|
||||
{ value: 'system', label: 'Sistema', count: eventStats.system }
|
||||
];
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'success': return 'green';
|
||||
case 'warning': return 'yellow';
|
||||
case 'error': return 'red';
|
||||
default: return 'blue';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
switch (category) {
|
||||
case 'sales': return <BarChart3 {...iconProps} />;
|
||||
case 'production': return <Activity {...iconProps} />;
|
||||
case 'inventory': return <Calendar {...iconProps} />;
|
||||
default: return <Activity {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEvents = selectedCategory === 'all'
|
||||
? events
|
||||
: events.filter(event => event.category === selectedCategory);
|
||||
|
||||
const formatTimeAgo = (timestamp: string) => {
|
||||
const now = new Date();
|
||||
const eventTime = new Date(timestamp);
|
||||
const diffInMs = now.getTime() - eventTime.getTime();
|
||||
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
|
||||
if (diffInDays > 0) {
|
||||
return `hace ${diffInDays}d`;
|
||||
} else if (diffInHours > 0) {
|
||||
return `hace ${diffInHours}h`;
|
||||
} else {
|
||||
return `hace ${Math.floor(diffInMs / (1000 * 60))}m`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Registro de Eventos"
|
||||
description="Seguimiento de todas las actividades y eventos del sistema"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros Avanzados
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Event Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Eventos</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{eventStats.total}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Hoy</p>
|
||||
<p className="text-3xl font-bold text-green-600">{eventStats.today}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Calendar className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Ventas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<BarChart3 className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Producción</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{eventStats.production}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="day">Hoy</option>
|
||||
<option value="week">Esta Semana</option>
|
||||
<option value="month">Este Mes</option>
|
||||
<option value="all">Todos</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.value}
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedCategory === category.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Events List */}
|
||||
<div className="space-y-4">
|
||||
{filteredEvents.map((event) => (
|
||||
<Card key={event.id} className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
|
||||
{getCategoryIcon(event.category)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{event.title}</h3>
|
||||
<Badge variant={getSeverityColor(event.severity)}>
|
||||
{event.category}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 mb-3">{event.description}</p>
|
||||
|
||||
{/* Event Metadata */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{Object.entries(event.metadata).map(([key, value]) => (
|
||||
<div key={key} className="bg-gray-50 p-3 rounded-lg">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-500">
|
||||
<span>{formatTimeAgo(event.timestamp)}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredEvents.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Activity className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay eventos</h3>
|
||||
<p className="text-gray-600">
|
||||
No se encontraron eventos para el período y categoría seleccionados.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventsPage;
|
||||
1
frontend/src/pages/app/data/events/index.ts
Normal file
1
frontend/src/pages/app/data/events/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as EventsPage } from './EventsPage';
|
||||
336
frontend/src/pages/app/data/traffic/TrafficPage.tsx
Normal file
336
frontend/src/pages/app/data/traffic/TrafficPage.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users, Clock, TrendingUp, MapPin, Calendar, BarChart3, Download, Filter } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TrafficPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
const [selectedMetric, setSelectedMetric] = useState('visitors');
|
||||
|
||||
const trafficData = {
|
||||
totalVisitors: 2847,
|
||||
peakHour: '12:00',
|
||||
averageVisitDuration: '23min',
|
||||
busyDays: ['Viernes', 'Sábado'],
|
||||
conversionRate: 68.4
|
||||
};
|
||||
|
||||
const hourlyTraffic = [
|
||||
{ hour: '07:00', visitors: 15, sales: 12, duration: '18min' },
|
||||
{ hour: '08:00', visitors: 32, sales: 24, duration: '22min' },
|
||||
{ hour: '09:00', visitors: 45, sales: 28, duration: '25min' },
|
||||
{ hour: '10:00', visitors: 38, sales: 25, duration: '24min' },
|
||||
{ hour: '11:00', visitors: 52, sales: 35, duration: '26min' },
|
||||
{ hour: '12:00', visitors: 78, sales: 54, duration: '28min' },
|
||||
{ hour: '13:00', visitors: 85, sales: 58, duration: '30min' },
|
||||
{ hour: '14:00', visitors: 62, sales: 42, duration: '27min' },
|
||||
{ hour: '15:00', visitors: 48, sales: 32, duration: '25min' },
|
||||
{ hour: '16:00', visitors: 55, sales: 38, duration: '26min' },
|
||||
{ hour: '17:00', visitors: 68, sales: 46, duration: '29min' },
|
||||
{ hour: '18:00', visitors: 74, sales: 52, duration: '31min' },
|
||||
{ hour: '19:00', visitors: 56, sales: 39, duration: '28min' },
|
||||
{ hour: '20:00', visitors: 28, sales: 18, duration: '22min' }
|
||||
];
|
||||
|
||||
const dailyTraffic = [
|
||||
{ day: 'Lun', visitors: 245, sales: 168, conversion: 68.6, avgDuration: '22min' },
|
||||
{ day: 'Mar', visitors: 289, sales: 195, conversion: 67.5, avgDuration: '24min' },
|
||||
{ day: 'Mié', visitors: 321, sales: 218, conversion: 67.9, avgDuration: '25min' },
|
||||
{ day: 'Jue', visitors: 356, sales: 242, conversion: 68.0, avgDuration: '26min' },
|
||||
{ day: 'Vie', visitors: 445, sales: 312, conversion: 70.1, avgDuration: '28min' },
|
||||
{ day: 'Sáb', visitors: 498, sales: 348, conversion: 69.9, avgDuration: '30min' },
|
||||
{ day: 'Dom', visitors: 398, sales: 265, conversion: 66.6, avgDuration: '27min' }
|
||||
];
|
||||
|
||||
const trafficSources = [
|
||||
{ source: 'Pie', visitors: 1245, percentage: 43.7, trend: 5.2 },
|
||||
{ source: 'Búsqueda Local', visitors: 687, percentage: 24.1, trend: 12.3 },
|
||||
{ source: 'Recomendaciones', visitors: 423, percentage: 14.9, trend: -2.1 },
|
||||
{ source: 'Redes Sociales', visitors: 298, percentage: 10.5, trend: 8.7 },
|
||||
{ source: 'Publicidad', visitors: 194, percentage: 6.8, trend: 15.4 }
|
||||
];
|
||||
|
||||
const customerSegments = [
|
||||
{
|
||||
segment: 'Regulares Matutinos',
|
||||
count: 145,
|
||||
percentage: 24.2,
|
||||
peakHours: ['07:00-09:00'],
|
||||
avgSpend: 12.50,
|
||||
frequency: 'Diaria'
|
||||
},
|
||||
{
|
||||
segment: 'Familia Fin de Semana',
|
||||
count: 198,
|
||||
percentage: 33.1,
|
||||
peakHours: ['10:00-13:00'],
|
||||
avgSpend: 28.90,
|
||||
frequency: 'Semanal'
|
||||
},
|
||||
{
|
||||
segment: 'Oficinistas Almuerzo',
|
||||
count: 112,
|
||||
percentage: 18.7,
|
||||
peakHours: ['12:00-14:00'],
|
||||
avgSpend: 8.75,
|
||||
frequency: '2-3x semana'
|
||||
},
|
||||
{
|
||||
segment: 'Clientes Ocasionales',
|
||||
count: 143,
|
||||
percentage: 23.9,
|
||||
peakHours: ['16:00-19:00'],
|
||||
avgSpend: 15.20,
|
||||
frequency: 'Mensual'
|
||||
}
|
||||
];
|
||||
|
||||
const getTrendColor = (trend: number) => {
|
||||
return trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]';
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: number) => {
|
||||
return trend >= 0 ? '↗' : '↘';
|
||||
};
|
||||
|
||||
const maxVisitors = Math.max(...hourlyTraffic.map(h => h.visitors));
|
||||
const maxDailyVisitors = Math.max(...dailyTraffic.map(d => d.visitors));
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Análisis de Tráfico"
|
||||
description="Monitorea los patrones de visitas y flujo de clientes"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Traffic Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Visitantes Totales</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-info)]">{trafficData.totalVisitors.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Hora Pico</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">{trafficData.peakHour}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Duración Promedio</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Conversión</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{trafficData.conversionRate}%</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<TrendingUp className="h-6 w-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Días Ocupados</p>
|
||||
<p className="text-sm font-bold text-[var(--color-error)]">{trafficData.busyDays.join(', ')}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
|
||||
<Calendar className="h-6 w-6 text-[var(--color-error)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="day">Hoy</option>
|
||||
<option value="week">Esta Semana</option>
|
||||
<option value="month">Este Mes</option>
|
||||
<option value="year">Este Año</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica</label>
|
||||
<select
|
||||
value={selectedMetric}
|
||||
onChange={(e) => setSelectedMetric(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="visitors">Visitantes</option>
|
||||
<option value="sales">Ventas</option>
|
||||
<option value="duration">Duración</option>
|
||||
<option value="conversion">Conversión</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Hourly Traffic */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico por Hora</h3>
|
||||
<div className="h-64 flex items-end space-x-1 justify-between">
|
||||
{hourlyTraffic.map((data, index) => (
|
||||
<div key={index} className="flex flex-col items-center flex-1">
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
|
||||
<div
|
||||
className="w-full bg-[var(--color-info)]/50 rounded-t"
|
||||
style={{
|
||||
height: `${(data.visitors / maxVisitors) * 200}px`,
|
||||
minHeight: '4px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
|
||||
{data.hour}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Daily Traffic */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico Semanal</h3>
|
||||
<div className="h-64 flex items-end space-x-2 justify-between">
|
||||
{dailyTraffic.map((data, index) => (
|
||||
<div key={index} className="flex flex-col items-center flex-1">
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
|
||||
<div
|
||||
className="w-full bg-green-500 rounded-t"
|
||||
style={{
|
||||
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
|
||||
minHeight: '8px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-sm text-[var(--text-secondary)] mt-2 font-medium">
|
||||
{data.day}
|
||||
</span>
|
||||
<div className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{data.conversion}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Traffic Sources */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Fuentes de Tráfico</h3>
|
||||
<div className="space-y-3">
|
||||
{trafficSources.map((source, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-3 h-3 bg-[var(--color-info)]/50 rounded-full"></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">{source.source}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">{source.visitors} visitantes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">{source.percentage}%</p>
|
||||
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
|
||||
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Customer Segments */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Segmentos de Clientes</h3>
|
||||
<div className="space-y-4">
|
||||
{customerSegments.map((segment, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{segment.segment}</h4>
|
||||
<Badge variant="blue">{segment.percentage}%</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-[var(--text-tertiary)]">Clientes</p>
|
||||
<p className="font-medium">{segment.count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[var(--text-tertiary)]">Gasto Promedio</p>
|
||||
<p className="font-medium">€{segment.avgSpend}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[var(--text-tertiary)]">Horario Pico</p>
|
||||
<p className="font-medium">{segment.peakHours.join(', ')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[var(--text-tertiary)]">Frecuencia</p>
|
||||
<p className="font-medium">{segment.frequency}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Traffic Heat Map placeholder */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Mapa de Calor - Zonas de la Panadería</h3>
|
||||
<div className="h-64 bg-[var(--bg-tertiary)] rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<MapPin className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<p className="text-[var(--text-secondary)]">Visualización de zonas de mayor tráfico</p>
|
||||
<p className="text-sm text-[var(--text-tertiary)] mt-1">Entrada: 45% • Mostrador: 32% • Zona sentada: 23%</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficPage;
|
||||
336
frontend/src/pages/app/data/traffic/TrafficPage.tsx.backup
Normal file
336
frontend/src/pages/app/data/traffic/TrafficPage.tsx.backup
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users, Clock, TrendingUp, MapPin, Calendar, BarChart3, Download, Filter } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TrafficPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
const [selectedMetric, setSelectedMetric] = useState('visitors');
|
||||
|
||||
const trafficData = {
|
||||
totalVisitors: 2847,
|
||||
peakHour: '12:00',
|
||||
averageVisitDuration: '23min',
|
||||
busyDays: ['Viernes', 'Sábado'],
|
||||
conversionRate: 68.4
|
||||
};
|
||||
|
||||
const hourlyTraffic = [
|
||||
{ hour: '07:00', visitors: 15, sales: 12, duration: '18min' },
|
||||
{ hour: '08:00', visitors: 32, sales: 24, duration: '22min' },
|
||||
{ hour: '09:00', visitors: 45, sales: 28, duration: '25min' },
|
||||
{ hour: '10:00', visitors: 38, sales: 25, duration: '24min' },
|
||||
{ hour: '11:00', visitors: 52, sales: 35, duration: '26min' },
|
||||
{ hour: '12:00', visitors: 78, sales: 54, duration: '28min' },
|
||||
{ hour: '13:00', visitors: 85, sales: 58, duration: '30min' },
|
||||
{ hour: '14:00', visitors: 62, sales: 42, duration: '27min' },
|
||||
{ hour: '15:00', visitors: 48, sales: 32, duration: '25min' },
|
||||
{ hour: '16:00', visitors: 55, sales: 38, duration: '26min' },
|
||||
{ hour: '17:00', visitors: 68, sales: 46, duration: '29min' },
|
||||
{ hour: '18:00', visitors: 74, sales: 52, duration: '31min' },
|
||||
{ hour: '19:00', visitors: 56, sales: 39, duration: '28min' },
|
||||
{ hour: '20:00', visitors: 28, sales: 18, duration: '22min' }
|
||||
];
|
||||
|
||||
const dailyTraffic = [
|
||||
{ day: 'Lun', visitors: 245, sales: 168, conversion: 68.6, avgDuration: '22min' },
|
||||
{ day: 'Mar', visitors: 289, sales: 195, conversion: 67.5, avgDuration: '24min' },
|
||||
{ day: 'Mié', visitors: 321, sales: 218, conversion: 67.9, avgDuration: '25min' },
|
||||
{ day: 'Jue', visitors: 356, sales: 242, conversion: 68.0, avgDuration: '26min' },
|
||||
{ day: 'Vie', visitors: 445, sales: 312, conversion: 70.1, avgDuration: '28min' },
|
||||
{ day: 'Sáb', visitors: 498, sales: 348, conversion: 69.9, avgDuration: '30min' },
|
||||
{ day: 'Dom', visitors: 398, sales: 265, conversion: 66.6, avgDuration: '27min' }
|
||||
];
|
||||
|
||||
const trafficSources = [
|
||||
{ source: 'Pie', visitors: 1245, percentage: 43.7, trend: 5.2 },
|
||||
{ source: 'Búsqueda Local', visitors: 687, percentage: 24.1, trend: 12.3 },
|
||||
{ source: 'Recomendaciones', visitors: 423, percentage: 14.9, trend: -2.1 },
|
||||
{ source: 'Redes Sociales', visitors: 298, percentage: 10.5, trend: 8.7 },
|
||||
{ source: 'Publicidad', visitors: 194, percentage: 6.8, trend: 15.4 }
|
||||
];
|
||||
|
||||
const customerSegments = [
|
||||
{
|
||||
segment: 'Regulares Matutinos',
|
||||
count: 145,
|
||||
percentage: 24.2,
|
||||
peakHours: ['07:00-09:00'],
|
||||
avgSpend: 12.50,
|
||||
frequency: 'Diaria'
|
||||
},
|
||||
{
|
||||
segment: 'Familia Fin de Semana',
|
||||
count: 198,
|
||||
percentage: 33.1,
|
||||
peakHours: ['10:00-13:00'],
|
||||
avgSpend: 28.90,
|
||||
frequency: 'Semanal'
|
||||
},
|
||||
{
|
||||
segment: 'Oficinistas Almuerzo',
|
||||
count: 112,
|
||||
percentage: 18.7,
|
||||
peakHours: ['12:00-14:00'],
|
||||
avgSpend: 8.75,
|
||||
frequency: '2-3x semana'
|
||||
},
|
||||
{
|
||||
segment: 'Clientes Ocasionales',
|
||||
count: 143,
|
||||
percentage: 23.9,
|
||||
peakHours: ['16:00-19:00'],
|
||||
avgSpend: 15.20,
|
||||
frequency: 'Mensual'
|
||||
}
|
||||
];
|
||||
|
||||
const getTrendColor = (trend: number) => {
|
||||
return trend >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: number) => {
|
||||
return trend >= 0 ? '↗' : '↘';
|
||||
};
|
||||
|
||||
const maxVisitors = Math.max(...hourlyTraffic.map(h => h.visitors));
|
||||
const maxDailyVisitors = Math.max(...dailyTraffic.map(d => d.visitors));
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Análisis de Tráfico"
|
||||
description="Monitorea los patrones de visitas y flujo de clientes"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Traffic Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Visitantes Totales</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{trafficData.totalVisitors.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Hora Pico</p>
|
||||
<p className="text-3xl font-bold text-green-600">{trafficData.peakHour}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Duración Promedio</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Conversión</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{trafficData.conversionRate}%</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<TrendingUp className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Días Ocupados</p>
|
||||
<p className="text-sm font-bold text-red-600">{trafficData.busyDays.join(', ')}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<Calendar className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Período</label>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="day">Hoy</option>
|
||||
<option value="week">Esta Semana</option>
|
||||
<option value="month">Este Mes</option>
|
||||
<option value="year">Este Año</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Métrica</label>
|
||||
<select
|
||||
value={selectedMetric}
|
||||
onChange={(e) => setSelectedMetric(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="visitors">Visitantes</option>
|
||||
<option value="sales">Ventas</option>
|
||||
<option value="duration">Duración</option>
|
||||
<option value="conversion">Conversión</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Hourly Traffic */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico por Hora</h3>
|
||||
<div className="h-64 flex items-end space-x-1 justify-between">
|
||||
{hourlyTraffic.map((data, index) => (
|
||||
<div key={index} className="flex flex-col items-center flex-1">
|
||||
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
|
||||
<div
|
||||
className="w-full bg-blue-500 rounded-t"
|
||||
style={{
|
||||
height: `${(data.visitors / maxVisitors) * 200}px`,
|
||||
minHeight: '4px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center">
|
||||
{data.hour}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Daily Traffic */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tráfico Semanal</h3>
|
||||
<div className="h-64 flex items-end space-x-2 justify-between">
|
||||
{dailyTraffic.map((data, index) => (
|
||||
<div key={index} className="flex flex-col items-center flex-1">
|
||||
<div className="text-xs text-gray-600 mb-1">{data.visitors}</div>
|
||||
<div
|
||||
className="w-full bg-green-500 rounded-t"
|
||||
style={{
|
||||
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
|
||||
minHeight: '8px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-sm text-gray-700 mt-2 font-medium">
|
||||
{data.day}
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{data.conversion}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Traffic Sources */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fuentes de Tráfico</h3>
|
||||
<div className="space-y-3">
|
||||
{trafficSources.map((source, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{source.source}</p>
|
||||
<p className="text-xs text-gray-500">{source.visitors} visitantes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-900">{source.percentage}%</p>
|
||||
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
|
||||
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Customer Segments */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Segmentos de Clientes</h3>
|
||||
<div className="space-y-4">
|
||||
{customerSegments.map((segment, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900">{segment.segment}</h4>
|
||||
<Badge variant="blue">{segment.percentage}%</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">Clientes</p>
|
||||
<p className="font-medium">{segment.count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Gasto Promedio</p>
|
||||
<p className="font-medium">€{segment.avgSpend}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Horario Pico</p>
|
||||
<p className="font-medium">{segment.peakHours.join(', ')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Frecuencia</p>
|
||||
<p className="font-medium">{segment.frequency}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Traffic Heat Map placeholder */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Mapa de Calor - Zonas de la Panadería</h3>
|
||||
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<MapPin className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">Visualización de zonas de mayor tráfico</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Entrada: 45% • Mostrador: 32% • Zona sentada: 23%</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficPage;
|
||||
1
frontend/src/pages/app/data/traffic/index.ts
Normal file
1
frontend/src/pages/app/data/traffic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as TrafficPage } from './TrafficPage';
|
||||
423
frontend/src/pages/app/data/weather/WeatherPage.tsx
Normal file
423
frontend/src/pages/app/data/weather/WeatherPage.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Cloud, Sun, CloudRain, Thermometer, Wind, Droplets, Calendar, TrendingUp } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const WeatherPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
|
||||
const currentWeather = {
|
||||
temperature: 18,
|
||||
condition: 'partly-cloudy',
|
||||
humidity: 65,
|
||||
windSpeed: 12,
|
||||
pressure: 1013,
|
||||
uvIndex: 4,
|
||||
visibility: 10,
|
||||
description: 'Parcialmente nublado'
|
||||
};
|
||||
|
||||
const forecast = [
|
||||
{
|
||||
date: '2024-01-27',
|
||||
day: 'Sábado',
|
||||
condition: 'sunny',
|
||||
tempMax: 22,
|
||||
tempMin: 12,
|
||||
humidity: 45,
|
||||
precipitation: 0,
|
||||
wind: 8,
|
||||
impact: 'high-demand',
|
||||
recommendation: 'Incrementar producción de helados y bebidas frías'
|
||||
},
|
||||
{
|
||||
date: '2024-01-28',
|
||||
day: 'Domingo',
|
||||
condition: 'partly-cloudy',
|
||||
tempMax: 19,
|
||||
tempMin: 11,
|
||||
humidity: 55,
|
||||
precipitation: 20,
|
||||
wind: 15,
|
||||
impact: 'normal',
|
||||
recommendation: 'Producción estándar'
|
||||
},
|
||||
{
|
||||
date: '2024-01-29',
|
||||
day: 'Lunes',
|
||||
condition: 'rainy',
|
||||
tempMax: 15,
|
||||
tempMin: 8,
|
||||
humidity: 85,
|
||||
precipitation: 80,
|
||||
wind: 22,
|
||||
impact: 'comfort-food',
|
||||
recommendation: 'Aumentar sopas, chocolates calientes y pan recién horneado'
|
||||
},
|
||||
{
|
||||
date: '2024-01-30',
|
||||
day: 'Martes',
|
||||
condition: 'cloudy',
|
||||
tempMax: 16,
|
||||
tempMin: 9,
|
||||
humidity: 70,
|
||||
precipitation: 40,
|
||||
wind: 18,
|
||||
impact: 'moderate',
|
||||
recommendation: 'Enfoque en productos de interior'
|
||||
},
|
||||
{
|
||||
date: '2024-01-31',
|
||||
day: 'Miércoles',
|
||||
condition: 'sunny',
|
||||
tempMax: 24,
|
||||
tempMin: 14,
|
||||
humidity: 40,
|
||||
precipitation: 0,
|
||||
wind: 10,
|
||||
impact: 'high-demand',
|
||||
recommendation: 'Incrementar productos frescos y ensaladas'
|
||||
}
|
||||
];
|
||||
|
||||
const weatherImpacts = [
|
||||
{
|
||||
condition: 'Día Soleado',
|
||||
icon: Sun,
|
||||
impact: 'Aumento del 25% en bebidas frías',
|
||||
recommendations: [
|
||||
'Incrementar producción de helados',
|
||||
'Más bebidas refrescantes',
|
||||
'Ensaladas y productos frescos',
|
||||
'Horario extendido de terraza'
|
||||
],
|
||||
color: 'yellow'
|
||||
},
|
||||
{
|
||||
condition: 'Día Lluvioso',
|
||||
icon: CloudRain,
|
||||
impact: 'Aumento del 40% en productos calientes',
|
||||
recommendations: [
|
||||
'Más sopas y caldos',
|
||||
'Chocolates calientes',
|
||||
'Pan recién horneado',
|
||||
'Productos de repostería'
|
||||
],
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
condition: 'Frío Intenso',
|
||||
icon: Thermometer,
|
||||
impact: 'Preferencia por comida reconfortante',
|
||||
recommendations: [
|
||||
'Aumentar productos horneados',
|
||||
'Bebidas calientes especiales',
|
||||
'Productos energéticos',
|
||||
'Promociones de interior'
|
||||
],
|
||||
color: 'purple'
|
||||
}
|
||||
];
|
||||
|
||||
const seasonalTrends = [
|
||||
{
|
||||
season: 'Primavera',
|
||||
period: 'Mar - May',
|
||||
trends: [
|
||||
'Aumento en productos frescos (+30%)',
|
||||
'Mayor demanda de ensaladas',
|
||||
'Bebidas naturales populares',
|
||||
'Horarios extendidos efectivos'
|
||||
],
|
||||
avgTemp: '15-20°C',
|
||||
impact: 'positive'
|
||||
},
|
||||
{
|
||||
season: 'Verano',
|
||||
period: 'Jun - Ago',
|
||||
trends: [
|
||||
'Pico de helados y granizados (+60%)',
|
||||
'Productos ligeros preferidos',
|
||||
'Horario matutino crítico',
|
||||
'Mayor tráfico de turistas'
|
||||
],
|
||||
avgTemp: '25-35°C',
|
||||
impact: 'high'
|
||||
},
|
||||
{
|
||||
season: 'Otoño',
|
||||
period: 'Sep - Nov',
|
||||
trends: [
|
||||
'Regreso a productos tradicionales',
|
||||
'Aumento en bollería (+20%)',
|
||||
'Bebidas calientes populares',
|
||||
'Horarios regulares'
|
||||
],
|
||||
avgTemp: '10-18°C',
|
||||
impact: 'stable'
|
||||
},
|
||||
{
|
||||
season: 'Invierno',
|
||||
period: 'Dec - Feb',
|
||||
trends: [
|
||||
'Máximo de productos calientes (+50%)',
|
||||
'Pan recién horneado crítico',
|
||||
'Chocolates y dulces festivos',
|
||||
'Menor tráfico general (-15%)'
|
||||
],
|
||||
avgTemp: '5-12°C',
|
||||
impact: 'comfort'
|
||||
}
|
||||
];
|
||||
|
||||
const getWeatherIcon = (condition: string) => {
|
||||
const iconProps = { className: "w-8 h-8" };
|
||||
switch (condition) {
|
||||
case 'sunny': return <Sun {...iconProps} className="w-8 h-8 text-yellow-500" />;
|
||||
case 'partly-cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-[var(--text-tertiary)]" />;
|
||||
case 'cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-[var(--text-secondary)]" />;
|
||||
case 'rainy': return <CloudRain {...iconProps} className="w-8 h-8 text-blue-500" />;
|
||||
default: return <Cloud {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getConditionLabel = (condition: string) => {
|
||||
switch (condition) {
|
||||
case 'sunny': return 'Soleado';
|
||||
case 'partly-cloudy': return 'Parcialmente nublado';
|
||||
case 'cloudy': return 'Nublado';
|
||||
case 'rainy': return 'Lluvioso';
|
||||
default: return condition;
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactColor = (impact: string) => {
|
||||
switch (impact) {
|
||||
case 'high-demand': return 'green';
|
||||
case 'comfort-food': return 'orange';
|
||||
case 'moderate': return 'blue';
|
||||
case 'normal': return 'gray';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactLabel = (impact: string) => {
|
||||
switch (impact) {
|
||||
case 'high-demand': return 'Alta Demanda';
|
||||
case 'comfort-food': return 'Comida Reconfortante';
|
||||
case 'moderate': return 'Demanda Moderada';
|
||||
case 'normal': return 'Demanda Normal';
|
||||
default: return impact;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Datos Meteorológicos"
|
||||
description="Integra información del clima para optimizar la producción y ventas"
|
||||
/>
|
||||
|
||||
{/* Current Weather */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Condiciones Actuales</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{getWeatherIcon(currentWeather.condition)}
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{currentWeather.temperature}°C</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{currentWeather.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Droplets className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Humedad: {currentWeather.humidity}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Wind className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Viento: {currentWeather.windSpeed} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
<span className="font-medium">Presión:</span> {currentWeather.pressure} hPa
|
||||
</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
<span className="font-medium">UV:</span> {currentWeather.uvIndex}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
<span className="font-medium">Visibilidad:</span> {currentWeather.visibility} km
|
||||
</div>
|
||||
<Badge variant="blue">Condiciones favorables</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Weather Forecast */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Pronóstico Extendido</h3>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
|
||||
>
|
||||
<option value="week">Próxima Semana</option>
|
||||
<option value="month">Próximo Mes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{forecast.map((day, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="text-center mb-3">
|
||||
<p className="font-medium text-[var(--text-primary)]">{day.day}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">{new Date(day.date).toLocaleDateString('es-ES')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-3">
|
||||
{getWeatherIcon(day.condition)}
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-3">
|
||||
<p className="text-sm text-[var(--text-secondary)]">{getConditionLabel(day.condition)}</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{day.tempMax}° <span className="text-sm text-[var(--text-tertiary)]">/ {day.tempMin}°</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs text-[var(--text-secondary)]">
|
||||
<div className="flex justify-between">
|
||||
<span>Humedad:</span>
|
||||
<span>{day.humidity}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Lluvia:</span>
|
||||
<span>{day.precipitation}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Viento:</span>
|
||||
<span>{day.wind} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Badge variant={getImpactColor(day.impact)} className="text-xs">
|
||||
{getImpactLabel(day.impact)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-[var(--text-secondary)]">{day.recommendation}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Weather Impact Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Impacto del Clima</h3>
|
||||
<div className="space-y-4">
|
||||
{weatherImpacts.map((impact, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div className={`p-2 rounded-lg bg-${impact.color}-100`}>
|
||||
<impact.icon className={`w-5 h-5 text-${impact.color}-600`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{impact.condition}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{impact.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10">
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">Recomendaciones:</p>
|
||||
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||
{impact.recommendations.map((rec, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full mr-2"></span>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Seasonal Trends */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tendencias Estacionales</h3>
|
||||
<div className="space-y-4">
|
||||
{seasonalTrends.map((season, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{season.season}</h4>
|
||||
<p className="text-sm text-[var(--text-tertiary)]">{season.period}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">{season.avgTemp}</p>
|
||||
<Badge variant={
|
||||
season.impact === 'high' ? 'green' :
|
||||
season.impact === 'positive' ? 'blue' :
|
||||
season.impact === 'comfort' ? 'orange' : 'gray'
|
||||
}>
|
||||
{season.impact === 'high' ? 'Alto' :
|
||||
season.impact === 'positive' ? 'Positivo' :
|
||||
season.impact === 'comfort' ? 'Confort' : 'Estable'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||
{season.trends.map((trend, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<TrendingUp className="w-3 h-3 mr-2 text-green-500" />
|
||||
{trend}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Weather Alerts */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Meteorológicas</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
|
||||
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
|
||||
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
|
||||
<CloudRain className="w-5 h-5 text-[var(--color-info)] mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--color-info)]">Lluvia intensa el lunes</p>
|
||||
<p className="text-sm text-[var(--color-info)]">80% probabilidad de precipitación con vientos fuertes</p>
|
||||
<p className="text-xs text-[var(--color-info)] mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeatherPage;
|
||||
423
frontend/src/pages/app/data/weather/WeatherPage.tsx.backup
Normal file
423
frontend/src/pages/app/data/weather/WeatherPage.tsx.backup
Normal file
@@ -0,0 +1,423 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Cloud, Sun, CloudRain, Thermometer, Wind, Droplets, Calendar, TrendingUp } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const WeatherPage: React.FC = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('week');
|
||||
|
||||
const currentWeather = {
|
||||
temperature: 18,
|
||||
condition: 'partly-cloudy',
|
||||
humidity: 65,
|
||||
windSpeed: 12,
|
||||
pressure: 1013,
|
||||
uvIndex: 4,
|
||||
visibility: 10,
|
||||
description: 'Parcialmente nublado'
|
||||
};
|
||||
|
||||
const forecast = [
|
||||
{
|
||||
date: '2024-01-27',
|
||||
day: 'Sábado',
|
||||
condition: 'sunny',
|
||||
tempMax: 22,
|
||||
tempMin: 12,
|
||||
humidity: 45,
|
||||
precipitation: 0,
|
||||
wind: 8,
|
||||
impact: 'high-demand',
|
||||
recommendation: 'Incrementar producción de helados y bebidas frías'
|
||||
},
|
||||
{
|
||||
date: '2024-01-28',
|
||||
day: 'Domingo',
|
||||
condition: 'partly-cloudy',
|
||||
tempMax: 19,
|
||||
tempMin: 11,
|
||||
humidity: 55,
|
||||
precipitation: 20,
|
||||
wind: 15,
|
||||
impact: 'normal',
|
||||
recommendation: 'Producción estándar'
|
||||
},
|
||||
{
|
||||
date: '2024-01-29',
|
||||
day: 'Lunes',
|
||||
condition: 'rainy',
|
||||
tempMax: 15,
|
||||
tempMin: 8,
|
||||
humidity: 85,
|
||||
precipitation: 80,
|
||||
wind: 22,
|
||||
impact: 'comfort-food',
|
||||
recommendation: 'Aumentar sopas, chocolates calientes y pan recién horneado'
|
||||
},
|
||||
{
|
||||
date: '2024-01-30',
|
||||
day: 'Martes',
|
||||
condition: 'cloudy',
|
||||
tempMax: 16,
|
||||
tempMin: 9,
|
||||
humidity: 70,
|
||||
precipitation: 40,
|
||||
wind: 18,
|
||||
impact: 'moderate',
|
||||
recommendation: 'Enfoque en productos de interior'
|
||||
},
|
||||
{
|
||||
date: '2024-01-31',
|
||||
day: 'Miércoles',
|
||||
condition: 'sunny',
|
||||
tempMax: 24,
|
||||
tempMin: 14,
|
||||
humidity: 40,
|
||||
precipitation: 0,
|
||||
wind: 10,
|
||||
impact: 'high-demand',
|
||||
recommendation: 'Incrementar productos frescos y ensaladas'
|
||||
}
|
||||
];
|
||||
|
||||
const weatherImpacts = [
|
||||
{
|
||||
condition: 'Día Soleado',
|
||||
icon: Sun,
|
||||
impact: 'Aumento del 25% en bebidas frías',
|
||||
recommendations: [
|
||||
'Incrementar producción de helados',
|
||||
'Más bebidas refrescantes',
|
||||
'Ensaladas y productos frescos',
|
||||
'Horario extendido de terraza'
|
||||
],
|
||||
color: 'yellow'
|
||||
},
|
||||
{
|
||||
condition: 'Día Lluvioso',
|
||||
icon: CloudRain,
|
||||
impact: 'Aumento del 40% en productos calientes',
|
||||
recommendations: [
|
||||
'Más sopas y caldos',
|
||||
'Chocolates calientes',
|
||||
'Pan recién horneado',
|
||||
'Productos de repostería'
|
||||
],
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
condition: 'Frío Intenso',
|
||||
icon: Thermometer,
|
||||
impact: 'Preferencia por comida reconfortante',
|
||||
recommendations: [
|
||||
'Aumentar productos horneados',
|
||||
'Bebidas calientes especiales',
|
||||
'Productos energéticos',
|
||||
'Promociones de interior'
|
||||
],
|
||||
color: 'purple'
|
||||
}
|
||||
];
|
||||
|
||||
const seasonalTrends = [
|
||||
{
|
||||
season: 'Primavera',
|
||||
period: 'Mar - May',
|
||||
trends: [
|
||||
'Aumento en productos frescos (+30%)',
|
||||
'Mayor demanda de ensaladas',
|
||||
'Bebidas naturales populares',
|
||||
'Horarios extendidos efectivos'
|
||||
],
|
||||
avgTemp: '15-20°C',
|
||||
impact: 'positive'
|
||||
},
|
||||
{
|
||||
season: 'Verano',
|
||||
period: 'Jun - Ago',
|
||||
trends: [
|
||||
'Pico de helados y granizados (+60%)',
|
||||
'Productos ligeros preferidos',
|
||||
'Horario matutino crítico',
|
||||
'Mayor tráfico de turistas'
|
||||
],
|
||||
avgTemp: '25-35°C',
|
||||
impact: 'high'
|
||||
},
|
||||
{
|
||||
season: 'Otoño',
|
||||
period: 'Sep - Nov',
|
||||
trends: [
|
||||
'Regreso a productos tradicionales',
|
||||
'Aumento en bollería (+20%)',
|
||||
'Bebidas calientes populares',
|
||||
'Horarios regulares'
|
||||
],
|
||||
avgTemp: '10-18°C',
|
||||
impact: 'stable'
|
||||
},
|
||||
{
|
||||
season: 'Invierno',
|
||||
period: 'Dec - Feb',
|
||||
trends: [
|
||||
'Máximo de productos calientes (+50%)',
|
||||
'Pan recién horneado crítico',
|
||||
'Chocolates y dulces festivos',
|
||||
'Menor tráfico general (-15%)'
|
||||
],
|
||||
avgTemp: '5-12°C',
|
||||
impact: 'comfort'
|
||||
}
|
||||
];
|
||||
|
||||
const getWeatherIcon = (condition: string) => {
|
||||
const iconProps = { className: "w-8 h-8" };
|
||||
switch (condition) {
|
||||
case 'sunny': return <Sun {...iconProps} className="w-8 h-8 text-yellow-500" />;
|
||||
case 'partly-cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-400" />;
|
||||
case 'cloudy': return <Cloud {...iconProps} className="w-8 h-8 text-gray-600" />;
|
||||
case 'rainy': return <CloudRain {...iconProps} className="w-8 h-8 text-blue-500" />;
|
||||
default: return <Cloud {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getConditionLabel = (condition: string) => {
|
||||
switch (condition) {
|
||||
case 'sunny': return 'Soleado';
|
||||
case 'partly-cloudy': return 'Parcialmente nublado';
|
||||
case 'cloudy': return 'Nublado';
|
||||
case 'rainy': return 'Lluvioso';
|
||||
default: return condition;
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactColor = (impact: string) => {
|
||||
switch (impact) {
|
||||
case 'high-demand': return 'green';
|
||||
case 'comfort-food': return 'orange';
|
||||
case 'moderate': return 'blue';
|
||||
case 'normal': return 'gray';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactLabel = (impact: string) => {
|
||||
switch (impact) {
|
||||
case 'high-demand': return 'Alta Demanda';
|
||||
case 'comfort-food': return 'Comida Reconfortante';
|
||||
case 'moderate': return 'Demanda Moderada';
|
||||
case 'normal': return 'Demanda Normal';
|
||||
default: return impact;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Datos Meteorológicos"
|
||||
description="Integra información del clima para optimizar la producción y ventas"
|
||||
/>
|
||||
|
||||
{/* Current Weather */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Condiciones Actuales</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{getWeatherIcon(currentWeather.condition)}
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-gray-900">{currentWeather.temperature}°C</p>
|
||||
<p className="text-sm text-gray-600">{currentWeather.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Droplets className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm text-gray-600">Humedad: {currentWeather.humidity}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Wind className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm text-gray-600">Viento: {currentWeather.windSpeed} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Presión:</span> {currentWeather.pressure} hPa
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">UV:</span> {currentWeather.uvIndex}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Visibilidad:</span> {currentWeather.visibility} km
|
||||
</div>
|
||||
<Badge variant="blue">Condiciones favorables</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Weather Forecast */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Pronóstico Extendido</h3>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="week">Próxima Semana</option>
|
||||
<option value="month">Próximo Mes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{forecast.map((day, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="text-center mb-3">
|
||||
<p className="font-medium text-gray-900">{day.day}</p>
|
||||
<p className="text-xs text-gray-500">{new Date(day.date).toLocaleDateString('es-ES')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-3">
|
||||
{getWeatherIcon(day.condition)}
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-3">
|
||||
<p className="text-sm text-gray-600">{getConditionLabel(day.condition)}</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{day.tempMax}° <span className="text-sm text-gray-500">/ {day.tempMin}°</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Humedad:</span>
|
||||
<span>{day.humidity}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Lluvia:</span>
|
||||
<span>{day.precipitation}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Viento:</span>
|
||||
<span>{day.wind} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Badge variant={getImpactColor(day.impact)} className="text-xs">
|
||||
{getImpactLabel(day.impact)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-gray-600">{day.recommendation}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Weather Impact Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Impacto del Clima</h3>
|
||||
<div className="space-y-4">
|
||||
{weatherImpacts.map((impact, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div className={`p-2 rounded-lg bg-${impact.color}-100`}>
|
||||
<impact.icon className={`w-5 h-5 text-${impact.color}-600`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{impact.condition}</h4>
|
||||
<p className="text-sm text-gray-600">{impact.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Recomendaciones:</p>
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
{impact.recommendations.map((rec, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full mr-2"></span>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Seasonal Trends */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tendencias Estacionales</h3>
|
||||
<div className="space-y-4">
|
||||
{seasonalTrends.map((season, index) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{season.season}</h4>
|
||||
<p className="text-sm text-gray-500">{season.period}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-700">{season.avgTemp}</p>
|
||||
<Badge variant={
|
||||
season.impact === 'high' ? 'green' :
|
||||
season.impact === 'positive' ? 'blue' :
|
||||
season.impact === 'comfort' ? 'orange' : 'gray'
|
||||
}>
|
||||
{season.impact === 'high' ? 'Alto' :
|
||||
season.impact === 'positive' ? 'Positivo' :
|
||||
season.impact === 'comfort' ? 'Confort' : 'Estable'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
{season.trends.map((trend, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<TrendingUp className="w-3 h-3 mr-2 text-green-500" />
|
||||
{trend}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Weather Alerts */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alertas Meteorológicas</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
|
||||
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
|
||||
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<CloudRain className="w-5 h-5 text-blue-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-800">Lluvia intensa el lunes</p>
|
||||
<p className="text-sm text-blue-700">80% probabilidad de precipitación con vientos fuertes</p>
|
||||
<p className="text-xs text-blue-600 mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeatherPage;
|
||||
1
frontend/src/pages/app/data/weather/index.ts
Normal file
1
frontend/src/pages/app/data/weather/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as WeatherPage } from './WeatherPage';
|
||||
Reference in New Issue
Block a user