Improve the frontend 4

This commit is contained in:
Urtzi Alfaro
2025-11-01 21:35:03 +01:00
parent f44d235c6d
commit 0220da1725
59 changed files with 5785 additions and 1870 deletions

View File

@@ -12,11 +12,21 @@ import {
Truck,
Calendar
} from 'lucide-react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend
} from 'recharts';
import { PageHeader } from '../../../components/layout';
import { Card, StatsGrid, Button, Tabs } from '../../../components/ui';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProcurementDashboard } from '../../../api/hooks/orders';
import { useProcurementDashboard, useProcurementTrends } from '../../../api/hooks/procurement';
import { formatters } from '../../../components/ui/Stats/StatsPresets';
const ProcurementAnalyticsPage: React.FC = () => {
@@ -27,6 +37,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
const { data: dashboard, isLoading: dashboardLoading } = useProcurementDashboard(tenantId);
const { data: trends, isLoading: trendsLoading } = useProcurementTrends(tenantId, 7);
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
@@ -162,32 +173,32 @@ const ProcurementAnalyticsPage: React.FC = () => {
<>
{/* 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>
<div className="space-y-3">
{dashboard?.plan_status_distribution?.map((status: any) => (
<div key={status.status} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{status.status}</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: `${(status.count / (dashboard?.summary?.total_plans || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-8 text-right">
{status.count}
</span>
{/* 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>
<div className="space-y-3">
{dashboard?.plan_status_distribution?.map((status: any) => (
<div key={status.status} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{status.status}</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: `${(status.count / (dashboard?.summary?.total_plans || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-8 text-right">
{status.count}
</span>
</div>
))}
</div>
</div>
))}
</div>
</Card>
</div>
</Card>
{/* Critical Requirements */}
<Card>
@@ -302,15 +313,63 @@ const ProcurementAnalyticsPage: React.FC = () => {
</Card>
</div>
{/* Performance Trend Chart Placeholder */}
{/* Performance Trend Chart */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencias de Rendimiento
Tendencias de Rendimiento (Últimos 7 días)
</h3>
<div className="h-64 flex items-center justify-center text-[var(--text-tertiary)]">
Gráfico de tendencias - Próximamente
</div>
{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>
</div>
) : 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>
)}
</div>
</Card>
</>
@@ -459,11 +518,51 @@ const ProcurementAnalyticsPage: React.FC = () => {
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencia de Calidad
Tendencia de Calidad (Últimos 7 días)
</h3>
<div className="h-48 flex items-center justify-center text-[var(--text-tertiary)]">
Gráfico de tendencia de calidad - Próximamente
</div>
{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>

View File

@@ -1,22 +1,80 @@
import React, { useState } from 'react';
import { Activity, Clock, Users, TrendingUp, Target, AlertCircle, Download, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import {
Activity,
Clock,
TrendingUp,
Target,
AlertCircle,
Download,
Calendar,
Lock,
BarChart3,
Zap,
DollarSign,
Package,
AlertOctagon,
} from 'lucide-react';
import {
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
LineChart,
Line,
RadarChart,
Radar,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis,
} from 'recharts';
import { Button, Card, Badge, StatsGrid, Tabs } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useSubscription } from '../../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import {
useCycleTimeMetrics,
useProcessEfficiencyScore,
useResourceUtilization,
useCostRevenueRatio,
useQualityImpactIndex,
useCriticalBottlenecks,
useDepartmentPerformance,
usePerformanceAlerts,
} from '../../../../api/hooks/performance';
import { TimePeriod } from '../../../../api/types/performance';
// Formatters for StatsGrid
const formatters = {
number: (value: number) => value.toFixed(0),
percentage: (value: number) => `${value.toFixed(1)}%`,
hours: (value: number) => `${value.toFixed(1)}h`,
currency: (value: number) => `${value.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`,
};
const PerformanceAnalyticsPage: React.FC = () => {
const [selectedTimeframe, setSelectedTimeframe] = useState('month');
const [selectedMetric, setSelectedMetric] = useState('efficiency');
const { canAccessAnalytics, subscriptionInfo } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const performanceMetrics = {
overallEfficiency: 87.5,
productionTime: 4.2,
qualityScore: 92.1,
employeeProductivity: 89.3,
customerSatisfaction: 94.7,
resourceUtilization: 78.9,
};
const [selectedPeriod, setSelectedPeriod] = useState<TimePeriod>('week');
const [activeTab, setActiveTab] = useState('overview');
const timeframes = [
// Fetch all cross-functional performance data
const { data: cycleTime, isLoading: cycleTimeLoading } = useCycleTimeMetrics(tenantId, selectedPeriod);
const { data: processScore, isLoading: processScoreLoading } = useProcessEfficiencyScore(tenantId, selectedPeriod);
const { data: resourceUtil, isLoading: resourceUtilLoading } = useResourceUtilization(tenantId, selectedPeriod);
const { data: costRevenue, isLoading: costRevenueLoading } = useCostRevenueRatio(tenantId, selectedPeriod);
const { data: qualityIndex, isLoading: qualityIndexLoading } = useQualityImpactIndex(tenantId, selectedPeriod);
const { data: bottlenecks, isLoading: bottlenecksLoading } = useCriticalBottlenecks(tenantId, selectedPeriod);
const { data: departments, isLoading: departmentsLoading } = useDepartmentPerformance(tenantId, selectedPeriod);
const { data: alerts, isLoading: alertsLoading } = usePerformanceAlerts(tenantId);
// Period options
const timeframes: { value: TimePeriod; label: string }[] = [
{ value: 'day', label: 'Hoy' },
{ value: 'week', label: 'Esta Semana' },
{ value: 'month', label: 'Este Mes' },
@@ -24,380 +82,568 @@ const PerformanceAnalyticsPage: React.FC = () => {
{ value: 'year', label: 'Año' },
];
const departmentPerformance = [
{
department: 'Producción',
efficiency: 91.2,
trend: 5.3,
issues: 2,
employees: 8,
metrics: {
avgBatchTime: '2.3h',
qualityRate: '94%',
wastePercentage: '3.1%'
}
},
{
department: 'Ventas',
efficiency: 88.7,
trend: -1.2,
issues: 1,
employees: 4,
metrics: {
avgServiceTime: '3.2min',
customerWaitTime: '2.1min',
salesPerHour: '€127'
}
},
{
department: 'Inventario',
efficiency: 82.4,
trend: 2.8,
issues: 3,
employees: 2,
metrics: {
stockAccuracy: '96.7%',
turnoverRate: '12.3',
wastageRate: '4.2%'
}
},
{
department: 'Administración',
efficiency: 94.1,
trend: 8.1,
issues: 0,
employees: 3,
metrics: {
responseTime: '1.2h',
taskCompletion: '98%',
documentAccuracy: '99.1%'
}
}
// Tab configuration
const tabs = [
{ id: 'overview', label: 'Vista General' },
{ id: 'efficiency', label: 'Eficiencia Operativa' },
{ id: 'quality', label: 'Impacto de Calidad' },
{ id: 'optimization', label: 'Optimización' },
];
const kpiTrends = [
{
name: 'Eficiencia General',
current: 87.5,
target: 90.0,
previous: 84.2,
unit: '%',
color: 'blue'
},
{
name: 'Tiempo de Producción',
current: 4.2,
target: 4.0,
previous: 4.5,
unit: 'h',
color: 'green',
inverse: true
},
{
name: 'Satisfacción Cliente',
current: 94.7,
target: 95.0,
previous: 93.1,
unit: '%',
color: 'purple'
},
{
name: 'Utilización de Recursos',
current: 78.9,
target: 85.0,
previous: 76.3,
unit: '%',
color: 'orange'
}
];
// Combined loading state
const isLoading =
cycleTimeLoading ||
processScoreLoading ||
resourceUtilLoading ||
costRevenueLoading ||
qualityIndexLoading ||
bottlenecksLoading ||
departmentsLoading ||
alertsLoading;
const performanceAlerts = [
{
id: '1',
type: 'warning',
title: 'Eficiencia de Inventario Baja',
description: 'El departamento de inventario está por debajo del objetivo del 85%',
value: '82.4%',
target: '85%',
department: 'Inventario'
},
{
id: '2',
type: 'info',
title: 'Tiempo de Producción Mejorado',
description: 'El tiempo promedio de producción ha mejorado este mes',
value: '4.2h',
target: '4.0h',
department: 'Producción'
},
{
id: '3',
type: 'success',
title: 'Administración Supera Objetivos',
description: 'El departamento administrativo está funcionando por encima del objetivo',
value: '94.1%',
target: '90%',
department: 'Administración'
}
];
// 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>
);
}
const productivityData = [
{ hour: '07:00', efficiency: 75, transactions: 12, employees: 3 },
{ hour: '08:00', efficiency: 82, transactions: 18, employees: 5 },
{ hour: '09:00', efficiency: 89, transactions: 28, employees: 6 },
{ hour: '10:00', efficiency: 91, transactions: 32, employees: 7 },
{ hour: '11:00', efficiency: 94, transactions: 38, employees: 8 },
{ hour: '12:00', efficiency: 96, transactions: 45, employees: 8 },
{ hour: '13:00', efficiency: 95, transactions: 42, employees: 8 },
{ hour: '14:00', efficiency: 88, transactions: 35, employees: 7 },
{ hour: '15:00', efficiency: 85, transactions: 28, employees: 6 },
{ hour: '16:00', efficiency: 83, transactions: 25, employees: 5 },
{ hour: '17:00', efficiency: 87, transactions: 31, employees: 6 },
{ hour: '18:00', efficiency: 90, transactions: 38, employees: 7 },
{ hour: '19:00', efficiency: 86, transactions: 29, employees: 5 },
{ hour: '20:00', efficiency: 78, transactions: 18, employees: 3 },
];
// 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>
);
}
const getTrendIcon = (trend: number) => {
if (trend > 0) {
// Helper functions
const getTrendIcon = (trend: 'up' | 'down' | 'stable') => {
if (trend === 'up') {
return <TrendingUp className="w-4 h-4 text-[var(--color-success)]" />;
} else {
} else if (trend === 'down') {
return <TrendingUp className="w-4 h-4 text-[var(--color-error)] transform rotate-180" />;
}
return <Activity className="w-4 h-4 text-gray-500" />;
};
const getTrendColor = (trend: number) => {
return trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]';
};
const getPerformanceColor = (value: number, target: number, inverse = false) => {
const comparison = inverse ? value < target : value >= target;
return comparison ? 'text-[var(--color-success)]' : value >= target * 0.9 ? 'text-yellow-600' : 'text-[var(--color-error)]';
};
const getAlertIcon = (type: string) => {
const getAlertIcon = (type: 'warning' | 'critical' | 'info') => {
switch (type) {
case 'critical':
return <AlertCircle className="w-5 h-5 text-[var(--color-error)]" />;
case 'warning':
return <AlertCircle className="w-5 h-5 text-yellow-600" />;
case 'success':
return <TrendingUp className="w-5 h-5 text-[var(--color-success)]" />;
default:
return <Activity className="w-5 h-5 text-[var(--color-info)]" />;
}
};
const getAlertColor = (type: string) => {
const getAlertColor = (type: 'warning' | 'critical' | 'info') => {
switch (type) {
case 'critical':
return 'bg-red-50 border-red-200';
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'success':
return 'bg-green-50 border-green-200';
default:
return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20';
}
};
// Prepare StatsGrid data (6 cross-functional metrics) - NO MEMOIZATION to avoid loops
const statsData = [
{
title: 'Tiempo de Ciclo',
value: cycleTime?.average_cycle_time || 0,
icon: Clock,
formatter: formatters.hours,
},
{
title: 'Eficiencia de Procesos',
value: processScore?.overall_score || 0,
icon: Zap,
formatter: formatters.percentage,
},
{
title: 'Utilización de Recursos',
value: resourceUtil?.overall_utilization || 0,
icon: Package,
formatter: formatters.percentage,
},
{
title: 'Ratio Costo-Ingreso',
value: costRevenue?.cost_revenue_ratio || 0,
icon: DollarSign,
formatter: formatters.percentage,
},
{
title: 'Índice de Calidad',
value: qualityIndex?.overall_quality_index || 0,
icon: Target,
formatter: formatters.percentage,
},
{
title: 'Cuellos de Botella Críticos',
value: bottlenecks?.critical_count || 0,
icon: AlertOctagon,
formatter: formatters.number,
},
];
return (
<div className="p-6 space-y-6">
{/* Page Header */}
<PageHeader
title="Análisis de Rendimiento"
description="Monitorea la eficiencia operativa y el rendimiento de todos los departamentos"
action={
<div className="flex space-x-2">
<Button variant="outline">
<Calendar className="w-4 h-4 mr-2" />
Configurar Alertas
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar Reporte
</Button>
</div>
}
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">
<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>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Período
</label>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value as TimePeriod)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md bg-white"
>
{timeframes.map(timeframe => (
<option key={timeframe.value} value={timeframe.value}>{timeframe.label}</option>
{timeframes.map((timeframe) => (
<option key={timeframe.value} value={timeframe.value}>
{timeframe.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Métrica Principal</label>
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="efficiency">Eficiencia</option>
<option value="productivity">Productividad</option>
<option value="quality">Calidad</option>
<option value="satisfaction">Satisfacción</option>
</select>
</div>
</div>
</Card>
{/* KPI Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpiTrends.map((kpi) => (
<Card key={kpi.name} className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-[var(--text-secondary)]">{kpi.name}</h3>
<div className={`w-3 h-3 rounded-full bg-${kpi.color}-500`}></div>
</div>
<div className="flex items-end justify-between">
<div>
<p className={`text-2xl font-bold ${getPerformanceColor(kpi.current, kpi.target, kpi.inverse)}`}>
{kpi.current}{kpi.unit}
</p>
<p className="text-xs text-[var(--text-tertiary)]">
Objetivo: {kpi.target}{kpi.unit}
</p>
</div>
<div className="text-right">
<div className="flex items-center">
{getTrendIcon(kpi.current - kpi.previous)}
<span className={`text-sm ml-1 ${getTrendColor(kpi.current - kpi.previous)}`}>
{Math.abs(kpi.current - kpi.previous).toFixed(1)}{kpi.unit}
</span>
</div>
</div>
</div>
<div className="mt-3 w-full bg-[var(--bg-quaternary)] rounded-full h-2">
<div
className={`bg-${kpi.color}-500 h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min((kpi.current / kpi.target) * 100, 100)}%` }}
></div>
</div>
</Card>
))}
</div>
{/* Block 1: StatsGrid with 6 cross-functional metrics */}
<StatsGrid stats={statsData} loading={isLoading} />
{/* Performance Alerts */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas de Rendimiento</h3>
<div className="space-y-3">
{performanceAlerts.map((alert) => (
<div key={alert.id} className={`p-4 rounded-lg border ${getAlertColor(alert.type)}`}>
<div className="flex items-start space-x-3">
{getAlertIcon(alert.type)}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-[var(--text-primary)]">{alert.title}</h4>
<Badge variant="gray">{alert.department}</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mt-1">{alert.description}</p>
<div className="flex items-center space-x-4 mt-2">
<span className="text-sm">
<strong>Actual:</strong> {alert.value}
</span>
<span className="text-sm">
<strong>Objetivo:</strong> {alert.target}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Block 2: Tabs */}
<Tabs items={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Department Performance */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Rendimiento por Departamento</h3>
<div className="space-y-4">
{departmentPerformance.map((dept) => (
<div key={dept.department} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<h4 className="font-medium text-[var(--text-primary)]">{dept.department}</h4>
<Badge variant="gray">{dept.employees} empleados</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-lg font-semibold text-[var(--text-primary)]">
{dept.efficiency}%
</span>
<div className="flex items-center">
{getTrendIcon(dept.trend)}
<span className={`text-sm ml-1 ${getTrendColor(dept.trend)}`}>
{Math.abs(dept.trend).toFixed(1)}%
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
{Object.entries(dept.metrics).map(([key, value]) => (
<div key={key}>
<p className="text-[var(--text-tertiary)] text-xs">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="font-medium">{value}</p>
{/* Block 3: Tab Content */}
<div className="space-y-6">
{/* 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>
<div className="space-y-4">
{departments.map((dept) => (
<div key={dept.department_id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-[var(--text-primary)]">
{dept.department_name}
</h4>
<div className="flex items-center space-x-2">
<span className="text-lg font-semibold text-[var(--text-primary)]">
{dept.efficiency.toFixed(1)}%
</span>
{getTrendIcon(dept.trend)}
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-[var(--text-tertiary)] text-xs">
{dept.metrics.primary_metric.label}
</p>
<p className="font-medium">
{dept.metrics.primary_metric.value.toFixed(1)}
{dept.metrics.primary_metric.unit}
</p>
</div>
<div>
<p className="text-[var(--text-tertiary)] text-xs">
{dept.metrics.secondary_metric.label}
</p>
<p className="font-medium">
{dept.metrics.secondary_metric.value.toFixed(1)}
{dept.metrics.secondary_metric.unit}
</p>
</div>
<div>
<p className="text-[var(--text-tertiary)] text-xs">
{dept.metrics.tertiary_metric.label}
</p>
<p className="font-medium">
{dept.metrics.tertiary_metric.value.toFixed(1)}
{dept.metrics.tertiary_metric.unit}
</p>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{dept.issues > 0 && (
<div className="mt-3 flex items-center text-sm text-[var(--color-warning)]">
<AlertCircle className="w-4 h-4 mr-1" />
{dept.issues} problema{dept.issues > 1 ? 's' : ''} detectado{dept.issues > 1 ? 's' : ''}
{/* 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>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={[
{ name: 'Producción', value: processScore.production_efficiency, weight: processScore.breakdown.production.weight },
{ name: 'Inventario', value: processScore.inventory_efficiency, weight: processScore.breakdown.inventory.weight },
{ name: 'Compras', value: processScore.procurement_efficiency, weight: processScore.breakdown.procurement.weight },
{ name: 'Pedidos', value: processScore.order_efficiency, weight: processScore.breakdown.orders.weight },
]}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="value" fill="var(--color-primary)" name="Eficiencia (%)" />
<Bar dataKey="weight" fill="var(--color-secondary)" name="Peso (%)" />
</BarChart>
</ResponsiveContainer>
</Card>
)}
</>
)}
{/* Eficiencia Operativa Tab */}
{activeTab === 'efficiency' && !isLoading && (
<>
{/* Cycle Time Breakdown */}
{cycleTime && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Análisis de Tiempo de Ciclo
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Pedido Producción</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{cycleTime.order_to_production_time.toFixed(1)}h
</p>
</div>
)}
</div>
))}
</div>
</Card>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Tiempo de Producción</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{cycleTime.production_time.toFixed(1)}h
</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Producción Entrega</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{cycleTime.production_to_delivery_time.toFixed(1)}h
</p>
</div>
</div>
<div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg">
<span className="text-sm font-medium">Tiempo Total de Ciclo</span>
<span className="text-xl font-bold text-[var(--color-primary)]">
{cycleTime.average_cycle_time.toFixed(1)}h
</span>
</div>
</Card>
)}
{/* Hourly Productivity */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Eficiencia por Hora</h3>
<div className="h-64 flex items-end space-x-1 justify-between">
{productivityData.map((data, index) => (
<div key={index} className="flex flex-col items-center flex-1">
<div className="text-xs text-[var(--text-secondary)] mb-1">{data.efficiency}%</div>
<div
className="w-full bg-[var(--color-info)]/50 rounded-t"
style={{
height: `${(data.efficiency / 100) * 200}px`,
minHeight: '8px',
backgroundColor: data.efficiency >= 90 ? '#10B981' : data.efficiency >= 80 ? '#F59E0B' : '#EF4444'
}}
></div>
<span className="text-xs text-[var(--text-tertiary)] mt-2 transform -rotate-45 origin-center">
{data.hour}
</span>
</div>
))}
</div>
<div className="mt-4 flex justify-center space-x-6 text-xs">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded mr-1"></div>
<span>90% Excelente</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-yellow-500 rounded mr-1"></div>
<span>80-89% Bueno</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded mr-1"></div>
<span>&lt;80% Bajo</span>
</div>
</div>
</Card>
{/* Bottlenecks Analysis */}
{bottlenecks && bottlenecks.bottlenecks.length > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Cuellos de Botella Detectados
</h3>
<div className="space-y-3">
{bottlenecks.bottlenecks.map((bottleneck, index) => (
<div
key={index}
className={`p-4 rounded-lg border ${
bottleneck.severity === 'high'
? 'bg-red-50 border-red-200'
: 'bg-yellow-50 border-yellow-200'
}`}
>
<div className="flex items-start space-x-3">
<AlertCircle
className={`w-5 h-5 ${
bottleneck.severity === 'high' ? 'text-red-600' : 'text-yellow-600'
}`}
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-[var(--text-primary)]">
{bottleneck.description}
</h4>
<Badge variant={bottleneck.severity === 'high' ? 'destructive' : 'default'}>
{bottleneck.area}
</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mt-1">
{bottleneck.metric}: {bottleneck.value.toFixed(1)}
</p>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{/* Resource Utilization */}
{resourceUtil && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Utilización de Recursos
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Equipamiento</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{resourceUtil.equipment_utilization.toFixed(1)}%
</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Inventario</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{resourceUtil.inventory_utilization.toFixed(1)}%
</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Balance</p>
<p className="text-lg font-bold text-[var(--text-primary)]">
{resourceUtil.resource_balance === 'balanced' ? 'Equilibrado' : 'Desbalanceado'}
</p>
</div>
</div>
</Card>
)}
</>
)}
{/* Impacto de Calidad Tab */}
{activeTab === 'quality' && !isLoading && (
<>
{/* Quality Index Overview */}
{qualityIndex && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Índice de Calidad General
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Calidad de Producción</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">
{qualityIndex.production_quality.toFixed(1)}%
</p>
</div>
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Calidad de Inventario</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">
{qualityIndex.inventory_quality.toFixed(1)}%
</p>
</div>
</div>
<div className="flex items-center justify-between p-4 bg-green-50 rounded-lg">
<span className="text-sm font-medium">Índice de Calidad Combinado</span>
<span className="text-2xl font-bold text-green-600">
{qualityIndex.overall_quality_index.toFixed(1)}%
</span>
</div>
</Card>
)}
{/* Quality Issues Breakdown */}
{qualityIndex && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Desglose de Problemas de Calidad
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<span className="text-sm font-medium">Defectos de Producción</span>
<span className="text-lg font-semibold text-red-600">
{qualityIndex.quality_issues.production_defects.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<span className="text-sm font-medium">Desperdicio</span>
<span className="text-lg font-semibold text-orange-600">
{qualityIndex.quality_issues.waste_percentage.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<span className="text-sm font-medium">Items por Vencer</span>
<span className="text-lg font-semibold text-yellow-600">
{qualityIndex.quality_issues.expiring_items}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<span className="text-sm font-medium">Stock Bajo Afectando Calidad</span>
<span className="text-lg font-semibold text-yellow-600">
{qualityIndex.quality_issues.low_stock_affecting_quality}
</span>
</div>
</div>
</Card>
)}
</>
)}
{/* Optimización Tab */}
{activeTab === 'optimization' && !isLoading && (
<>
{/* Cost-Revenue Analysis */}
{costRevenue && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Análisis de Rentabilidad
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Ingresos Totales</p>
<p className="text-3xl font-bold text-green-600">
{costRevenue.total_revenue.toLocaleString('es-ES')}
</p>
</div>
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Costos Estimados</p>
<p className="text-3xl font-bold text-red-600">
{costRevenue.estimated_costs.toLocaleString('es-ES')}
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg">
<span className="text-sm font-medium">Ratio Costo-Ingreso</span>
<span className="text-xl font-bold text-[var(--color-primary)]">
{costRevenue.cost_revenue_ratio.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between p-4 bg-green-50 rounded-lg">
<span className="text-sm font-medium">Margen de Beneficio</span>
<span className="text-xl font-bold text-green-600">
{costRevenue.profit_margin.toFixed(1)}%
</span>
</div>
</div>
</Card>
)}
{/* Improvement Recommendations */}
{bottlenecks && bottlenecks.total_bottlenecks > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Recomendaciones de Mejora
</h3>
<div className="space-y-4">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-[var(--text-primary)] mb-2">
Área más crítica: {bottlenecks.most_critical_area}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
Se han detectado {bottlenecks.critical_count} cuellos de botella críticos.
Prioriza la optimización de esta área para mejorar el flujo general.
</p>
</div>
{qualityIndex && qualityIndex.waste_impact > 5 && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-medium text-[var(--text-primary)] mb-2">
Reducir Desperdicio
</h4>
<p className="text-sm text-[var(--text-secondary)]">
El desperdicio actual es de {qualityIndex.waste_impact.toFixed(1)}%.
Implementa controles de calidad más estrictos para reducir pérdidas.
</p>
</div>
)}
{resourceUtil && resourceUtil.resource_balance === 'imbalanced' && (
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
<h4 className="font-medium text-[var(--text-primary)] mb-2">
Balance de Recursos
</h4>
<p className="text-sm text-[var(--text-secondary)]">
Los recursos están desbalanceados entre departamentos.
Considera redistribuir para optimizar la utilización general.
</p>
</div>
)}
</div>
</Card>
)}
{/* No Recommendations */}
{(!bottlenecks || bottlenecks.total_bottlenecks === 0) && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Recomendaciones de Mejora
</h3>
<div className="text-center py-8">
<Target className="w-12 h-12 mx-auto text-[var(--color-success)] mb-3" />
<p className="text-[var(--text-secondary)]">
¡Excelente! No se han detectado áreas críticas que requieran optimización inmediata.
</p>
</div>
</Card>
)}
</>
)}
</div>
</div>
);
};
export default PerformanceAnalyticsPage;
export default PerformanceAnalyticsPage;

View File

@@ -5,7 +5,7 @@
* Allows users to test different scenarios and see potential impacts on demand
*/
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useTenantStore } from '../../../../stores';
import { forecastingService } from '../../../../api/services/forecasting';
@@ -39,8 +39,11 @@ import {
ArrowDownRight,
Play,
Sparkles,
Package,
} from 'lucide-react';
import { PageHeader } from '../../../../components/layout';
import { useIngredients } from '../../../../api/hooks/inventory';
import { useModels } from '../../../../api/hooks/training';
export const ScenarioSimulationPage: React.FC = () => {
const { t } = useTranslation();
@@ -57,6 +60,43 @@ export const ScenarioSimulationPage: React.FC = () => {
const [durationDays, setDurationDays] = useState(7);
const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
// Fetch real inventory data
const {
data: ingredientsData,
isLoading: ingredientsLoading,
} = useIngredients(currentTenant?.id || '');
// Fetch trained models to filter products
const {
data: modelsData,
isLoading: modelsLoading,
} = useModels(currentTenant?.id || '', { active_only: true });
// Build products list from ingredients that have trained models
const availableProducts = useMemo(() => {
if (!ingredientsData || !modelsData) {
return [];
}
// Handle both array and paginated response formats
const modelsList = Array.isArray(modelsData) ? modelsData : (modelsData.models || modelsData.items || []);
// Get inventory product IDs that have trained models
const modelProductIds = new Set(modelsList.map((model: any) => model.inventory_product_id));
// Filter ingredients to only those with models
const ingredientsWithModels = ingredientsData.filter(ingredient =>
modelProductIds.has(ingredient.id)
);
return ingredientsWithModels.map(ingredient => ({
id: ingredient.id,
name: ingredient.name,
category: ingredient.category || 'Other',
hasModel: true
}));
}, [ingredientsData, modelsData]);
// Scenario-specific parameters
const [weatherParams, setWeatherParams] = useState<WeatherScenario>({
temperature_change: 15,
@@ -81,6 +121,16 @@ export const ScenarioSimulationPage: React.FC = () => {
promotion_type: 'discount',
expected_traffic_increase: 0.3,
});
const [holidayParams, setHolidayParams] = useState({
holiday_name: 'christmas',
expected_impact_multiplier: 1.5,
});
const [supplyDisruptionParams, setSupplyDisruptionParams] = useState({
severity: 'moderate',
affected_percentage: 30,
duration_days: 7,
});
const [customParams, setCustomParams] = useState<Record<string, number>>({});
const handleSimulate = async () => {
if (!currentTenant?.id) return;
@@ -119,6 +169,15 @@ export const ScenarioSimulationPage: React.FC = () => {
case ScenarioType.PROMOTION:
request.promotion_params = promotionParams;
break;
case ScenarioType.HOLIDAY:
request.custom_multipliers = { holiday_multiplier: holidayParams.expected_impact_multiplier };
break;
case ScenarioType.SUPPLY_DISRUPTION:
request.custom_multipliers = { disruption_severity: supplyDisruptionParams.affected_percentage / 100 };
break;
case ScenarioType.CUSTOM:
request.custom_multipliers = customParams;
break;
}
const result = await forecastingService.simulateScenario(currentTenant.id, request);
@@ -201,7 +260,7 @@ export const ScenarioSimulationPage: React.FC = () => {
value={scenarioName}
onChange={(e) => setScenarioName(e.target.value)}
placeholder={t('analytics.scenario_simulation.scenario_name_placeholder', 'e.g., Summer Heatwave Impact')}
className="w-full px-3 py-2 border rounded-lg"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
@@ -215,7 +274,7 @@ export const ScenarioSimulationPage: React.FC = () => {
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={new Date().toISOString().split('T')[0]}
/>
</div>
@@ -229,10 +288,92 @@ export const ScenarioSimulationPage: React.FC = () => {
onChange={(e) => setDurationDays(parseInt(e.target.value) || 7)}
min={1}
max={30}
className="w-full px-3 py-2 border rounded-lg"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
{/* Product Selection */}
<div className="mt-4 space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium flex items-center gap-2">
<Package className="w-4 h-4" />
{t('analytics.scenario_simulation.select_products', 'Select Products to Simulate')}
</label>
<span className="text-xs text-gray-500">
{selectedProducts.length} selected
</span>
</div>
{ingredientsLoading || modelsLoading ? (
<div className="p-4 border rounded-lg text-center text-sm text-gray-500">
Loading products...
</div>
) : availableProducts.length === 0 ? (
<div className="p-4 border border-amber-200 bg-amber-50 rounded-lg">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-amber-800">
<p className="font-medium">No products available for simulation</p>
<p className="mt-1">You need to train ML models for your products first. Visit the Training section to get started.</p>
</div>
</div>
</div>
) : (
<>
{/* Quick Select All/None */}
<div className="flex gap-2">
<button
onClick={() => setSelectedProducts(availableProducts.map(p => p.id))}
className="text-xs text-blue-600 hover:text-blue-700 font-medium"
>
Select All
</button>
<span className="text-xs text-gray-300">|</span>
<button
onClick={() => setSelectedProducts([])}
className="text-xs text-gray-600 hover:text-gray-700 font-medium"
>
Clear All
</button>
</div>
{/* Product Grid */}
<div className="max-h-64 overflow-y-auto border border-gray-300 dark:border-gray-600 rounded-lg p-3 space-y-2 bg-gray-50 dark:bg-gray-800">
{availableProducts.map((product) => (
<label
key={product.id}
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition-all ${
selectedProducts.includes(product.id)
? 'border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/30'
: 'border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-500'
}`}
>
<input
type="checkbox"
checked={selectedProducts.includes(product.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts([...selectedProducts, product.id]);
} else {
setSelectedProducts(selectedProducts.filter(id => id !== product.id));
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-500 rounded focus:ring-blue-500 dark:bg-gray-600"
/>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{product.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{product.category}</div>
</div>
<Badge variant="success" className="text-xs">
ML Ready
</Badge>
</label>
))}
</div>
</>
)}
</div>
</div>
<div>
@@ -274,7 +415,7 @@ export const ScenarioSimulationPage: React.FC = () => {
type="number"
value={weatherParams.temperature_change || 0}
onChange={(e) => setWeatherParams({ ...weatherParams, temperature_change: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={-30}
max={30}
/>
@@ -284,7 +425,7 @@ export const ScenarioSimulationPage: React.FC = () => {
<select
value={weatherParams.weather_type || 'heatwave'}
onChange={(e) => setWeatherParams({ ...weatherParams, weather_type: e.target.value })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="heatwave">Heatwave</option>
<option value="cold_snap">Cold Snap</option>
@@ -303,7 +444,7 @@ export const ScenarioSimulationPage: React.FC = () => {
type="number"
value={competitionParams.new_competitors}
onChange={(e) => setCompetitionParams({ ...competitionParams, new_competitors: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={1}
max={10}
/>
@@ -315,7 +456,7 @@ export const ScenarioSimulationPage: React.FC = () => {
step="0.1"
value={competitionParams.distance_km}
onChange={(e) => setCompetitionParams({ ...competitionParams, distance_km: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0.1}
max={10}
/>
@@ -326,7 +467,7 @@ export const ScenarioSimulationPage: React.FC = () => {
type="number"
value={competitionParams.estimated_market_share_loss * 100}
onChange={(e) => setCompetitionParams({ ...competitionParams, estimated_market_share_loss: parseFloat(e.target.value) / 100 })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={50}
/>
@@ -342,10 +483,24 @@ export const ScenarioSimulationPage: React.FC = () => {
type="number"
value={promotionParams.discount_percent}
onChange={(e) => setPromotionParams({ ...promotionParams, discount_percent: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={75}
/>
<p className="text-xs text-gray-500 mt-1">Typical range: 10-30%</p>
</div>
<div>
<label className="text-sm">Promotion Type</label>
<select
value={promotionParams.promotion_type}
onChange={(e) => setPromotionParams({ ...promotionParams, promotion_type: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="discount">Discount</option>
<option value="bogo">Buy One Get One</option>
<option value="bundle">Bundle Deal</option>
<option value="flash_sale">Flash Sale</option>
</select>
</div>
<div>
<label className="text-sm">Expected Traffic Increase (%)</label>
@@ -353,10 +508,208 @@ export const ScenarioSimulationPage: React.FC = () => {
type="number"
value={promotionParams.expected_traffic_increase * 100}
onChange={(e) => setPromotionParams({ ...promotionParams, expected_traffic_increase: parseFloat(e.target.value) / 100 })}
className="w-full px-3 py-2 border rounded-lg mt-1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={200}
/>
<p className="text-xs text-gray-500 mt-1">Most promotions see 20-50% increase</p>
</div>
</div>
)}
{selectedScenarioType === ScenarioType.EVENT && (
<div className="space-y-3">
<div>
<label className="text-sm">Event Type</label>
<select
value={eventParams.event_type}
onChange={(e) => setEventParams({ ...eventParams, event_type: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="festival">Festival</option>
<option value="sports">Sports Event</option>
<option value="concert">Concert</option>
<option value="conference">Conference</option>
<option value="market">Street Market</option>
</select>
</div>
<div>
<label className="text-sm">Expected Attendance</label>
<input
type="number"
value={eventParams.expected_attendance}
onChange={(e) => setEventParams({ ...eventParams, expected_attendance: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={100}
step={100}
/>
<p className="text-xs text-gray-500 mt-1">Number of people expected</p>
</div>
<div>
<label className="text-sm">Distance from Location (km)</label>
<input
type="number"
step="0.1"
value={eventParams.distance_km}
onChange={(e) => setEventParams({ ...eventParams, distance_km: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={50}
/>
<p className="text-xs text-gray-500 mt-1">Closer events have bigger impact</p>
</div>
<div>
<label className="text-sm">Event Duration (days)</label>
<input
type="number"
value={eventParams.duration_days}
onChange={(e) => setEventParams({ ...eventParams, duration_days: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={1}
max={30}
/>
</div>
</div>
)}
{selectedScenarioType === ScenarioType.PRICING && (
<div className="space-y-3">
<div>
<label className="text-sm">Price Change (%)</label>
<input
type="number"
value={pricingParams.price_change_percent}
onChange={(e) => setPricingParams({ ...pricingParams, price_change_percent: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={-50}
max={100}
/>
<p className="text-xs text-gray-500 mt-1">
Negative for price decrease, positive for increase
</p>
</div>
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="text-xs text-blue-800">
<p className="font-medium">💡 Pricing Impact Guide:</p>
<ul className="mt-1 space-y-1 ml-4 list-disc">
<li>-10% price: Usually +8-12% demand</li>
<li>+10% price: Usually -8-12% demand</li>
<li>Impact varies by product type and competition</li>
</ul>
</div>
</div>
</div>
)}
{selectedScenarioType === ScenarioType.HOLIDAY && (
<div className="space-y-3">
<div>
<label className="text-sm">Holiday Period</label>
<select
value={holidayParams.holiday_name}
onChange={(e) => setHolidayParams({ ...holidayParams, holiday_name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="christmas">Christmas</option>
<option value="new_year">New Year</option>
<option value="easter">Easter</option>
<option value="valentines">Valentine's Day</option>
<option value="mothers_day">Mother's Day</option>
<option value="thanksgiving">Thanksgiving</option>
<option value="halloween">Halloween</option>
</select>
</div>
<div>
<label className="text-sm">Expected Impact Multiplier</label>
<input
type="number"
step="0.1"
value={holidayParams.expected_impact_multiplier}
onChange={(e) => setHolidayParams({ ...holidayParams, expected_impact_multiplier: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0.5}
max={3}
/>
<p className="text-xs text-gray-500 mt-1">
1.0 = no change, 1.5 = 50% increase, 0.8 = 20% decrease
</p>
</div>
</div>
)}
{selectedScenarioType === ScenarioType.SUPPLY_DISRUPTION && (
<div className="space-y-3">
<div>
<label className="text-sm">Disruption Severity</label>
<select
value={supplyDisruptionParams.severity}
onChange={(e) => setSupplyDisruptionParams({ ...supplyDisruptionParams, severity: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="minor">Minor (10-20% affected)</option>
<option value="moderate">Moderate (20-40% affected)</option>
<option value="major">Major (40-60% affected)</option>
<option value="severe">Severe (60%+ affected)</option>
</select>
</div>
<div>
<label className="text-sm">Affected Supply (%)</label>
<input
type="number"
value={supplyDisruptionParams.affected_percentage}
onChange={(e) => setSupplyDisruptionParams({ ...supplyDisruptionParams, affected_percentage: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={100}
/>
<p className="text-xs text-gray-500 mt-1">
Percentage of normal supply that will be unavailable
</p>
</div>
<div>
<label className="text-sm">Expected Duration (days)</label>
<input
type="number"
value={supplyDisruptionParams.duration_days}
onChange={(e) => setSupplyDisruptionParams({ ...supplyDisruptionParams, duration_days: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={1}
max={30}
/>
</div>
</div>
)}
{selectedScenarioType === ScenarioType.CUSTOM && (
<div className="space-y-3">
<div className="p-3 bg-purple-50 border border-purple-200 rounded-lg">
<p className="text-sm text-purple-800 font-medium">Custom Scenario</p>
<p className="text-xs text-purple-700 mt-1">
Define your own demand multiplier for unique situations
</p>
</div>
<div>
<label className="text-sm">Demand Multiplier</label>
<input
type="number"
step="0.1"
value={customParams.custom_multiplier || 1.0}
onChange={(e) => setCustomParams({ custom_multiplier: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min={0}
max={5}
/>
<p className="text-xs text-gray-500 mt-1">
1.0 = no change, 1.5 = 50% increase, 0.7 = 30% decrease
</p>
</div>
<div>
<label className="text-sm">Scenario Description (optional)</label>
<textarea
placeholder="Describe what you're simulating..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3}
/>
</div>
</div>
)}
@@ -377,43 +730,138 @@ export const ScenarioSimulationPage: React.FC = () => {
{/* Quick Examples */}
<Card>
<div className="p-6">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
{t('analytics.scenario_simulation.quick_examples', 'Quick Examples')}
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2">
<Lightbulb className="w-5 h-5 text-yellow-500" />
{t('analytics.scenario_simulation.quick_examples', 'Quick Start Templates')}
</h3>
<p className="text-xs text-gray-600 mb-4">
Click any template to pre-fill the scenario parameters
</p>
<div className="space-y-2">
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.WEATHER);
setScenarioName('Summer Heatwave Next Week');
setScenarioName('Summer Heatwave Impact');
setWeatherParams({ temperature_change: 15, weather_type: 'heatwave' });
setDurationDays(7);
}}
className="w-full text-left px-3 py-2 border rounded-lg hover:bg-gray-50 text-sm"
className="w-full text-left px-4 py-3 border-2 border-orange-200 bg-orange-50 rounded-lg hover:bg-orange-100 hover:border-orange-300 transition-all group"
>
<Sun className="w-4 h-4 inline mr-2" />
What if a heatwave hits next week?
<div className="flex items-start gap-3">
<Sun className="w-5 h-5 text-orange-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">Summer Heatwave</div>
<div className="text-xs text-gray-600 mt-1">
+15°C temperature spike - See impact on cold drinks & ice cream
</div>
</div>
<Badge variant="warning" className="text-xs">+20-40%</Badge>
</div>
</button>
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.COMPETITION);
setScenarioName('New Bakery Opening Nearby');
setScenarioName('New Competitor Opening');
setCompetitionParams({ new_competitors: 1, distance_km: 0.3, estimated_market_share_loss: 0.2 });
setDurationDays(30);
}}
className="w-full text-left px-3 py-2 border rounded-lg hover:bg-gray-50 text-sm"
className="w-full text-left px-4 py-3 border-2 border-red-200 bg-red-50 rounded-lg hover:bg-red-100 hover:border-red-300 transition-all"
>
<Users className="w-4 h-4 inline mr-2" />
How would a new competitor affect sales?
<div className="flex items-start gap-3">
<Users className="w-5 h-5 text-red-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">New Nearby Competitor</div>
<div className="text-xs text-gray-600 mt-1">
Bakery opening 300m away - Est. 20% market share loss
</div>
</div>
<Badge variant="error" className="text-xs">-15-25%</Badge>
</div>
</button>
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.PROMOTION);
setScenarioName('Weekend Flash Sale');
setPromotionParams({ discount_percent: 25, promotion_type: 'flash_sale', expected_traffic_increase: 0.5 });
setDurationDays(3);
}}
className="w-full text-left px-3 py-2 border rounded-lg hover:bg-gray-50 text-sm"
className="w-full text-left px-4 py-3 border-2 border-green-200 bg-green-50 rounded-lg hover:bg-green-100 hover:border-green-300 transition-all"
>
<Tag className="w-4 h-4 inline mr-2" />
Impact of a 25% weekend promotion?
<div className="flex items-start gap-3">
<Tag className="w-5 h-5 text-green-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">Weekend Flash Sale</div>
<div className="text-xs text-gray-600 mt-1">
25% discount + 50% traffic boost - Test promotion impact
</div>
</div>
<Badge variant="success" className="text-xs">+40-60%</Badge>
</div>
</button>
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.EVENT);
setScenarioName('Local Festival Impact');
setEventParams({ event_type: 'festival', expected_attendance: 5000, distance_km: 0.5, duration_days: 3 });
setDurationDays(3);
}}
className="w-full text-left px-4 py-3 border-2 border-purple-200 bg-purple-50 rounded-lg hover:bg-purple-100 hover:border-purple-300 transition-all"
>
<div className="flex items-start gap-3">
<Calendar className="w-5 h-5 text-purple-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">Local Festival</div>
<div className="text-xs text-gray-600 mt-1">
5,000 attendees 500m away - Capture festival traffic
</div>
</div>
<Badge variant="primary" className="text-xs">+30-50%</Badge>
</div>
</button>
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.HOLIDAY);
setScenarioName('Christmas Holiday Rush');
setHolidayParams({ holiday_name: 'christmas', expected_impact_multiplier: 1.8 });
setDurationDays(14);
}}
className="w-full text-left px-4 py-3 border-2 border-blue-200 bg-blue-50 rounded-lg hover:bg-blue-100 hover:border-blue-300 transition-all"
>
<div className="flex items-start gap-3">
<Sparkles className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">Christmas Season</div>
<div className="text-xs text-gray-600 mt-1">
Holiday demand spike - Plan for seasonal products
</div>
</div>
<Badge variant="primary" className="text-xs">+60-100%</Badge>
</div>
</button>
<button
onClick={() => {
setSelectedScenarioType(ScenarioType.PRICING);
setScenarioName('Price Adjustment Test');
setPricingParams({ price_change_percent: -10 });
setDurationDays(14);
}}
className="w-full text-left px-4 py-3 border-2 border-indigo-200 bg-indigo-50 rounded-lg hover:bg-indigo-100 hover:border-indigo-300 transition-all"
>
<div className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-indigo-600 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">Price Decrease</div>
<div className="text-xs text-gray-600 mt-1">
10% price reduction - Test elasticity and demand response
</div>
</div>
<Badge variant="info" className="text-xs">+8-15%</Badge>
</div>
</button>
</div>
</div>
@@ -438,31 +886,52 @@ export const ScenarioSimulationPage: React.FC = () => {
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-sm text-gray-500 mb-1">Baseline Demand</div>
<div className="text-2xl font-bold">{Math.round(simulationResult.total_baseline_demand)}</div>
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="text-xs text-gray-500 mb-1 uppercase tracking-wide">Baseline Demand</div>
<div className="text-2xl font-bold text-gray-900">{Math.round(simulationResult.total_baseline_demand)}</div>
<div className="text-xs text-gray-500 mt-1">units expected normally</div>
</div>
<div className="p-4 bg-blue-50 rounded-lg">
<div className="text-sm text-gray-500 mb-1">Scenario Demand</div>
<div className="text-2xl font-bold">{Math.round(simulationResult.total_scenario_demand)}</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="text-xs text-blue-600 mb-1 uppercase tracking-wide">Scenario Demand</div>
<div className="text-2xl font-bold text-blue-900">{Math.round(simulationResult.total_scenario_demand)}</div>
<div className="text-xs text-blue-600 mt-1">units with this scenario</div>
</div>
</div>
<div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg">
<div className={`p-5 rounded-xl border-2 ${
simulationResult.overall_impact_percent > 0
? 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-200'
: 'bg-gradient-to-r from-red-50 to-orange-50 border-red-200'
}`}>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Overall Impact</span>
<div className="flex items-center gap-2">
{simulationResult.overall_impact_percent > 0 ? (
<ArrowUpRight className="w-5 h-5 text-green-500" />
) : (
<ArrowDownRight className="w-5 h-5 text-red-500" />
)}
<span className={`text-2xl font-bold ${
simulationResult.overall_impact_percent > 0 ? 'text-green-600' : 'text-red-600'
}`}>
{simulationResult.overall_impact_percent > 0 ? '+' : ''}
{simulationResult.overall_impact_percent.toFixed(1)}%
</span>
<div>
<div className="text-xs font-medium text-gray-600 mb-1 uppercase tracking-wide">Overall Impact</div>
<div className="flex items-center gap-2">
{simulationResult.overall_impact_percent > 0 ? (
<div className="flex items-center gap-1 text-green-700">
<ArrowUpRight className="w-6 h-6" />
<span className="text-3xl font-bold">
+{simulationResult.overall_impact_percent.toFixed(1)}%
</span>
</div>
) : (
<div className="flex items-center gap-1 text-red-700">
<ArrowDownRight className="w-6 h-6" />
<span className="text-3xl font-bold">
{simulationResult.overall_impact_percent.toFixed(1)}%
</span>
</div>
)}
</div>
</div>
<div className={`px-4 py-2 rounded-lg ${
simulationResult.overall_impact_percent > 0
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
<div className="text-xs font-medium">
{simulationResult.overall_impact_percent > 0 ? '📈 Increased Demand' : '📉 Decreased Demand'}
</div>
</div>
</div>
</div>
@@ -472,15 +941,15 @@ export const ScenarioSimulationPage: React.FC = () => {
{/* Insights */}
<Card>
<div className="p-6">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
{t('analytics.scenario_simulation.insights', 'Key Insights')}
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2">
<Lightbulb className="w-5 h-5 text-yellow-500" />
<span>{t('analytics.scenario_simulation.insights', 'Key Insights')}</span>
</h3>
<div className="space-y-2">
<div className="space-y-3">
{simulationResult.insights.map((insight, index) => (
<div key={index} className="flex items-start gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
<span>{insight}</span>
<div key={index} className="flex items-start gap-3 p-3 bg-blue-50 border border-blue-100 rounded-lg">
<CheckCircle className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-800">{insight}</span>
</div>
))}
</div>
@@ -490,15 +959,17 @@ export const ScenarioSimulationPage: React.FC = () => {
{/* Recommendations */}
<Card>
<div className="p-6">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
{t('analytics.scenario_simulation.recommendations', 'Recommendations')}
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-green-600" />
<span>{t('analytics.scenario_simulation.recommendations', 'Action Plan')}</span>
</h3>
<div className="space-y-2">
<div className="space-y-3">
{simulationResult.recommendations.map((recommendation, index) => (
<div key={index} className="flex items-start gap-2 text-sm p-3 bg-blue-50 rounded-lg">
<span className="font-medium text-blue-600">{index + 1}.</span>
<span>{recommendation}</span>
<div key={index} className="flex items-start gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-lg hover:shadow-sm transition-shadow">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-green-600 text-white font-bold text-xs flex-shrink-0">
{index + 1}
</div>
<span className="text-sm text-gray-800 font-medium">{recommendation}</span>
</div>
))}
</div>
@@ -509,29 +980,70 @@ export const ScenarioSimulationPage: React.FC = () => {
{simulationResult.product_impacts.length > 0 && (
<Card>
<div className="p-6">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
{t('analytics.scenario_simulation.product_impacts', 'Product Impacts')}
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-purple-600" />
<span>{t('analytics.scenario_simulation.product_impacts', 'Product-Level Impact')}</span>
</h3>
<div className="space-y-3">
{simulationResult.product_impacts.map((impact, index) => (
<div key={index} className="p-3 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{impact.inventory_product_id}</span>
<span className={`text-sm font-bold ${
impact.demand_change_percent > 0 ? 'text-green-600' : 'text-red-600'
}`}>
{impact.demand_change_percent > 0 ? '+' : ''}
{impact.demand_change_percent.toFixed(1)}%
</span>
{simulationResult.product_impacts.map((impact, index) => {
const impactPercent = Math.abs(impact.demand_change_percent);
const isPositive = impact.demand_change_percent > 0;
return (
<div key={index} className={`p-4 border-2 rounded-lg ${
isPositive ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'
}`}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Package className="w-4 h-4 text-gray-600" />
<span className="text-sm font-semibold text-gray-900">
{availableProducts.find(p => p.id === impact.inventory_product_id)?.name || impact.inventory_product_id}
</span>
</div>
<div className={`flex items-center gap-1 px-3 py-1 rounded-full ${
isPositive ? 'bg-green-100' : 'bg-red-100'
}`}>
{isPositive ? (
<ArrowUpRight className="w-4 h-4 text-green-700" />
) : (
<ArrowDownRight className="w-4 h-4 text-red-700" />
)}
<span className={`text-sm font-bold ${
isPositive ? 'text-green-700' : 'text-red-700'
}`}>
{isPositive ? '+' : ''}{impact.demand_change_percent.toFixed(1)}%
</span>
</div>
</div>
{/* Visual bar showing demand change */}
<div className="mb-2">
<div className="flex items-center justify-between text-xs text-gray-600 mb-1">
<span>Baseline: {Math.round(impact.baseline_demand)} units</span>
<span>Scenario: {Math.round(impact.simulated_demand)} units</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full ${isPositive ? 'bg-green-500' : 'bg-red-500'}`}
style={{ width: `${Math.min(impactPercent, 100)}%` }}
/>
</div>
</div>
{/* Traffic light risk indicator */}
<div className="flex items-center gap-2 mt-2">
<div className="flex gap-1">
<div className={`w-2 h-2 rounded-full ${impactPercent > 30 ? 'bg-red-500' : 'bg-gray-300'}`} />
<div className={`w-2 h-2 rounded-full ${impactPercent > 15 && impactPercent <= 30 ? 'bg-yellow-500' : 'bg-gray-300'}`} />
<div className={`w-2 h-2 rounded-full ${impactPercent <= 15 ? 'bg-green-500' : 'bg-gray-300'}`} />
</div>
<span className="text-xs text-gray-600">
{impactPercent > 30 ? '🔴 High impact' : impactPercent > 15 ? '🟡 Medium impact' : '🟢 Low impact'}
</span>
</div>
</div>
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Baseline: {Math.round(impact.baseline_demand)}</span>
<span></span>
<span>Scenario: {Math.round(impact.simulated_demand)}</span>
</div>
</div>
))}
);
})}
</div>
</div>
</Card>

View File

@@ -205,13 +205,12 @@ export const DemoPage: React.FC = () => {
</div>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-6xl">
<span className="block">Prueba BakeryIA</span>
<span className="block">Prueba El Panadero Digital</span>
<span className="block text-[var(--color-primary)]">sin compromiso</span>
</h1>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
Explora nuestro sistema con datos reales de panaderías españolas.
Elige el tipo de negocio que mejor se adapte a tu caso.
Elige el tipo de panadería que se ajuste a tu negocio
</p>
<div className="mt-8 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
@@ -225,7 +224,7 @@ export const DemoPage: React.FC = () => {
</div>
<div className="flex items-center">
<Shield className="w-4 h-4 text-green-500 mr-2" />
Datos aislados y seguros
Datos reales en español
</div>
</div>
</div>
@@ -258,10 +257,14 @@ export const DemoPage: React.FC = () => {
</div>
<div className="ml-4">
<h2 className="text-2xl font-bold text-[var(--text-primary)]">
{account.name}
{account.account_type === 'individual_bakery'
? 'Panadería Individual con Producción local'
: 'Panadería Franquiciada con Obrador Central'}
</h2>
<p className="text-sm text-[var(--text-tertiary)] mt-1">
{account.business_model}
{account.account_type === 'individual_bakery'
? account.business_model
: 'Punto de Venta + Obrador Central'}
</p>
</div>
</div>
@@ -275,9 +278,57 @@ export const DemoPage: React.FC = () => {
{account.description}
</p>
{/* Key Characteristics */}
<div className="mb-6 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-default)]">
<p className="text-xs font-semibold text-[var(--text-tertiary)] uppercase mb-3">
Características del negocio
</p>
<div className="grid grid-cols-2 gap-3 text-sm">
{account.account_type === 'individual_bakery' ? (
<>
<div>
<span className="text-[var(--text-tertiary)]">Empleados:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">~8</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Turnos:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">1/día</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Ventas:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">Directas</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Productos:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">Local</span>
</div>
</>
) : (
<>
<div>
<span className="text-[var(--text-tertiary)]">Empleados:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">~5-6</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Turnos:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">2/día</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Modelo:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">Franquicia</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Productos:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">De obrador</span>
</div>
</>
)}
</div>
</div>
{/* Features */}
{account.features && account.features.length > 0 && (
<div className="mb-6 space-y-2">
<div className="mb-8 space-y-2">
<p className="text-sm font-semibold text-[var(--text-primary)] mb-3">
Funcionalidades incluidas:
</p>
@@ -290,31 +341,16 @@ export const DemoPage: React.FC = () => {
</div>
)}
{/* Demo Benefits */}
<div className="space-y-2 mb-8 pt-6 border-t border-[var(--border-default)]">
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Check className="w-4 h-4 mr-2 text-green-500" />
Datos reales en español
</div>
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Check className="w-4 h-4 mr-2 text-green-500" />
Sesión aislada de 30 minutos
</div>
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Check className="w-4 h-4 mr-2 text-green-500" />
Sin necesidad de registro
</div>
</div>
{/* CTA Button */}
<Button
onClick={() => handleStartDemo(account.account_type)}
disabled={creatingSession}
size="lg"
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
className="w-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:from-[var(--color-primary-dark)] hover:to-[var(--color-primary)] text-white shadow-lg hover:shadow-2xl transform hover:scale-[1.02] transition-all duration-200 font-semibold text-base py-4"
>
<Play className="mr-2 w-5 h-5" />
Probar Demo Ahora
Iniciar Demo
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</div>
</div>

View File

@@ -284,28 +284,28 @@ const LandingPage: React.FC = () => {
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 rounded-2xl p-8 border-2 border-amber-200 dark:border-amber-800">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 bg-amber-600 rounded-xl flex items-center justify-center">
<Network className="w-8 h-8 text-white" />
<Store className="w-8 h-8 text-white" />
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)]">{t('landing:business_models.central_workshop.title', 'Obrador Central + Puntos de Venta')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('landing:business_models.central_workshop.subtitle', 'Producción centralizada, distribución múltiple')}</p>
<h3 className="text-2xl font-bold text-[var(--text-primary)]">{t('landing:business_models.central_workshop.title', 'Panadería Franquiciada')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('landing:business_models.central_workshop.subtitle', 'Punto de venta con obrador central')}</p>
</div>
</div>
<p className="text-[var(--text-secondary)] mb-6 leading-relaxed">
{t('landing:business_models.central_workshop.description', 'Produces centralmente y distribuyes a múltiples puntos de venta. Necesitas coordinar producción, logística y demanda entre ubicaciones para optimizar cada punto.')}
{t('landing:business_models.central_workshop.description', 'Operas un punto de venta que recibe productos de un obrador central. Necesitas gestionar pedidos, inventario y ventas para optimizar tu operación retail.')}
</p>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.prediction', '<strong>Predicción agregada y por punto de venta</strong> individual') }} />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.prediction', '<strong>Gestión de pedidos</strong> al obrador central') }} />
</div>
<div className="flex items-start gap-3">
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.distribution', '<strong>Gestión de distribución</strong> multi-ubicación coordinada') }} />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.distribution', '<strong>Control de inventario</strong> de productos recibidos') }} />
</div>
<div className="flex items-start gap-3">
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.visibility', '<strong>Visibilidad centralizada</strong> con control granular') }} />
<span className="text-sm text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:business_models.central_workshop.features.visibility', '<strong>Previsión de ventas</strong> para tu punto') }} />
</div>
</div>
</div>