Improve the frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-02 20:24:44 +01:00
parent 0220da1725
commit 5adb0e39c0
90 changed files with 10658 additions and 2548 deletions

View File

@@ -6,9 +6,7 @@ import {
Target,
DollarSign,
Award,
Lock,
BarChart3,
Package,
Truck,
Calendar
} from 'lucide-react';
@@ -22,8 +20,7 @@ import {
ResponsiveContainer,
Legend
} from 'recharts';
import { PageHeader } from '../../../components/layout';
import { Card, StatsGrid, Button, Tabs } from '../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../components/analytics';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProcurementDashboard, useProcurementTrends } from '../../../api/hooks/procurement';
@@ -42,54 +39,6 @@ const ProcurementAnalyticsPage: React.FC = () => {
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
// Show loading state while subscription data is being fetched
if (subscriptionInfo.loading) {
return (
<div className="space-y-6">
<PageHeader
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
/>
<Card className="p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
<p className="text-[var(--text-secondary)]">Cargando información de suscripción...</p>
</div>
</Card>
</div>
);
}
// If user doesn't have access to advanced analytics, show upgrade message
if (!hasAdvancedAccess) {
return (
<div className="space-y-6">
<PageHeader
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
/>
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Funcionalidad Exclusiva para Profesionales y Empresas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
La analítica avanzada de compras está disponible solo para planes Professional y Enterprise.
Actualiza tu plan para acceder a análisis detallados de proveedores, optimización de costos y métricas de rendimiento.
</p>
<Button
variant="primary"
size="md"
onClick={() => window.location.hash = '#/app/settings/profile'}
>
Actualizar Plan
</Button>
</Card>
</div>
);
}
// Tab configuration
const tabs = [
{
@@ -120,65 +69,50 @@ const ProcurementAnalyticsPage: React.FC = () => {
];
return (
<div className="space-y-6">
<PageHeader
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
/>
{/* Summary Stats */}
<StatsGrid
stats={[
{
label: 'Planes Activos',
value: dashboard?.summary?.total_plans || 0,
icon: ShoppingCart,
formatter: formatters.number
},
{
label: 'Tasa de Cumplimiento',
value: dashboard?.performance_metrics?.average_fulfillment_rate || 0,
icon: Target,
formatter: formatters.percentage,
change: dashboard?.performance_metrics?.fulfillment_trend
},
{
label: 'Entregas a Tiempo',
value: dashboard?.performance_metrics?.average_on_time_delivery || 0,
icon: Calendar,
formatter: formatters.percentage,
change: dashboard?.performance_metrics?.on_time_trend
},
{
label: 'Variación de Costos',
value: dashboard?.performance_metrics?.cost_accuracy || 0,
icon: DollarSign,
formatter: formatters.percentage,
change: dashboard?.performance_metrics?.cost_variance_trend
}
]}
loading={dashboardLoading}
/>
{/* Tabs */}
<Tabs
items={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{/* Tab Content */}
<div className="space-y-6">
<AnalyticsPageLayout
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
subscriptionLoading={subscriptionInfo.loading}
hasAccess={hasAdvancedAccess}
dataLoading={dashboardLoading}
stats={[
{
title: 'Planes Activos',
value: dashboard?.summary?.total_plans || 0,
icon: ShoppingCart,
formatter: formatters.number
},
{
title: 'Tasa de Cumplimiento',
value: dashboard?.performance_metrics?.average_fulfillment_rate || 0,
icon: Target,
formatter: formatters.percentage
},
{
title: 'Entregas a Tiempo',
value: dashboard?.performance_metrics?.average_on_time_delivery || 0,
icon: Calendar,
formatter: formatters.percentage
},
{
title: 'Variación de Costos',
value: dashboard?.performance_metrics?.cost_accuracy || 0,
icon: DollarSign,
formatter: formatters.percentage
}
]}
statsColumns={4}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
showMobileNotice={true}
>
{activeTab === 'overview' && (
<>
{/* Overview Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Plan Status Distribution */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Distribución de Estados de Planes
</h3>
<AnalyticsCard title="Distribución de Estados de Planes">
<div className="space-y-3">
{dashboard?.plan_status_distribution?.map((status: any) => (
<div key={status.status} className="flex items-center justify-between">
@@ -197,18 +131,13 @@ const ProcurementAnalyticsPage: React.FC = () => {
</div>
))}
</div>
</div>
</Card>
</AnalyticsCard>
{/* Critical Requirements */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-[var(--text-primary)]">
Requerimientos Críticos
</h3>
<AlertCircle className="h-5 w-5 text-[var(--color-error)]" />
</div>
<AnalyticsCard
title="Requerimientos Críticos"
actions={<AlertCircle className="h-5 w-5 text-[var(--color-error)]" />}
>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Stock Crítico</span>
@@ -229,16 +158,11 @@ const ProcurementAnalyticsPage: React.FC = () => {
</span>
</div>
</div>
</div>
</Card>
</AnalyticsCard>
</div>
{/* Recent Plans */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Planes Recientes
</h3>
<AnalyticsCard title="Planes Recientes">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
@@ -273,8 +197,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
</tbody>
</table>
</div>
</div>
</Card>
</AnalyticsCard>
</>
)}
@@ -282,50 +205,214 @@ const ProcurementAnalyticsPage: React.FC = () => {
<>
{/* Performance Tab */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<div className="p-6 text-center">
<AnalyticsCard>
<div className="text-center">
<Target className="mx-auto h-8 w-8 text-[var(--color-success)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{formatters.percentage(dashboard?.performance_metrics?.average_fulfillment_rate || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Tasa de Cumplimiento</div>
</div>
</Card>
</AnalyticsCard>
<Card>
<div className="p-6 text-center">
<AnalyticsCard>
<div className="text-center">
<Calendar className="mx-auto h-8 w-8 text-[var(--color-info)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{formatters.percentage(dashboard?.performance_metrics?.average_on_time_delivery || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Entregas a Tiempo</div>
</div>
</Card>
</AnalyticsCard>
<Card>
<div className="p-6 text-center">
<AnalyticsCard>
<div className="text-center">
<Award className="mx-auto h-8 w-8 text-[var(--color-warning)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{dashboard?.performance_metrics?.supplier_performance?.toFixed(1) || '0.0'}
</div>
<div className="text-sm text-[var(--text-secondary)]">Puntuación de Calidad</div>
</div>
</Card>
</AnalyticsCard>
</div>
{/* Performance Trend Chart */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencias de Rendimiento (Últimos 7 días)
</h3>
{trendsLoading ? (
<div className="h-64 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
<AnalyticsCard title="Tendencias de Rendimiento (Últimos 7 días)" loading={trendsLoading}>
{trends && trends.performance_trend && trends.performance_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trends.performance_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
<XAxis
dataKey="date"
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
/>
<YAxis
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
tickFormatter={(value) => `${(value * 100).toFixed(0)}%`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--bg-primary)',
border: '1px solid var(--border-primary)',
borderRadius: '8px'
}}
formatter={(value: any) => `${(value * 100).toFixed(1)}%`}
labelStyle={{ color: 'var(--text-primary)' }}
/>
<Legend />
<Line
type="monotone"
dataKey="fulfillment_rate"
stroke="var(--color-success)"
strokeWidth={2}
name="Tasa de Cumplimiento"
dot={{ fill: 'var(--color-success)' }}
/>
<Line
type="monotone"
dataKey="on_time_rate"
stroke="var(--color-info)"
strokeWidth={2}
name="Entregas a Tiempo"
dot={{ fill: 'var(--color-info)' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de tendencias disponibles
</div>
)}
</AnalyticsCard>
</>
)}
{activeTab === 'suppliers' && (
<>
{/* Suppliers Tab */}
<AnalyticsCard title="Rendimiento de Proveedores">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[var(--border-primary)]">
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Proveedor</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Órdenes</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Tasa Cumplimiento</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Entregas a Tiempo</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Calidad</th>
</tr>
</thead>
<tbody>
{dashboard?.supplier_performance?.map((supplier: any) => (
<tr key={supplier.id} className="border-b border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]">
<td className="py-3 px-4 text-[var(--text-primary)]">{supplier.name}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">{supplier.total_orders}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.fulfillment_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.on_time_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{supplier.quality_score?.toFixed(1) || 'N/A'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</AnalyticsCard>
</>
)}
{activeTab === 'costs' && (
<>
{/* Costs Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AnalyticsCard title="Análisis de Costos">
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Estimado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.summary?.total_estimated_cost || 0)}
</span>
</div>
) : trends && trends.performance_trend && trends.performance_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trends.performance_trend}>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Aprobado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.summary?.total_approved_cost || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Variación Promedio</span>
<span className={`text-2xl font-bold ${
(dashboard?.summary?.cost_variance || 0) > 0
? 'text-[var(--color-error)]'
: 'text-[var(--color-success)]'
}`}>
{formatters.currency(Math.abs(dashboard?.summary?.cost_variance || 0))}
</span>
</div>
</div>
</AnalyticsCard>
<AnalyticsCard title="Distribución de Costos por Categoría">
<div className="space-y-3">
{dashboard?.cost_by_category?.map((category: any) => (
<div key={category.name} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{category.name}</span>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)]"
style={{ width: `${(category.amount / (dashboard?.summary?.total_estimated_cost || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-20 text-right">
{formatters.currency(category.amount)}
</span>
</div>
</div>
))}
</div>
</AnalyticsCard>
</div>
</>
)}
{activeTab === 'quality' && (
<>
{/* Quality Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AnalyticsCard title="Métricas de Calidad">
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Puntuación Promedio</span>
<span className="text-3xl font-bold text-[var(--text-primary)]">
{dashboard?.quality_metrics?.avg_score?.toFixed(1) || '0.0'} / 10
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Alta</span>
<span className="text-2xl font-bold text-[var(--color-success)]">
{dashboard?.quality_metrics?.high_quality_count || 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Baja</span>
<span className="text-2xl font-bold text-[var(--color-error)]">
{dashboard?.quality_metrics?.low_quality_count || 0}
</span>
</div>
</div>
</AnalyticsCard>
<AnalyticsCard title="Tendencia de Calidad (Últimos 7 días)" loading={trendsLoading}>
{trends && trends.quality_trend && trends.quality_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={trends.quality_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
<XAxis
dataKey="date"
@@ -335,7 +422,8 @@ const ProcurementAnalyticsPage: React.FC = () => {
<YAxis
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
tickFormatter={(value) => `${(value * 100).toFixed(0)}%`}
domain={[0, 10]}
ticks={[0, 2, 4, 6, 8, 10]}
/>
<Tooltip
contentStyle={{
@@ -343,233 +431,29 @@ const ProcurementAnalyticsPage: React.FC = () => {
border: '1px solid var(--border-primary)',
borderRadius: '8px'
}}
formatter={(value: any) => `${(value * 100).toFixed(1)}%`}
formatter={(value: any) => `${value.toFixed(1)} / 10`}
labelStyle={{ color: 'var(--text-primary)' }}
/>
<Legend />
<Line
type="monotone"
dataKey="fulfillment_rate"
stroke="var(--color-success)"
dataKey="quality_score"
stroke="var(--color-warning)"
strokeWidth={2}
name="Tasa de Cumplimiento"
dot={{ fill: 'var(--color-success)' }}
/>
<Line
type="monotone"
dataKey="on_time_rate"
stroke="var(--color-info)"
strokeWidth={2}
name="Entregas a Tiempo"
dot={{ fill: 'var(--color-info)' }}
name="Puntuación de Calidad"
dot={{ fill: 'var(--color-warning)' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de tendencias disponibles
<div className="h-48 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de calidad disponibles
</div>
)}
</div>
</Card>
</>
)}
{activeTab === 'suppliers' && (
<>
{/* Suppliers Tab */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Rendimiento de Proveedores
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[var(--border-primary)]">
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Proveedor</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Órdenes</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Tasa Cumplimiento</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Entregas a Tiempo</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Calidad</th>
</tr>
</thead>
<tbody>
{dashboard?.supplier_performance?.map((supplier: any) => (
<tr key={supplier.id} className="border-b border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]">
<td className="py-3 px-4 text-[var(--text-primary)]">{supplier.name}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">{supplier.total_orders}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.fulfillment_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.on_time_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{supplier.quality_score?.toFixed(1) || 'N/A'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</>
)}
{activeTab === 'costs' && (
<>
{/* Costs Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Análisis de Costos
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Estimado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.summary?.total_estimated_cost || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Aprobado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.summary?.total_approved_cost || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Variación Promedio</span>
<span className={`text-2xl font-bold ${
(dashboard?.summary?.cost_variance || 0) > 0
? 'text-[var(--color-error)]'
: 'text-[var(--color-success)]'
}`}>
{formatters.currency(Math.abs(dashboard?.summary?.cost_variance || 0))}
</span>
</div>
</div>
</div>
</Card>
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Distribución de Costos por Categoría
</h3>
<div className="space-y-3">
{dashboard?.cost_by_category?.map((category: any) => (
<div key={category.name} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{category.name}</span>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)]"
style={{ width: `${(category.amount / (dashboard?.summary?.total_estimated_cost || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-20 text-right">
{formatters.currency(category.amount)}
</span>
</div>
</div>
))}
</div>
</div>
</Card>
</AnalyticsCard>
</div>
</>
)}
{activeTab === 'quality' && (
<>
{/* Quality Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Métricas de Calidad
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Puntuación Promedio</span>
<span className="text-3xl font-bold text-[var(--text-primary)]">
{dashboard?.quality_metrics?.avg_score?.toFixed(1) || '0.0'} / 10
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Alta</span>
<span className="text-2xl font-bold text-[var(--color-success)]">
{dashboard?.quality_metrics?.high_quality_count || 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Baja</span>
<span className="text-2xl font-bold text-[var(--color-error)]">
{dashboard?.quality_metrics?.low_quality_count || 0}
</span>
</div>
</div>
</div>
</Card>
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencia de Calidad (Últimos 7 días)
</h3>
{trendsLoading ? (
<div className="h-48 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
</div>
) : trends && trends.quality_trend && trends.quality_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={trends.quality_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
<XAxis
dataKey="date"
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
/>
<YAxis
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
domain={[0, 10]}
ticks={[0, 2, 4, 6, 8, 10]}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--bg-primary)',
border: '1px solid var(--border-primary)',
borderRadius: '8px'
}}
formatter={(value: any) => `${value.toFixed(1)} / 10`}
labelStyle={{ color: 'var(--text-primary)' }}
/>
<Line
type="monotone"
dataKey="quality_score"
stroke="var(--color-warning)"
strokeWidth={2}
name="Puntuación de Calidad"
dot={{ fill: 'var(--color-warning)' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-48 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de calidad disponibles
</div>
)}
</div>
</Card>
</div>
</>
)}
</div>
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -6,14 +6,12 @@ import {
Award,
Settings,
Brain,
Lock,
BarChart3,
TrendingUp,
Target,
Zap
} from 'lucide-react';
import { PageHeader } from '../../../components/layout';
import { Card, StatsGrid, Button, Tabs } from '../../../components/ui';
import { AnalyticsPageLayout } from '../../../components/analytics';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProductionDashboard } from '../../../api/hooks/production';
@@ -49,53 +47,6 @@ const ProductionAnalyticsPage: React.FC = () => {
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
// Show loading state while subscription data is being fetched
if (subscriptionInfo.loading) {
return (
<div className="space-y-6">
<PageHeader
title={t('analytics.production_analytics')}
description={t('analytics.advanced_insights_professionals_enterprises')}
/>
<Card className="p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
<p className="text-[var(--text-secondary)]">{t('common.loading') || 'Cargando información de suscripción...'}</p>
</div>
</Card>
</div>
);
}
// If user doesn't have access to advanced analytics, show upgrade message
if (!hasAdvancedAccess) {
return (
<div className="space-y-6">
<PageHeader
title={t('analytics.production_analytics')}
description={t('analytics.advanced_insights_professionals_enterprises')}
/>
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
{t('subscription.exclusive_professional_enterprise')}
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{t('subscription.advanced_production_analytics_description')}
</p>
<Button
variant="primary"
size="md"
onClick={() => window.location.hash = '#/app/settings/profile'}
>
{t('subscription.upgrade_plan')}
</Button>
</Card>
</div>
);
}
// Tab configuration
const tabs = [
{
@@ -131,66 +82,67 @@ const ProductionAnalyticsPage: React.FC = () => {
];
return (
<div className="space-y-6">
<PageHeader
title={t('analytics.production_analytics')}
description={t('analytics.advanced_insights_professionals_enterprises')}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<TrendingUp className="w-4 h-4 mr-2" />
{t('actions.export_report')}
</Button>
<Button variant="primary" size="sm">
<Zap className="w-4 h-4 mr-2" />
{t('actions.optimize_production')}
</Button>
</div>
<AnalyticsPageLayout
title={t('analytics.production_analytics')}
description={t('analytics.advanced_insights_professionals_enterprises')}
subscriptionLoading={subscriptionInfo.loading}
hasAccess={hasAdvancedAccess}
dataLoading={dashboardLoading}
actions={[
{
id: 'export-report',
label: t('actions.export_report'),
icon: TrendingUp,
onClick: () => {},
variant: 'outline',
size: 'sm',
},
{
id: 'optimize-production',
label: t('actions.optimize_production'),
icon: Zap,
onClick: () => {},
variant: 'primary',
size: 'sm',
},
]}
stats={[
{
title: t('stats.overall_efficiency'),
value: dashboard?.efficiency_percentage ? `${dashboard.efficiency_percentage.toFixed(1)}%` : '94%',
variant: 'success' as const,
icon: Target,
subtitle: t('stats.vs_target_95')
},
{
title: t('stats.average_cost_per_unit'),
value: '€2.45',
variant: 'info' as const,
icon: DollarSign,
subtitle: t('stats.down_3_vs_last_week')
},
{
title: t('stats.active_equipment'),
value: '8/9',
variant: 'warning' as const,
icon: Settings,
subtitle: t('stats.one_in_maintenance')
},
{
title: t('stats.quality_score'),
value: dashboard?.average_quality_score ? `${dashboard.average_quality_score.toFixed(1)}/10` : '9.2/10',
variant: 'success' as const,
icon: Award,
subtitle: t('stats.excellent_standards')
}
/>
{/* Key Performance Indicators */}
<StatsGrid
stats={[
{
title: t('stats.overall_efficiency'),
value: dashboard?.efficiency_percentage ? `${dashboard.efficiency_percentage.toFixed(1)}%` : '94%',
variant: 'success' as const,
icon: Target,
subtitle: t('stats.vs_target_95')
},
{
title: t('stats.average_cost_per_unit'),
value: '€2.45',
variant: 'info' as const,
icon: DollarSign,
subtitle: t('stats.down_3_vs_last_week')
},
{
title: t('stats.active_equipment'),
value: '8/9',
variant: 'warning' as const,
icon: Settings,
subtitle: t('stats.one_in_maintenance')
},
{
title: t('stats.quality_score'),
value: dashboard?.average_quality_score ? `${dashboard.average_quality_score.toFixed(1)}/10` : '9.2/10',
variant: 'success' as const,
icon: Award,
subtitle: t('stats.excellent_standards')
}
]}
columns={4}
/>
{/* Analytics Tabs */}
<Tabs
items={tabs.map(tab => ({ id: tab.id, label: tab.label }))}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
]}
statsColumns={4}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
showMobileNotice={true}
mobileNoticeText={t('mobile.swipe_scroll_interact')}
>
{/* Tab Content */}
<div className="min-h-screen">
{/* Overview Tab - Mixed Dashboard */}
@@ -249,22 +201,7 @@ const ProductionAnalyticsPage: React.FC = () => {
</div>
)}
</div>
{/* Mobile Optimization Notice */}
<div className="md:hidden p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-3">
<Target className="w-5 h-5 mt-0.5 text-blue-600 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('mobile.optimized_experience')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('mobile.swipe_scroll_interact')}
</p>
</div>
</div>
</div>
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
import { Button, Card, Badge, StatsGrid } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { Button, Card, Badge } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
@@ -156,67 +156,70 @@ const AIInsightsPage: React.FC = () => {
};
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>
<AnalyticsPageLayout
title="Inteligencia Artificial"
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
subscriptionLoading={false}
hasAccess={true}
dataLoading={isRefreshing}
actions={[
{
id: 'refresh',
label: 'Actualizar',
icon: RefreshCw,
onClick: handleRefresh,
variant: 'outline',
disabled: isRefreshing,
},
{
id: 'export',
label: 'Exportar',
icon: Download,
onClick: () => {},
variant: 'outline',
},
]}
stats={[
{
title: "Total Insights",
value: aiMetrics.totalInsights,
icon: Brain,
variant: "info"
},
{
title: "Accionables",
value: aiMetrics.actionableInsights,
icon: Zap,
variant: "success"
},
{
title: "Confianza Promedio",
value: `${aiMetrics.averageConfidence}%`,
icon: Target,
variant: "info"
},
{
title: "Alta Prioridad",
value: aiMetrics.highPriorityInsights,
icon: AlertTriangle,
variant: "error"
},
{
title: "Media Prioridad",
value: aiMetrics.mediumPriorityInsights,
icon: TrendingUp,
variant: "warning"
},
{
title: "Baja Prioridad",
value: aiMetrics.lowPriorityInsights,
icon: Lightbulb,
variant: "success"
}
/>
{/* AI Metrics */}
<StatsGrid
stats={[
{
title: "Total Insights",
value: aiMetrics.totalInsights,
icon: Brain,
variant: "info"
},
{
title: "Accionables",
value: aiMetrics.actionableInsights,
icon: Zap,
variant: "success"
},
{
title: "Confianza Promedio",
value: `${aiMetrics.averageConfidence}%`,
icon: Target,
variant: "info"
},
{
title: "Alta Prioridad",
value: aiMetrics.highPriorityInsights,
icon: AlertTriangle,
variant: "error"
},
{
title: "Media Prioridad",
value: aiMetrics.mediumPriorityInsights,
icon: TrendingUp,
variant: "warning"
},
{
title: "Baja Prioridad",
value: aiMetrics.lowPriorityInsights,
icon: Lightbulb,
variant: "success"
}
]}
columns={3}
/>
]}
statsColumns={6}
showMobileNotice={true}
>
{/* Category Filter */}
<Card className="p-6">
<div className="flex flex-wrap gap-2">
@@ -300,7 +303,7 @@ const AIInsightsPage: React.FC = () => {
</Button>
</Card>
)}
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -0,0 +1,357 @@
import React, { useState, useMemo } from 'react';
import {
Clock,
Filter,
Download,
Search,
AlertTriangle,
Eye,
FileText,
Activity,
X,
} from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { AnalyticsPageLayout } from '../../../../components/analytics';
import { LoadingSpinner } from '../../../../components/ui';
import { useAllAuditLogs, useAllAuditLogStats } from '../../../../api/hooks/auditLogs';
import { useTenantId } from '../../../../hooks/useTenantId';
import { AuditLogFilters, AggregatedAuditLog } from '../../../../api/types/auditLogs';
import { auditLogsService } from '../../../../api/services/auditLogs';
import { EventFilterSidebar } from '../../../../components/analytics/events/EventFilterSidebar';
import { EventDetailModal } from '../../../../components/analytics/events/EventDetailModal';
import { EventStatsWidget } from '../../../../components/analytics/events/EventStatsWidget';
import { SeverityBadge } from '../../../../components/analytics/events/SeverityBadge';
import { ServiceBadge } from '../../../../components/analytics/events/ServiceBadge';
import { ActionBadge } from '../../../../components/analytics/events/ActionBadge';
import { formatDistanceToNow } from 'date-fns';
const EventRegistryPage: React.FC = () => {
const tenantId = useTenantId();
// UI State
const [showFilters, setShowFilters] = useState(true);
const [selectedEvent, setSelectedEvent] = useState<AggregatedAuditLog | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
// Filter State
const [filters, setFilters] = useState<AuditLogFilters>({
limit: 50,
offset: 0,
});
// Calculate pagination
const paginatedFilters = useMemo(() => ({
...filters,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
}), [filters, currentPage, pageSize]);
// Fetch audit logs
const {
data: auditLogs,
isLoading: logsLoading,
error: logsError,
refetch: refetchLogs,
} = useAllAuditLogs(tenantId, paginatedFilters, {
enabled: !!tenantId,
retry: 1,
retryDelay: 1000,
});
// Fetch statistics
const {
data: stats,
isLoading: statsLoading,
} = useAllAuditLogStats(
tenantId,
{
start_date: filters.start_date,
end_date: filters.end_date,
},
{
enabled: !!tenantId,
retry: 1,
retryDelay: 1000,
}
);
// Handle filter changes
const handleFilterChange = (newFilters: Partial<AuditLogFilters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
setCurrentPage(1); // Reset to first page when filters change
};
// Handle export
const handleExport = async (format: 'csv' | 'json') => {
if (!auditLogs || auditLogs.length === 0) return;
try {
auditLogsService.downloadAuditLogs(auditLogs, format);
} catch (error) {
console.error('Export failed:', error);
}
};
// Format timestamp
const formatTimestamp = (timestamp: string) => {
try {
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
} catch {
return timestamp;
}
};
// Get total pages
const totalPages = Math.ceil((auditLogs?.length || 0) / pageSize);
return (
<AnalyticsPageLayout
title="Registro de Eventos"
subtitle="Seguimiento de todas las actividades y eventos del sistema"
icon={FileText}
>
{/* Statistics Widget */}
{!statsLoading && stats && (
<div className="mb-6">
<EventStatsWidget stats={stats} />
</div>
)}
{/* Controls Bar */}
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<Button
variant={showFilters ? 'primary' : 'secondary'}
size="sm"
onClick={() => setShowFilters(!showFilters)}
icon={Filter}
>
{showFilters ? 'Ocultar Filtros' : 'Mostrar Filtros'}
</Button>
{Object.keys(filters).length > 2 && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setFilters({ limit: 50, offset: 0 });
setCurrentPage(1);
}}
icon={X}
>
Limpiar Filtros
</Button>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => handleExport('csv')}
icon={Download}
disabled={!auditLogs || auditLogs.length === 0}
>
Exportar CSV
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => handleExport('json')}
icon={Download}
disabled={!auditLogs || auditLogs.length === 0}
>
Exportar JSON
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex gap-6">
{/* Filter Sidebar */}
{showFilters && (
<div className="w-80 flex-shrink-0">
<EventFilterSidebar
filters={filters}
onFiltersChange={handleFilterChange}
stats={stats}
/>
</div>
)}
{/* Event Table */}
<div className="flex-1">
<Card>
{logsLoading ? (
<div className="flex items-center justify-center py-16">
<LoadingSpinner size="lg" />
</div>
) : logsError ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<AlertTriangle className="mb-4 h-12 w-12 text-red-500" />
<h3 className="mb-2 text-lg font-semibold text-gray-900">
Error al cargar eventos
</h3>
<p className="mb-4 text-sm text-gray-600">
Ocurrió un error al obtener los registros de auditoría
</p>
<Button onClick={() => refetchLogs()} size="sm">
Reintentar
</Button>
</div>
) : !auditLogs || auditLogs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<Activity className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-semibold text-gray-900">
No se encontraron eventos
</h3>
<p className="text-sm text-gray-600">
No hay registros de auditoría que coincidan con los filtros actuales
</p>
</div>
) : (
<>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-gray-200 bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Timestamp
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Servicio
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Acción
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Recurso
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Severidad
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Descripción
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Acciones
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{auditLogs.map((event) => (
<tr
key={event.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
<div className="flex flex-col">
<span className="font-medium">
{formatTimestamp(event.created_at)}
</span>
<span className="text-xs text-gray-500">
{new Date(event.created_at).toLocaleString()}
</span>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4">
<ServiceBadge service={event.service_name} />
</td>
<td className="whitespace-nowrap px-6 py-4">
<ActionBadge action={event.action} />
</td>
<td className="px-6 py-4 text-sm">
<div className="flex flex-col">
<span className="font-medium text-gray-900">
{event.resource_type}
</span>
{event.resource_id && (
<span className="text-xs text-gray-500 truncate max-w-xs" title={event.resource_id}>
{event.resource_id}
</span>
)}
</div>
</td>
<td className="whitespace-nowrap px-6 py-4">
<SeverityBadge severity={event.severity} />
</td>
<td className="px-6 py-4 text-sm text-gray-600">
<div className="max-w-md truncate" title={event.description}>
{event.description}
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedEvent(event)}
icon={Eye}
>
Ver
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="border-t border-gray-200 bg-gray-50 px-6 py-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700">
Mostrando{' '}
<span className="font-medium">
{(currentPage - 1) * pageSize + 1}
</span>{' '}
a{' '}
<span className="font-medium">
{Math.min(currentPage * pageSize, auditLogs.length)}
</span>{' '}
de{' '}
<span className="font-medium">{auditLogs.length}</span>{' '}
eventos
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Anterior
</Button>
<span className="text-sm text-gray-700">
Página {currentPage} de {totalPages}
</span>
<Button
variant="secondary"
size="sm"
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Siguiente
</Button>
</div>
</div>
</div>
</>
)}
</Card>
</div>
</div>
{/* Event Detail Modal */}
{selectedEvent && (
<EventDetailModal
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
/>
)}
</AnalyticsPageLayout>
);
};
export default EventRegistryPage;

View File

@@ -1,7 +1,7 @@
import React, { useState, useMemo } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Settings, Loader, Zap, Brain, Target, Activity } from 'lucide-react';
import { Button, Card, Badge, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { Button, Card, Badge, StatusCard, getStatusColor } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { LoadingSpinner } from '../../../../components/ui';
import { DemandChart } from '../../../../components/domain/forecasting';
import { useTenantForecasts, useCreateSingleForecast } from '../../../../api/hooks/forecasting';
@@ -215,43 +215,41 @@ const ForecastingPage: React.FC = () => {
}
return (
<div className="space-y-6">
<PageHeader
title="Predicción de Demanda"
description="Sistema inteligente de predicción de demanda basado en IA"
/>
{/* Stats Grid - Similar to POSPage */}
<StatsGrid
stats={[
{
title: 'Ingredientes con Modelos',
value: products.length,
variant: 'default' as const,
icon: Brain,
},
{
title: 'Predicciones Generadas',
value: forecasts.length,
variant: 'info' as const,
icon: TrendingUp,
},
{
title: 'Confianza Promedio',
value: `${averageConfidence}%`,
variant: 'success' as const,
icon: Target,
},
{
title: 'Demanda Total',
value: formatters.number(Math.round(totalDemand)),
variant: 'warning' as const,
icon: BarChart3,
},
]}
columns={4}
/>
<AnalyticsPageLayout
title="Predicción de Demanda"
description="Sistema inteligente de predicción de demanda basado en IA"
subscriptionLoading={false}
hasAccess={true}
dataLoading={isLoading}
stats={[
{
title: 'Ingredientes con Modelos',
value: products.length,
variant: 'default' as const,
icon: Brain,
},
{
title: 'Predicciones Generadas',
value: forecasts.length,
variant: 'info' as const,
icon: TrendingUp,
},
{
title: 'Confianza Promedio',
value: `${averageConfidence}%`,
variant: 'success' as const,
icon: Target,
},
{
title: 'Demanda Total',
value: formatters.number(Math.round(totalDemand)),
variant: 'warning' as const,
icon: BarChart3,
},
]}
statsColumns={4}
showMobileNotice={true}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Ingredient Selection Section */}
<div className="lg:col-span-2 space-y-6">
@@ -485,7 +483,7 @@ const ForecastingPage: React.FC = () => {
</div>
</Card>
)}
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -7,7 +7,6 @@ import {
AlertCircle,
Download,
Calendar,
Lock,
BarChart3,
Zap,
DollarSign,
@@ -31,8 +30,8 @@ import {
PolarAngleAxis,
PolarRadiusAxis,
} from 'recharts';
import { Button, Card, Badge, StatsGrid, Tabs } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { Badge, Card } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { useSubscription } from '../../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import {
@@ -101,53 +100,6 @@ const PerformanceAnalyticsPage: React.FC = () => {
departmentsLoading ||
alertsLoading;
// Show loading state while subscription data is being fetched
if (subscriptionInfo.loading) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea métricas transversales que muestran cómo los departamentos trabajan juntos"
/>
<Card className="p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
<p className="text-[var(--text-secondary)]">Cargando información de suscripción...</p>
</div>
</Card>
</div>
);
}
// If user doesn't have access to advanced analytics, show upgrade message
if (!canAccessAnalytics('advanced')) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea métricas transversales que muestran cómo los departamentos trabajan juntos"
/>
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Funcionalidad Exclusiva para Profesionales y Empresas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
El análisis de rendimiento avanzado está disponible solo para planes Professional y Enterprise.
Actualiza tu plan para acceder a métricas transversales de rendimiento, análisis de procesos integrados y optimización operativa.
</p>
<Button
variant="primary"
size="md"
onClick={() => (window.location.hash = '#/app/settings/profile')}
>
Actualizar Plan
</Button>
</Card>
</div>
);
}
// Helper functions
const getTrendIcon = (trend: 'up' | 'down' | 'stable') => {
if (trend === 'up') {
@@ -221,33 +173,31 @@ const PerformanceAnalyticsPage: React.FC = () => {
];
return (
<div className="p-6 space-y-6">
{/* Page Header */}
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea métricas transversales que muestran cómo los departamentos trabajan juntos"
actions={[
{
id: 'configure-alerts',
label: 'Configurar Alertas',
icon: Calendar,
onClick: () => {},
variant: 'outline',
disabled: true,
},
{
id: 'export-report',
label: 'Exportar Reporte',
icon: Download,
onClick: () => {},
variant: 'outline',
disabled: true,
},
]}
/>
{/* Controls */}
<Card className="p-6">
<AnalyticsPageLayout
title="Análisis de Rendimiento"
description="Monitorea métricas transversales que muestran cómo los departamentos trabajan juntos"
subscriptionLoading={subscriptionInfo.loading}
hasAccess={canAccessAnalytics('advanced')}
dataLoading={isLoading}
actions={[
{
id: 'configure-alerts',
label: 'Configurar Alertas',
icon: Calendar,
onClick: () => {},
variant: 'outline',
disabled: true,
},
{
id: 'export-report',
label: 'Exportar Reporte',
icon: Download,
onClick: () => {},
variant: 'outline',
disabled: true,
},
]}
filters={
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
@@ -266,25 +216,20 @@ const PerformanceAnalyticsPage: React.FC = () => {
</select>
</div>
</div>
</Card>
{/* Block 1: StatsGrid with 6 cross-functional metrics */}
<StatsGrid stats={statsData} loading={isLoading} />
{/* Block 2: Tabs */}
<Tabs items={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
{/* Block 3: Tab Content */}
<div className="space-y-6">
}
stats={statsData}
statsColumns={6}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
showMobileNotice={true}
>
{/* Vista General Tab */}
{activeTab === 'overview' && !isLoading && (
<>
{/* Department Comparison Matrix */}
{departments && departments.length > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Comparación de Departamentos
</h3>
<AnalyticsCard title="Comparación de Departamentos">
<div className="space-y-4">
{departments.map((dept) => (
<div key={dept.department_id} className="border rounded-lg p-4">
@@ -331,15 +276,12 @@ const PerformanceAnalyticsPage: React.FC = () => {
</div>
))}
</div>
</Card>
</AnalyticsCard>
)}
{/* Process Efficiency Breakdown */}
{processScore && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Desglose de Eficiencia por Procesos
</h3>
<AnalyticsCard title="Desglose de Eficiencia por Procesos">
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={[
@@ -358,7 +300,7 @@ const PerformanceAnalyticsPage: React.FC = () => {
<Bar dataKey="weight" fill="var(--color-secondary)" name="Peso (%)" />
</BarChart>
</ResponsiveContainer>
</Card>
</AnalyticsCard>
)}
</>
)}
@@ -641,8 +583,7 @@ const PerformanceAnalyticsPage: React.FC = () => {
)}
</>
)}
</div>
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -1,7 +1,7 @@
import React, { useState, useMemo } from 'react';
import { Calendar, TrendingUp, Euro, ShoppingCart, Download, Filter, Eye, Users, Package, CreditCard, BarChart3, AlertTriangle, Clock } from 'lucide-react';
import { Button, Card, Badge, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { Button, Card, Badge, StatusCard, getStatusColor } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { LoadingSpinner } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { useSalesAnalytics, useSalesRecords, useProductCategories } from '../../../../api/hooks/sales';
@@ -11,7 +11,7 @@ import { SalesDataResponse } from '../../../../api/types/sales';
const SalesAnalyticsPage: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('year');
const [selectedCategory, setSelectedCategory] = useState('all');
const [viewMode, setViewMode] = useState<'overview' | 'detailed'>('overview');
const [viewMode, setViewMode] = useState<'overview' | 'detailed' | 'patterns'>('overview');
const [exportLoading, setExportLoading] = useState(false);
const tenantId = useTenantId();
@@ -139,6 +139,50 @@ const SalesAnalyticsPage: React.FC = () => {
}));
}, [salesRecords]);
// Process traffic patterns from sales data
const trafficPatterns = useMemo(() => {
if (!salesRecords || salesRecords.length === 0) {
return {
hourlyTraffic: [],
weeklyTraffic: []
};
}
// Hourly traffic: count transactions per hour
const hourlyMap = new Map<number, number>();
const weeklyMap = new Map<number, number>();
salesRecords.forEach(record => {
const date = new Date(record.date);
const hour = date.getHours();
const dayOfWeek = date.getDay(); // 0 = Sunday, 6 = Saturday
// Count transactions per hour
hourlyMap.set(hour, (hourlyMap.get(hour) || 0) + 1);
// Count transactions per day of week
weeklyMap.set(dayOfWeek, (weeklyMap.get(dayOfWeek) || 0) + 1);
});
// Format hourly traffic data (0-23 hours)
const hourlyTraffic = Array.from({ length: 24 }, (_, hour) => ({
hour: `${hour.toString().padStart(2, '0')}:00`,
transactions: hourlyMap.get(hour) || 0
}));
// Format weekly traffic data (Sun-Sat)
const dayNames = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
const weeklyTraffic = dayNames.map((day, index) => ({
day,
transactions: weeklyMap.get(index) || 0
}));
return {
hourlyTraffic,
weeklyTraffic
};
}, [salesRecords]);
// Categories for filter
const categories = useMemo(() => {
const allCategories = [{ value: 'all', label: 'Todas las Categorías' }];
@@ -349,25 +393,23 @@ const SalesAnalyticsPage: React.FC = () => {
}
return (
<div className="space-y-6">
<PageHeader
title="Análisis de Ventas"
description="Análisis detallado del rendimiento de ventas y tendencias de tu panadería"
actions={[
{
id: "export-data",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => handleExport('csv'),
tooltip: "Exportar datos a CSV",
disabled: exportLoading || !salesRecords?.length
}
]}
/>
{/* Controls */}
<Card className="p-4">
<AnalyticsPageLayout
title="Análisis de Ventas"
description="Análisis detallado del rendimiento de ventas y tendencias de tu panadería"
subscriptionLoading={false}
hasAccess={true}
dataLoading={isLoading}
actions={[
{
id: "export-data",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => handleExport('csv'),
disabled: exportLoading || !salesRecords?.length
}
]}
filters={
<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>
@@ -408,6 +450,15 @@ const SalesAnalyticsPage: React.FC = () => {
<BarChart3 className="w-4 h-4 mr-1" />
General
</Button>
<Button
variant={viewMode === 'patterns' ? 'primary' : 'outline'}
onClick={() => setViewMode('patterns')}
size="sm"
className="rounded-none flex-1 border-l-0"
>
<Users className="w-4 h-4 mr-1" />
Patrones
</Button>
<Button
variant={viewMode === 'detailed' ? 'primary' : 'outline'}
onClick={() => setViewMode('detailed')}
@@ -421,32 +472,29 @@ const SalesAnalyticsPage: React.FC = () => {
</div>
</div>
</div>
</Card>
{/* Stats Grid */}
<StatsGrid
stats={[
{
title: 'Ingresos Totales',
value: formatters.currency(salesMetrics.totalRevenue),
variant: 'success' as const,
icon: Euro,
},
{
title: 'Total Transacciones',
value: salesMetrics.totalOrders.toLocaleString(),
variant: 'info' as const,
icon: ShoppingCart,
},
{
title: 'Ticket Promedio',
value: formatters.currency(salesMetrics.averageOrderValue),
variant: 'warning' as const,
icon: CreditCard,
},
{
title: 'Cantidad Total',
value: salesMetrics.totalQuantity.toLocaleString(),
}
stats={[
{
title: 'Ingresos Totales',
value: formatters.currency(salesMetrics.totalRevenue),
variant: 'success' as const,
icon: Euro,
},
{
title: 'Total Transacciones',
value: salesMetrics.totalOrders.toLocaleString(),
variant: 'info' as const,
icon: ShoppingCart,
},
{
title: 'Ticket Promedio',
value: formatters.currency(salesMetrics.averageOrderValue),
variant: 'warning' as const,
icon: CreditCard,
},
{
title: 'Cantidad Total',
value: salesMetrics.totalQuantity.toLocaleString(),
variant: 'default' as const,
icon: Package,
},
@@ -463,10 +511,185 @@ const SalesAnalyticsPage: React.FC = () => {
icon: Users,
},
]}
columns={3}
/>
statsColumns={6}
showMobileNotice={true}
>
{viewMode === 'patterns' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hourly Traffic Pattern */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<Clock className="w-5 h-5 mr-2" />
Tráfico por Hora
</h3>
{trafficPatterns.hourlyTraffic.length === 0 || trafficPatterns.hourlyTraffic.every(h => h.transactions === 0) ? (
<div className="text-center py-12">
<Clock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<p className="text-[var(--text-secondary)]">No hay datos de tráfico horario para este período</p>
</div>
) : (
<div className="h-64 flex items-end space-x-1 justify-between">
{trafficPatterns.hourlyTraffic.map((data, index) => {
const maxTransactions = Math.max(...trafficPatterns.hourlyTraffic.map(h => h.transactions));
const height = maxTransactions > 0 ? (data.transactions / maxTransactions) * 200 : 4;
{viewMode === 'overview' ? (
return (
<div key={index} className="flex flex-col items-center flex-1 group relative">
{data.transactions > 0 && (
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.transactions}</div>
)}
<div
className="w-full bg-[var(--color-info)]/70 hover:bg-[var(--color-info)] rounded-t transition-colors cursor-pointer"
style={{
height: `${height}px`,
minHeight: '4px'
}}
title={`${data.hour}: ${data.transactions} transacciones`}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center whitespace-nowrap">
{data.hour}
</span>
</div>
);
})}
</div>
)}
<div className="mt-4 pt-4 border-t border-[var(--border-secondary)]">
<p className="text-sm text-[var(--text-secondary)]">
Patrones de transacciones por hora del día basados en datos de ventas
</p>
</div>
</Card>
{/* Weekly Traffic Pattern */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<Calendar className="w-5 h-5 mr-2" />
Tráfico Semanal
</h3>
{trafficPatterns.weeklyTraffic.length === 0 || trafficPatterns.weeklyTraffic.every(d => d.transactions === 0) ? (
<div className="text-center py-12">
<Calendar className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<p className="text-[var(--text-secondary)]">No hay datos de tráfico semanal para este período</p>
</div>
) : (
<div className="h-64 flex items-end space-x-2 justify-between">
{trafficPatterns.weeklyTraffic.map((data, index) => {
const maxTransactions = Math.max(...trafficPatterns.weeklyTraffic.map(d => d.transactions));
const height = maxTransactions > 0 ? (data.transactions / maxTransactions) * 200 : 8;
return (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.transactions}</div>
<div
className="w-full bg-[var(--color-success)] hover:bg-[var(--color-success)]/80 rounded-t transition-colors cursor-pointer"
style={{
height: `${height}px`,
minHeight: '8px'
}}
title={`${data.day}: ${data.transactions} transacciones`}
></div>
<span className="text-sm text-[var(--text-secondary)] mt-2 font-medium">
{data.day}
</span>
</div>
);
})}
</div>
)}
<div className="mt-4 pt-4 border-t border-[var(--border-secondary)]">
<p className="text-sm text-[var(--text-secondary)]">
Distribución de transacciones por día de la semana
</p>
</div>
</Card>
{/* Peak Hours Summary */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<TrendingUp className="w-5 h-5 mr-2" />
Resumen de Horarios Pico
</h3>
{trafficPatterns.hourlyTraffic.length > 0 && trafficPatterns.hourlyTraffic.some(h => h.transactions > 0) ? (
<div className="space-y-4">
{(() => {
const sorted = [...trafficPatterns.hourlyTraffic]
.filter(h => h.transactions > 0)
.sort((a, b) => b.transactions - a.transactions)
.slice(0, 5);
return sorted.map((data, index) => (
<div key={data.hour} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-[var(--text-tertiary)] w-6">{index + 1}.</span>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{data.hour}</p>
<p className="text-xs text-[var(--text-tertiary)]">Horario pico</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--color-info)]">
{data.transactions} transacciones
</p>
</div>
</div>
));
})()}
</div>
) : (
<div className="text-center py-8">
<TrendingUp className="mx-auto h-8 w-8 text-[var(--text-tertiary)] mb-2" />
<p className="text-[var(--text-secondary)]">No hay datos suficientes para mostrar horarios pico</p>
</div>
)}
</Card>
{/* Busiest Days Summary */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<Calendar className="w-5 h-5 mr-2" />
Días Más Activos
</h3>
{trafficPatterns.weeklyTraffic.length > 0 && trafficPatterns.weeklyTraffic.some(d => d.transactions > 0) ? (
<div className="space-y-4">
{(() => {
const sorted = [...trafficPatterns.weeklyTraffic]
.filter(d => d.transactions > 0)
.sort((a, b) => b.transactions - a.transactions)
.slice(0, 5);
return sorted.map((data, index) => (
<div key={data.day} 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 rounded-full ${
index === 0 ? 'bg-[var(--color-success)]' :
index === 1 ? 'bg-[var(--color-info)]' :
index === 2 ? 'bg-[var(--color-warning)]' :
'bg-[var(--color-primary)]'
}`}></div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{data.day}</p>
<p className="text-xs text-[var(--text-tertiary)]">Día activo</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--color-success)]">
{data.transactions} transacciones
</p>
</div>
</div>
));
})()}
</div>
) : (
<div className="text-center py-8">
<Calendar className="mx-auto h-8 w-8 text-[var(--text-tertiary)] mb-2" />
<p className="text-[var(--text-secondary)]">No hay datos suficientes para mostrar días más activos</p>
</div>
)}
</Card>
</div>
) : viewMode === 'overview' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products */}
<Card className="p-6">
@@ -740,7 +963,7 @@ const SalesAnalyticsPage: React.FC = () => {
)}
</Card>
)}
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -24,6 +24,7 @@ import {
Button,
Badge,
} from '../../../../components/ui';
import { AnalyticsPageLayout } from '../../../../components/analytics';
import {
CloudRain,
Sun,
@@ -41,7 +42,6 @@ import {
Sparkles,
Package,
} from 'lucide-react';
import { PageHeader } from '../../../../components/layout';
import { useIngredients } from '../../../../api/hooks/inventory';
import { useModels } from '../../../../api/hooks/training';
@@ -220,17 +220,14 @@ export const ScenarioSimulationPage: React.FC = () => {
};
return (
<div className="space-y-6">
<PageHeader
title={t('analytics.scenario_simulation.title', 'Scenario Simulation')}
subtitle={t('analytics.scenario_simulation.subtitle', 'Test "what-if" scenarios to optimize your planning')}
icon={Sparkles}
status={{
text: t('subscription.professional_enterprise', 'Professional/Enterprise'),
variant: 'primary'
}}
/>
<AnalyticsPageLayout
title={t('analytics.scenario_simulation.title', 'Scenario Simulation')}
description={t('analytics.scenario_simulation.subtitle', 'Test "what-if" scenarios to optimize your planning')}
subscriptionLoading={false}
hasAccess={true}
dataLoading={isSimulating}
showMobileNotice={true}
>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
@@ -1061,7 +1058,7 @@ export const ScenarioSimulationPage: React.FC = () => {
)}
</div>
</div>
</div>
</AnalyticsPageLayout>
);
};

View File

@@ -1,314 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
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={t('events:title', 'Registro de Eventos')}
description={t('events:description', 'Seguimiento de todas las actividades y eventos del sistema')}
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros Avanzados
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Event Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Eventos</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{eventStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Hoy</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{eventStats.today}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{eventStats.sales}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{eventStats.production}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Activity className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
</div>
{/* Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="all">Todos</option>
</select>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category.value}
onClick={() => setSelectedCategory(category.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{category.label} ({category.count})
</button>
))}
</div>
</div>
</Card>
{/* Events List */}
<div className="space-y-4">
{filteredEvents.map((event) => (
<Card key={event.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className={`p-2 rounded-lg bg-${getSeverityColor(event.severity)}-100`}>
{getCategoryIcon(event.category)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{event.title}</h3>
<Badge variant={getSeverityColor(event.severity)}>
{event.category}
</Badge>
</div>
<p className="text-[var(--text-secondary)] mb-3">{event.description}</p>
{/* Event Metadata */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{Object.entries(event.metadata).map(([key, value]) => (
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider mb-1">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{value}</p>
</div>
))}
</div>
<div className="flex items-center text-sm text-[var(--text-tertiary)]">
<span>{formatTimeAgo(event.timestamp)}</span>
<span className="mx-2"></span>
<span>{new Date(event.timestamp).toLocaleString('es-ES')}</span>
</div>
</div>
</div>
<Button size="sm" variant="outline">
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
</div>
</Card>
))}
</div>
{filteredEvents.length === 0 && (
<Card className="p-12 text-center">
<Activity className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No hay eventos</h3>
<p className="text-[var(--text-secondary)]">
No se encontraron eventos para el período y categoría seleccionados.
</p>
</Card>
)}
</div>
);
};
export default EventsPage;

View File

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

View File

@@ -1,338 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
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={t('traffic:title', 'Análisis de Tráfico')}
description={t('traffic:description', 'Monitorea los patrones de visitas y flujo de clientes')}
action={
<div className="flex space-x-2">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
}
/>
{/* Traffic Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Visitantes Totales</p>
<p className="text-3xl font-bold text-[var(--color-info)]">{trafficData.totalVisitors.toLocaleString()}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Hora Pico</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{trafficData.peakHour}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Duración Promedio</p>
<p className="text-3xl font-bold text-purple-600">{trafficData.averageVisitDuration}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Conversión</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{trafficData.conversionRate}%</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<TrendingUp className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Días Ocupados</p>
<p className="text-sm font-bold text-[var(--color-error)]">{trafficData.busyDays.join(', ')}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<Calendar className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="day">Hoy</option>
<option value="week">Esta Semana</option>
<option value="month">Este Mes</option>
<option value="year">Este Año</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="visitors">Visitantes</option>
<option value="sales">Ventas</option>
<option value="duration">Duración</option>
<option value="conversion">Conversión</option>
</select>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Hourly Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{hourlyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
<div
className="w-full bg-[var(--color-info)]/50 rounded-t"
style={{
height: `${(data.visitors / maxVisitors) * 200}px`,
minHeight: '4px'
}}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
</Card>
{/* Daily Traffic */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tráfico Semanal</h3>
<div className="h-64 flex items-end space-x-2 justify-between">
{dailyTraffic.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.visitors}</div>
<div
className="w-full bg-green-500 rounded-t"
style={{
height: `${(data.visitors / maxDailyVisitors) * 200}px`,
minHeight: '8px'
}}
></div>
<span className="text-sm text-[var(--text-secondary)] mt-2 font-medium">
{data.day}
</span>
<div className="text-xs text-[var(--text-tertiary)] mt-1">
{data.conversion}%
</div>
</div>
))}
</div>
</Card>
{/* Traffic Sources */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Fuentes de Tráfico</h3>
<div className="space-y-3">
{trafficSources.map((source, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-[var(--color-info)]/50 rounded-full"></div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{source.source}</p>
<p className="text-xs text-[var(--text-tertiary)]">{source.visitors} visitantes</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-[var(--text-primary)]">{source.percentage}%</p>
<div className={`text-xs flex items-center ${getTrendColor(source.trend)}`}>
<span>{getTrendIcon(source.trend)} {Math.abs(source.trend).toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Customer Segments */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Segmentos de Clientes</h3>
<div className="space-y-4">
{customerSegments.map((segment, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]">{segment.segment}</h4>
<Badge variant="blue">{segment.percentage}%</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-[var(--text-tertiary)]">Clientes</p>
<p className="font-medium">{segment.count}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Gasto Promedio</p>
<p className="font-medium">{segment.avgSpend}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Horario Pico</p>
<p className="font-medium">{segment.peakHours.join(', ')}</p>
</div>
<div>
<p className="text-[var(--text-tertiary)]">Frecuencia</p>
<p className="font-medium">{segment.frequency}</p>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Traffic Heat Map placeholder */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Mapa de Calor - Zonas de la Panadería</h3>
<div className="h-64 bg-[var(--bg-tertiary)] rounded-lg flex items-center justify-center">
<div className="text-center">
<MapPin className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">Visualización de zonas de mayor tráfico</p>
<p className="text-sm text-[var(--text-tertiary)] mt-1">Entrada: 45% Mostrador: 32% Zona sentada: 23%</p>
</div>
</div>
</Card>
</div>
);
};
export default TrafficPage;

View File

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

View File

@@ -1,425 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
const [selectedPeriod, setSelectedPeriod] = useState('week');
const currentWeather = {
temperature: 18,
condition: 'partly-cloudy',
humidity: 65,
windSpeed: 12,
pressure: 1013,
uvIndex: 4,
visibility: 10,
description: t('weather:conditions.partly_cloudy', 'Parcialmente nublado')
};
const forecast = [
{
date: '2024-01-27',
day: t('weather:days.saturday', 'Sábado'),
condition: 'sunny',
tempMax: 22,
tempMin: 12,
humidity: 45,
precipitation: 0,
wind: 8,
impact: 'high-demand',
recommendation: t('weather:recommendations.increase_ice_cream', 'Incrementar producción de helados y bebidas frías')
},
{
date: '2024-01-28',
day: t('weather:days.sunday', 'Domingo'),
condition: 'partly-cloudy',
tempMax: 19,
tempMin: 11,
humidity: 55,
precipitation: 20,
wind: 15,
impact: 'normal',
recommendation: t('weather:recommendations.standard_production', 'Producción estándar')
},
{
date: '2024-01-29',
day: t('weather:days.monday', 'Lunes'),
condition: 'rainy',
tempMax: 15,
tempMin: 8,
humidity: 85,
precipitation: 80,
wind: 22,
impact: 'comfort-food',
recommendation: t('weather:recommendations.comfort_foods', 'Aumentar sopas, chocolates calientes y pan recién horneado')
},
{
date: '2024-01-30',
day: t('weather:days.tuesday', 'Martes'),
condition: 'cloudy',
tempMax: 16,
tempMin: 9,
humidity: 70,
precipitation: 40,
wind: 18,
impact: 'moderate',
recommendation: t('weather:recommendations.indoor_focus', 'Enfoque en productos de interior')
},
{
date: '2024-01-31',
day: t('weather:days.wednesday', 'Miércoles'),
condition: 'sunny',
tempMax: 24,
tempMin: 14,
humidity: 40,
precipitation: 0,
wind: 10,
impact: 'high-demand',
recommendation: t('weather:recommendations.fresh_products', 'Incrementar productos frescos y ensaladas')
}
];
const weatherImpacts = [
{
condition: t('weather:impacts.sunny_day.condition', 'Día Soleado'),
icon: Sun,
impact: t('weather:impacts.sunny_day.impact', 'Aumento del 25% en bebidas frías'),
recommendations: [
t('weather:impacts.sunny_day.recommendations.0', 'Incrementar producción de helados'),
t('weather:impacts.sunny_day.recommendations.1', 'Más bebidas refrescantes'),
t('weather:impacts.sunny_day.recommendations.2', 'Ensaladas y productos frescos'),
t('weather:impacts.sunny_day.recommendations.3', 'Horario extendido de terraza')
],
color: 'yellow'
},
{
condition: t('weather:impacts.rainy_day.condition', 'Día Lluvioso'),
icon: CloudRain,
impact: t('weather:impacts.rainy_day.impact', 'Aumento del 40% en productos calientes'),
recommendations: [
t('weather:impacts.rainy_day.recommendations.0', 'Más sopas y caldos'),
t('weather:impacts.rainy_day.recommendations.1', 'Chocolates calientes'),
t('weather:impacts.rainy_day.recommendations.2', 'Pan recién horneado'),
t('weather:impacts.rainy_day.recommendations.3', 'Productos de repostería')
],
color: 'blue'
},
{
condition: t('weather:impacts.cold_day.condition', 'Frío Intenso'),
icon: Thermometer,
impact: t('weather:impacts.cold_day.impact', 'Preferencia por comida reconfortante'),
recommendations: [
t('weather:impacts.cold_day.recommendations.0', 'Aumentar productos horneados'),
t('weather:impacts.cold_day.recommendations.1', 'Bebidas calientes especiales'),
t('weather:impacts.cold_day.recommendations.2', 'Productos energéticos'),
t('weather:impacts.cold_day.recommendations.3', '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 t('weather:conditions.sunny', 'Soleado');
case 'partly-cloudy': return t('weather:conditions.partly_cloudy', 'Parcialmente nublado');
case 'cloudy': return t('weather:conditions.cloudy', 'Nublado');
case 'rainy': return t('weather:conditions.rainy', '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 t('weather:impact.high_demand', 'Alta Demanda');
case 'comfort-food': return t('weather:impact.comfort_food', 'Comida Reconfortante');
case 'moderate': return t('weather:impact.moderate', 'Demanda Moderada');
case 'normal': return t('weather:impact.normal', 'Demanda Normal');
default: return impact;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title={t('weather:title', 'Datos Meteorológicos')}
description={t('weather: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">{t('weather:current.title', '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)]">{t('weather:current.humidity', '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)]">{t('weather:current.wind', '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">{t('weather:current.pressure', 'Presión')}:</span> {currentWeather.pressure} hPa
</div>
<div className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">{t('weather:current.uv', 'UV')}:</span> {currentWeather.uvIndex}
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">{t('weather:current.visibility', 'Visibilidad')}:</span> {currentWeather.visibility} km
</div>
<Badge variant="blue">{t('weather:current.favorable_conditions', '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)]">{t('weather:forecast.title', '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">{t('weather:forecast.next_week', 'Próxima Semana')}</option>
<option value="month">{t('weather:forecast.next_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>{t('weather:current.humidity', 'Humedad')}:</span>
<span>{day.humidity}%</span>
</div>
<div className="flex justify-between">
<span>{t('weather:forecast.rain', 'Lluvia')}:</span>
<span>{day.precipitation}%</span>
</div>
<div className="flex justify-between">
<span>{t('weather:current.wind', '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">{t('weather:impact.title', '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">{t('weather:impact.recommendations', '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">{t('weather:seasonal.title', '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">{t('weather:alerts.title', 'Alertas Meteorológicas')}</h3>
<div className="space-y-3">
<div className="flex items-center p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Sun className="w-5 h-5 text-yellow-600 mr-3" />
<div>
<p className="text-sm font-medium text-yellow-800">Ola de calor prevista</p>
<p className="text-sm text-yellow-700">Se esperan temperaturas superiores a 30°C los próximos 3 días</p>
<p className="text-xs text-yellow-600 mt-1">Recomendación: Incrementar stock de bebidas frías y helados</p>
</div>
</div>
<div className="flex items-center p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
<CloudRain className="w-5 h-5 text-[var(--color-info)] mr-3" />
<div>
<p className="text-sm font-medium text-[var(--color-info)]">Lluvia intensa el lunes</p>
<p className="text-sm text-[var(--color-info)]">80% probabilidad de precipitación con vientos fuertes</p>
<p className="text-xs text-[var(--color-info)] mt-1">Recomendación: Preparar más productos calientes y de refugio</p>
</div>
</div>
</div>
</Card>
</div>
);
};
export default WeatherPage;

View File

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