Improve the frontend 4
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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><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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user