diff --git a/docker-compose.yml b/docker-compose.yml index 7a5b7ad..d711ad0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,8 @@ services: container_name: beyond-backend environment: # credenciales del API (las mismas que usas ahora) - BASIC_AUTH_USERNAME: admin - BASIC_AUTH_PASSWORD: admin + BASIC_AUTH_USERNAME: "beyond" + BASIC_AUTH_PASSWORD: "beyond2026" expose: - "8000" networks: @@ -34,7 +34,9 @@ services: - frontend ports: - "80:80" + - "443:443" volumes: + - /etc/letsencrypt:/etc/letsencrypt:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro networks: - beyond-net diff --git a/frontend/components/DashboardHeader.tsx b/frontend/components/DashboardHeader.tsx new file mode 100644 index 0000000..827252a --- /dev/null +++ b/frontend/components/DashboardHeader.tsx @@ -0,0 +1,90 @@ +import { motion } from 'framer-motion'; +import { LayoutDashboard, Layers, Bot, Map } from 'lucide-react'; + +export type TabId = 'executive' | 'dimensions' | 'readiness' | 'roadmap'; + +export interface TabConfig { + id: TabId; + label: string; + icon: React.ElementType; +} + +interface DashboardHeaderProps { + title?: string; + activeTab: TabId; + onTabChange: (id: TabId) => void; +} + +const TABS: TabConfig[] = [ + { id: 'executive', label: 'Resumen', icon: LayoutDashboard }, + { id: 'dimensions', label: 'Dimensiones', icon: Layers }, + { id: 'readiness', label: 'Agentic Readiness', icon: Bot }, + { id: 'roadmap', label: 'Roadmap', icon: Map }, +]; + +const formatDate = (): string => { + const now = new Date(); + const months = [ + 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', + 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre' + ]; + return `${months[now.getMonth()]} ${now.getFullYear()}`; +}; + +export function DashboardHeader({ + title = 'AIR EUROPA - Beyond CX Analytics', + activeTab, + onTabChange +}: DashboardHeaderProps) { + return ( +
+ {/* Top row: Title and Date */} +
+
+

{title}

+ {formatDate()} +
+
+ + {/* Tab Navigation */} + +
+ ); +} + +export default DashboardHeader; diff --git a/frontend/components/DashboardTabs.tsx b/frontend/components/DashboardTabs.tsx new file mode 100644 index 0000000..543406e --- /dev/null +++ b/frontend/components/DashboardTabs.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowLeft } from 'lucide-react'; +import { DashboardHeader, TabId } from './DashboardHeader'; +import { ExecutiveSummaryTab } from './tabs/ExecutiveSummaryTab'; +import { DimensionAnalysisTab } from './tabs/DimensionAnalysisTab'; +import { AgenticReadinessTab } from './tabs/AgenticReadinessTab'; +import { RoadmapTab } from './tabs/RoadmapTab'; +import type { AnalysisData } from '../types'; + +interface DashboardTabsProps { + data: AnalysisData; + title?: string; + onBack?: () => void; +} + +export function DashboardTabs({ + data, + title = 'AIR EUROPA - Beyond CX Analytics', + onBack +}: DashboardTabsProps) { + const [activeTab, setActiveTab] = useState('executive'); + + const renderTabContent = () => { + switch (activeTab) { + case 'executive': + return ; + case 'dimensions': + return ; + case 'readiness': + return ; + case 'roadmap': + return ; + default: + return ; + } + }; + + return ( +
+ {/* Back button */} + {onBack && ( +
+
+ +
+
+ )} + + {/* Sticky Header with Tabs */} + + + {/* Tab Content */} +
+ + + {renderTabContent()} + + +
+ + {/* Footer */} +
+
+
+ Beyond Diagnosis - Contact Center Analytics Platform + + Análisis: {data.tier ? data.tier.toUpperCase() : 'GOLD'} | + Fuente: {data.source || 'synthetic'} + +
+
+
+
+ ); +} + +export default DashboardTabs; diff --git a/frontend/components/SinglePageDataRequestIntegrated.tsx b/frontend/components/SinglePageDataRequestIntegrated.tsx index 56a19f3..7b423ea 100644 --- a/frontend/components/SinglePageDataRequestIntegrated.tsx +++ b/frontend/components/SinglePageDataRequestIntegrated.tsx @@ -7,7 +7,7 @@ import { Toaster } from 'react-hot-toast'; import { TierKey, AnalysisData } from '../types'; import TierSelectorEnhanced from './TierSelectorEnhanced'; import DataInputRedesigned from './DataInputRedesigned'; -import DashboardReorganized from './DashboardReorganized'; +import DashboardTabs from './DashboardTabs'; import { generateAnalysis } from '../utils/analysisGenerator'; import toast from 'react-hot-toast'; import { useAuth } from '../utils/AuthContext'; @@ -111,7 +111,7 @@ const SinglePageDataRequestIntegrated: React.FC = () => { console.log('📊 Dimensions length:', analysisData.dimensions?.length); try { - return ; + return ; } catch (error) { console.error('❌ Error rendering dashboard:', error); return ( diff --git a/frontend/components/charts/BulletChart.tsx b/frontend/components/charts/BulletChart.tsx new file mode 100644 index 0000000..73cd7a5 --- /dev/null +++ b/frontend/components/charts/BulletChart.tsx @@ -0,0 +1,159 @@ +import { useMemo } from 'react'; + +export interface BulletChartProps { + label: string; + actual: number; + target: number; + ranges: [number, number, number]; // [poor, satisfactory, good/max] + unit?: string; + percentile?: number; + inverse?: boolean; // true if lower is better (e.g., AHT) + formatValue?: (value: number) => string; +} + +export function BulletChart({ + label, + actual, + target, + ranges, + unit = '', + percentile, + inverse = false, + formatValue = (v) => v.toLocaleString() +}: BulletChartProps) { + const [poor, satisfactory, max] = ranges; + + const { actualPercent, targetPercent, rangePercents, performance } = useMemo(() => { + const actualPct = Math.min((actual / max) * 100, 100); + const targetPct = Math.min((target / max) * 100, 100); + + const poorPct = (poor / max) * 100; + const satPct = (satisfactory / max) * 100; + + // Determine performance level + let perf: 'poor' | 'satisfactory' | 'good'; + if (inverse) { + // Lower is better (e.g., AHT, hold time) + if (actual <= satisfactory) perf = 'good'; + else if (actual <= poor) perf = 'satisfactory'; + else perf = 'poor'; + } else { + // Higher is better (e.g., FCR, CSAT) + if (actual >= satisfactory) perf = 'good'; + else if (actual >= poor) perf = 'satisfactory'; + else perf = 'poor'; + } + + return { + actualPercent: actualPct, + targetPercent: targetPct, + rangePercents: { poor: poorPct, satisfactory: satPct }, + performance: perf + }; + }, [actual, target, ranges, inverse, poor, satisfactory, max]); + + const performanceColors = { + poor: 'bg-red-500', + satisfactory: 'bg-amber-500', + good: 'bg-emerald-500' + }; + + const performanceLabels = { + poor: 'Crítico', + satisfactory: 'Aceptable', + good: 'Óptimo' + }; + + return ( +
+ {/* Header */} +
+
+ {label} + {percentile !== undefined && ( + + P{percentile} + + )} +
+ + {performanceLabels[performance]} + +
+ + {/* Bullet Chart */} +
+ {/* Background ranges */} +
+ {inverse ? ( + // Inverse: green on left, red on right + <> +
+
+
+ + ) : ( + // Normal: red on left, green on right + <> +
+
+
+ + )} +
+ + {/* Actual value bar */} +
+ + {/* Target marker */} +
+
+
+
+ + {/* Values */} +
+
+ {formatValue(actual)} + {unit} + actual +
+
+ {formatValue(target)} + {unit} + benchmark +
+
+
+ ); +} + +export default BulletChart; diff --git a/frontend/components/charts/OpportunityTreemap.tsx b/frontend/components/charts/OpportunityTreemap.tsx new file mode 100644 index 0000000..a6aede2 --- /dev/null +++ b/frontend/components/charts/OpportunityTreemap.tsx @@ -0,0 +1,214 @@ +import { Treemap, ResponsiveContainer, Tooltip } from 'recharts'; + +export type ReadinessCategory = 'automate_now' | 'assist_copilot' | 'optimize_first'; + +export interface TreemapData { + name: string; + value: number; // Savings potential (determines size) + category: ReadinessCategory; + skill: string; + score: number; // Agentic readiness score 0-10 + volume?: number; +} + +export interface OpportunityTreemapProps { + data: TreemapData[]; + title?: string; + height?: number; + onItemClick?: (item: TreemapData) => void; +} + +const CATEGORY_COLORS: Record = { + automate_now: '#059669', // emerald-600 + assist_copilot: '#6D84E3', // primary blue + optimize_first: '#D97706' // amber-600 +}; + +const CATEGORY_LABELS: Record = { + automate_now: 'Automatizar Ahora', + assist_copilot: 'Asistir con Copilot', + optimize_first: 'Optimizar Primero' +}; + +interface TreemapContentProps { + x: number; + y: number; + width: number; + height: number; + name: string; + category: ReadinessCategory; + score: number; + value: number; +} + +const CustomizedContent = ({ + x, + y, + width, + height, + name, + category, + score, + value +}: TreemapContentProps) => { + const showLabel = width > 60 && height > 40; + const showScore = width > 80 && height > 55; + const showValue = width > 100 && height > 70; + + const baseColor = CATEGORY_COLORS[category] || '#94A3B8'; + + return ( + + + {showLabel && ( + + {name.length > 15 && width < 120 ? `${name.slice(0, 12)}...` : name} + + )} + {showScore && ( + + Score: {score.toFixed(1)} + + )} + {showValue && ( + + €{(value / 1000).toFixed(0)}K + + )} + + ); +}; + +interface TooltipPayload { + payload: TreemapData; +} + +const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: TooltipPayload[] }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.name}

+

{data.skill}

+
+
+ Readiness Score: + {data.score.toFixed(1)}/10 +
+
+ Ahorro Potencial: + €{data.value.toLocaleString()} +
+ {data.volume && ( +
+ Volumen: + {data.volume.toLocaleString()}/mes +
+ )} +
+ Categoría: + + {CATEGORY_LABELS[data.category]} + +
+
+
+ ); + } + return null; +}; + +export function OpportunityTreemap({ + data, + title, + height = 350, + onItemClick +}: OpportunityTreemapProps) { + // Group data by category for treemap + const treemapData = data.map(item => ({ + ...item, + size: item.value + })); + + return ( +
+ {title && ( +

{title}

+ )} + + + } + onClick={onItemClick ? (node) => onItemClick(node as unknown as TreemapData) : undefined} + > + } /> + + + + {/* Legend */} +
+ {Object.entries(CATEGORY_COLORS).map(([category, color]) => ( +
+
+ + {CATEGORY_LABELS[category as ReadinessCategory]} + +
+ ))} +
+
+ ); +} + +export default OpportunityTreemap; diff --git a/frontend/components/charts/WaterfallChart.tsx b/frontend/components/charts/WaterfallChart.tsx new file mode 100644 index 0000000..7d67577 --- /dev/null +++ b/frontend/components/charts/WaterfallChart.tsx @@ -0,0 +1,197 @@ +import { + ComposedChart, + Bar, + Cell, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceLine, + LabelList +} from 'recharts'; + +export interface WaterfallDataPoint { + label: string; + value: number; + cumulative: number; + type: 'initial' | 'increase' | 'decrease' | 'total'; +} + +export interface WaterfallChartProps { + data: WaterfallDataPoint[]; + title?: string; + height?: number; + formatValue?: (value: number) => string; +} + +interface ProcessedDataPoint { + label: string; + value: number; + cumulative: number; + type: 'initial' | 'increase' | 'decrease' | 'total'; + start: number; + end: number; + displayValue: number; +} + +export function WaterfallChart({ + data, + title, + height = 300, + formatValue = (v) => `€${Math.abs(v).toLocaleString()}` +}: WaterfallChartProps) { + // Process data for waterfall visualization + const processedData: ProcessedDataPoint[] = data.map((item) => { + let start: number; + let end: number; + + if (item.type === 'initial' || item.type === 'total') { + start = 0; + end = item.cumulative; + } else if (item.type === 'decrease') { + // Savings: bar goes down from previous cumulative + start = item.cumulative; + end = item.cumulative - item.value; + } else { + // Increase: bar goes up from previous cumulative + start = item.cumulative - item.value; + end = item.cumulative; + } + + return { + ...item, + start: Math.min(start, end), + end: Math.max(start, end), + displayValue: Math.abs(item.value) + }; + }); + + const getBarColor = (type: string): string => { + switch (type) { + case 'initial': + return '#64748B'; // slate-500 + case 'decrease': + return '#059669'; // emerald-600 (savings) + case 'increase': + return '#DC2626'; // red-600 (costs) + case 'total': + return '#6D84E3'; // primary blue + default: + return '#94A3B8'; + } + }; + + const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: ProcessedDataPoint }> }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.label}

+

+ {data.type === 'decrease' ? '-' : data.type === 'increase' ? '+' : ''} + {formatValue(data.value)} +

+ {data.type !== 'initial' && data.type !== 'total' && ( +

+ Acumulado: {formatValue(data.cumulative)} +

+ )} +
+ ); + } + return null; + }; + + // Find min/max for Y axis + const allValues = processedData.flatMap(d => [d.start, d.end]); + const minValue = Math.min(0, ...allValues); + const maxValue = Math.max(...allValues); + const padding = (maxValue - minValue) * 0.1; + + return ( +
+ {title && ( +

{title}

+ )} + + + + + + `€${(value / 1000).toFixed(0)}K`} + /> + } /> + + + {/* Invisible bar for spacing (from 0 to start) */} + + + {/* Visible bar (the actual segment) */} + + {processedData.map((entry, index) => ( + + ))} + formatValue(value)} + style={{ fontSize: 10, fill: '#475569' }} + /> + + + + + {/* Legend */} +
+
+
+ Coste Base +
+
+
+ Ahorro +
+
+
+ Inversión +
+
+
+ Total +
+
+
+ ); +} + +export default WaterfallChart; diff --git a/frontend/components/tabs/AgenticReadinessTab.tsx b/frontend/components/tabs/AgenticReadinessTab.tsx new file mode 100644 index 0000000..581de34 --- /dev/null +++ b/frontend/components/tabs/AgenticReadinessTab.tsx @@ -0,0 +1,278 @@ +import { motion } from 'framer-motion'; +import { Bot, Zap, Brain, Activity, ChevronRight } from 'lucide-react'; +import { OpportunityTreemap, TreemapData, ReadinessCategory } from '../charts/OpportunityTreemap'; +import type { AnalysisData, HeatmapDataPoint, SubFactor } from '../../types'; + +interface AgenticReadinessTabProps { + data: AnalysisData; +} + +// Global Score Gauge +function GlobalScoreGauge({ score, confidence }: { score: number; confidence?: string }) { + const getColor = (s: number) => { + if (s >= 7) return '#059669'; // emerald-600 - Ready to automate + if (s >= 5) return '#6D84E3'; // primary blue - Assist with copilot + if (s >= 3) return '#D97706'; // amber-600 - Optimize first + return '#DC2626'; // red-600 - Not ready + }; + + const getLabel = (s: number) => { + if (s >= 7) return 'Listo para Automatizar'; + if (s >= 5) return 'Asistir con Copilot'; + if (s >= 3) return 'Optimizar Primero'; + return 'No Apto'; + }; + + const color = getColor(score); + const percentage = (score / 10) * 100; + + return ( +
+
+

+ + Agentic Readiness Score +

+ {confidence && ( + + Confianza: {confidence === 'high' ? 'Alta' : confidence === 'medium' ? 'Media' : 'Baja'} + + )} +
+ + {/* Score Display */} +
+
+ + + + +
+ {score.toFixed(1)} + /10 +
+
+ +
+

{getLabel(score)}

+

+ {score >= 7 + ? 'Skills con alta predictibilidad y bajo riesgo de error' + : score >= 5 + ? 'Skills aptos para asistencia AI con supervisión humana' + : 'Requiere optimización de procesos antes de automatizar'} +

+
+
+
+ ); +} + +// Sub-factors Breakdown +function SubFactorsBreakdown({ subFactors }: { subFactors: SubFactor[] }) { + const getIcon = (name: string) => { + if (name.includes('repetitiv')) return Activity; + if (name.includes('predict')) return Brain; + if (name.includes('estructura') || name.includes('complex')) return Zap; + return Bot; + }; + + return ( +
+

Desglose de Factores

+
+ {subFactors.map((factor) => { + const Icon = getIcon(factor.name); + const percentage = (factor.score / 10) * 100; + const weightPct = Math.round(factor.weight * 100); + + return ( +
+
+
+ + {factor.displayName} + ({weightPct}%) +
+ {factor.score.toFixed(1)} +
+
+
+
+

{factor.description}

+
+ ); + })} +
+
+ ); +} + +// Skills Heatmap/Table +function SkillsReadinessTable({ heatmapData }: { heatmapData: HeatmapDataPoint[] }) { + // Sort by automation_readiness descending + const sortedData = [...heatmapData].sort((a, b) => b.automation_readiness - a.automation_readiness); + + const getReadinessColor = (score: number) => { + if (score >= 70) return 'bg-emerald-100 text-emerald-700'; + if (score >= 50) return 'bg-[#6D84E3]/20 text-[#6D84E3]'; + if (score >= 30) return 'bg-amber-100 text-amber-700'; + return 'bg-red-100 text-red-700'; + }; + + const getCategoryLabel = (category?: string) => { + switch (category) { + case 'automate_now': return 'Automatizar'; + case 'assist_copilot': return 'Copilot'; + case 'optimize_first': return 'Optimizar'; + default: return 'Evaluar'; + } + }; + + const getRecommendation = (dataPoint: HeatmapDataPoint): string => { + const score = dataPoint.automation_readiness; + if (score >= 70) return 'Implementar agente autónomo con supervisión mínima'; + if (score >= 50) return 'Desplegar copilot con escalado humano'; + if (score >= 30) return 'Reducir variabilidad antes de automatizar'; + return 'Optimizar procesos y reducir transferencias'; + }; + + return ( +
+
+

Análisis por Skill

+
+
+ + + + + + + + + + + + + + + {sortedData.map((item) => ( + + + + + + + + + + + ))} + +
SkillVolumenAHTCV AHTTransferScoreCategoríaSiguiente Paso
{item.skill} + {item.volume.toLocaleString()} + + {item.aht_seconds}s + + 50 ? 'text-amber-600' : 'text-slate-600'}> + {item.variability.cv_aht.toFixed(0)}% + + + 20 ? 'text-red-600' : 'text-slate-600'}> + {item.variability.transfer_rate.toFixed(0)}% + + + + {(item.automation_readiness / 10).toFixed(1)} + + + + {getCategoryLabel(item.readiness_category)} + + +
+ + {getRecommendation(item)} +
+
+
+
+ ); +} + +export function AgenticReadinessTab({ data }: AgenticReadinessTabProps) { + // Get agentic readiness dimension or use fallback + const agenticDimension = data.dimensions.find(d => d.name === 'agentic_readiness'); + const globalScore = data.agenticReadiness?.score || agenticDimension?.score || 0; + const subFactors = data.agenticReadiness?.sub_factors || agenticDimension?.sub_factors || []; + const confidence = data.agenticReadiness?.confidence; + + // Convert heatmap data to treemap format + const treemapData: TreemapData[] = data.heatmapData.map(item => ({ + name: item.skill, + value: item.annual_cost || item.volume * item.aht_seconds * 0.005, // Use annual cost or estimate + category: item.readiness_category || ( + item.automation_readiness >= 70 ? 'automate_now' : + item.automation_readiness >= 50 ? 'assist_copilot' : 'optimize_first' + ) as ReadinessCategory, + skill: item.skill, + score: item.automation_readiness / 10, + volume: item.volume + })); + + return ( +
+ {/* Top Row: Score Gauge + Sub-factors */} +
+ + +
+ + {/* Treemap */} + + + {/* Skills Table */} + +
+ ); +} + +export default AgenticReadinessTab; diff --git a/frontend/components/tabs/DimensionAnalysisTab.tsx b/frontend/components/tabs/DimensionAnalysisTab.tsx new file mode 100644 index 0000000..0d29af6 --- /dev/null +++ b/frontend/components/tabs/DimensionAnalysisTab.tsx @@ -0,0 +1,213 @@ +import { motion } from 'framer-motion'; +import { ChevronRight, TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import type { AnalysisData, DimensionAnalysis, Finding, Recommendation } from '../../types'; + +interface DimensionAnalysisTabProps { + data: AnalysisData; +} + +// Dimension Card Component +function DimensionCard({ + dimension, + findings, + recommendations, + delay = 0 +}: { + dimension: DimensionAnalysis; + findings: Finding[]; + recommendations: Recommendation[]; + delay?: number; +}) { + const Icon = dimension.icon; + + const getScoreColor = (score: number) => { + if (score >= 80) return 'text-emerald-600 bg-emerald-100'; + if (score >= 60) return 'text-amber-600 bg-amber-100'; + return 'text-red-600 bg-red-100'; + }; + + const getScoreLabel = (score: number) => { + if (score >= 80) return 'Óptimo'; + if (score >= 60) return 'Aceptable'; + if (score >= 40) return 'Mejorable'; + return 'Crítico'; + }; + + // Get KPI trend icon + const TrendIcon = dimension.kpi.changeType === 'positive' ? TrendingUp : + dimension.kpi.changeType === 'negative' ? TrendingDown : Minus; + + const trendColor = dimension.kpi.changeType === 'positive' ? 'text-emerald-600' : + dimension.kpi.changeType === 'negative' ? 'text-red-600' : 'text-slate-500'; + + return ( + + {/* Header */} +
+
+
+
+ +
+
+

{dimension.title}

+

{dimension.summary}

+
+
+
+ {dimension.score} + {getScoreLabel(dimension.score)} +
+
+
+ + {/* KPI Highlight */} +
+
+ {dimension.kpi.label} +
+ {dimension.kpi.value} + {dimension.kpi.change && ( +
+ + {dimension.kpi.change} +
+ )} +
+
+ {dimension.percentile && ( +
+
+ Percentil + P{dimension.percentile} +
+
+
+
+
+ )} +
+ + {/* Findings */} +
+

+ Hallazgos Clave +

+
    + {findings.slice(0, 3).map((finding, idx) => ( +
  • + + {finding.text} +
  • + ))} + {findings.length === 0 && ( +
  • Sin hallazgos destacados
  • + )} +
+
+ + {/* Recommendations Preview */} + {recommendations.length > 0 && ( +
+
+
+ Recomendación: + {recommendations[0].text} +
+
+
+ )} + + ); +} + +// Benchmark Comparison Table +function BenchmarkTable({ benchmarkData }: { benchmarkData: AnalysisData['benchmarkData'] }) { + const getPercentileColor = (percentile: number) => { + if (percentile >= 75) return 'text-emerald-600'; + if (percentile >= 50) return 'text-amber-600'; + return 'text-red-600'; + }; + + return ( +
+
+

Benchmark vs Industria

+
+
+ + + + + + + + + + + {benchmarkData.map((item) => ( + + + + + + + ))} + +
KPIActualIndustriaPercentil
{item.kpi} + {item.userDisplay} + + {item.industryDisplay} + + P{item.percentile} +
+
+
+ ); +} + +export function DimensionAnalysisTab({ data }: DimensionAnalysisTabProps) { + // Filter out agentic_readiness (has its own tab) + const coreDimensions = data.dimensions.filter(d => d.name !== 'agentic_readiness'); + + // Group findings and recommendations by dimension + const getFindingsForDimension = (dimensionId: string) => + data.findings.filter(f => f.dimensionId === dimensionId); + + const getRecommendationsForDimension = (dimensionId: string) => + data.recommendations.filter(r => r.dimensionId === dimensionId); + + return ( +
+ {/* Dimensions Grid */} +
+ {coreDimensions.map((dimension, idx) => ( + + ))} +
+ + {/* Benchmark Table */} + +
+ ); +} + +export default DimensionAnalysisTab; diff --git a/frontend/components/tabs/ExecutiveSummaryTab.tsx b/frontend/components/tabs/ExecutiveSummaryTab.tsx new file mode 100644 index 0000000..f48b91f --- /dev/null +++ b/frontend/components/tabs/ExecutiveSummaryTab.tsx @@ -0,0 +1,292 @@ +import { TrendingUp, TrendingDown, Minus, AlertTriangle, CheckCircle, Target } from 'lucide-react'; +import { BulletChart } from '../charts/BulletChart'; +import type { AnalysisData, Finding } from '../../types'; + +interface ExecutiveSummaryTabProps { + data: AnalysisData; +} + +// Health Score Gauge Component +function HealthScoreGauge({ score }: { score: number }) { + const getColor = (s: number) => { + if (s >= 80) return '#059669'; // emerald-600 + if (s >= 60) return '#D97706'; // amber-600 + return '#DC2626'; // red-600 + }; + + const getLabel = (s: number) => { + if (s >= 80) return 'Excelente'; + if (s >= 60) return 'Bueno'; + if (s >= 40) return 'Regular'; + return 'Crítico'; + }; + + const color = getColor(score); + const circumference = 2 * Math.PI * 45; + const strokeDasharray = `${(score / 100) * circumference} ${circumference}`; + + return ( +
+

Health Score General

+
+ + {/* Background circle */} + + {/* Progress circle */} + + +
+ {score} + /100 +
+
+

{getLabel(score)}

+
+ ); +} + +// KPI Card Component +function KpiCard({ label, value, change, changeType }: { + label: string; + value: string; + change?: string; + changeType?: 'positive' | 'negative' | 'neutral'; +}) { + const ChangeIcon = changeType === 'positive' ? TrendingUp : + changeType === 'negative' ? TrendingDown : Minus; + + const changeColor = changeType === 'positive' ? 'text-emerald-600' : + changeType === 'negative' ? 'text-red-600' : 'text-slate-500'; + + return ( +
+

{label}

+

{value}

+ {change && ( +
+ + {change} +
+ )} +
+ ); +} + +// Top Opportunities Component (McKinsey style) +function TopOpportunities({ findings, opportunities }: { + findings: Finding[]; + opportunities: { name: string; impact: number; savings: number }[]; +}) { + // Combine critical findings and high-impact opportunities + const items = [ + ...findings + .filter(f => f.type === 'critical' || f.type === 'warning') + .slice(0, 3) + .map((f, i) => ({ + rank: i + 1, + title: f.title || f.text.split(':')[0], + metric: f.text.includes(':') ? f.text.split(':')[1].trim() : '', + action: f.description || 'Acción requerida', + type: f.type as 'critical' | 'warning' | 'info' + })), + ].slice(0, 3); + + // Fill with opportunities if not enough findings + if (items.length < 3) { + const remaining = 3 - items.length; + opportunities + .sort((a, b) => b.savings - a.savings) + .slice(0, remaining) + .forEach((opp, i) => { + items.push({ + rank: items.length + 1, + title: opp.name, + metric: `€${opp.savings.toLocaleString()} ahorro potencial`, + action: 'Implementar', + type: 'info' as const + }); + }); + } + + const getIcon = (type: string) => { + if (type === 'critical') return ; + if (type === 'warning') return ; + return ; + }; + + return ( +
+

Top 3 Oportunidades

+
+ {items.map((item) => ( +
+
+ {item.rank} +
+
+
+ {getIcon(item.type)} + {item.title} +
+ {item.metric && ( +

{item.metric}

+ )} +

+ → {item.action} +

+
+
+ ))} +
+
+ ); +} + +export function ExecutiveSummaryTab({ data }: ExecutiveSummaryTabProps) { + // Extract key KPIs for bullet charts + const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); + const avgAHT = data.heatmapData.length > 0 + ? Math.round(data.heatmapData.reduce((sum, h) => sum + h.aht_seconds, 0) / data.heatmapData.length) + : 0; + const avgFCR = data.heatmapData.length > 0 + ? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.fcr, 0) / data.heatmapData.length) + : 0; + const avgTransferRate = data.heatmapData.length > 0 + ? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate, 0) / data.heatmapData.length) + : 0; + + // Find benchmark data + const ahtBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('aht')); + const fcrBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('fcr')); + + return ( +
+ {/* Main Grid: KPIs + Health Score */} +
+ {/* Summary KPIs */} + {data.summaryKpis.slice(0, 3).map((kpi) => ( + + ))} + + {/* Health Score Gauge */} + +
+ + {/* Bullet Charts Row */} +
+ v >= 1000 ? `${(v / 1000).toFixed(1)}K` : v.toString()} + /> + + 480s poor, 420-480 ok, <420 good + unit="s" + percentile={ahtBenchmark?.percentile} + inverse={true} + formatValue={(v) => v.toString()} + /> + + 75 good + unit="%" + percentile={fcrBenchmark?.percentile} + formatValue={(v) => v.toString()} + /> + + 25% poor, 15-25 ok, <15 good + unit="%" + inverse={true} + formatValue={(v) => v.toString()} + /> +
+ + {/* Bottom Row: Top Opportunities + Economic Summary */} +
+ + + {/* Economic Impact Summary */} +
+

Impacto Económico

+
+
+

Coste Anual Actual

+

+ €{data.economicModel.currentAnnualCost.toLocaleString()} +

+
+
+

Ahorro Potencial

+

+ €{data.economicModel.annualSavings.toLocaleString()} +

+
+
+

Inversión Inicial

+

+ €{data.economicModel.initialInvestment.toLocaleString()} +

+
+
+

ROI a 3 Años

+

+ {data.economicModel.roi3yr}% +

+
+
+ + {/* Payback indicator */} +
+
+ Payback + + {data.economicModel.paybackMonths} meses + +
+
+
+
+
+ ); +} + +export default ExecutiveSummaryTab; diff --git a/frontend/components/tabs/RoadmapTab.tsx b/frontend/components/tabs/RoadmapTab.tsx new file mode 100644 index 0000000..c7fa849 --- /dev/null +++ b/frontend/components/tabs/RoadmapTab.tsx @@ -0,0 +1,355 @@ +import { motion } from 'framer-motion'; +import { Zap, Clock, DollarSign, TrendingUp, AlertTriangle, CheckCircle, ArrowRight } from 'lucide-react'; +import { WaterfallChart, WaterfallDataPoint } from '../charts/WaterfallChart'; +import type { AnalysisData, RoadmapInitiative, RoadmapPhase } from '../../types'; + +interface RoadmapTabProps { + data: AnalysisData; +} + +// Quick Wins Section +function QuickWins({ initiatives, economicModel }: { + initiatives: RoadmapInitiative[]; + economicModel: AnalysisData['economicModel']; +}) { + // Filter for quick wins (low investment, quick timeline) + const quickWins = initiatives + .filter(i => i.phase === RoadmapPhase.Automate || i.risk === 'low') + .slice(0, 3); + + if (quickWins.length === 0) { + // Create synthetic quick wins from savings breakdown + const topSavings = economicModel.savingsBreakdown.slice(0, 3); + return ( +
+
+ +

Quick Wins Identificados

+
+
+ {topSavings.map((saving, idx) => ( +
+
+ + {saving.category} +
+

+ €{saving.amount.toLocaleString()} +

+

{saving.percentage}% del ahorro total

+
+ ))} +
+
+ ); + } + + return ( +
+
+ +

Quick Wins

+
+
+ {quickWins.map((initiative) => ( +
+
+ + {initiative.name} +
+

{initiative.timeline}

+

+ €{initiative.investment.toLocaleString()} inversión +

+
+ ))} +
+
+ ); +} + +// Initiative Card +function InitiativeCard({ initiative, delay = 0 }: { initiative: RoadmapInitiative; delay?: number }) { + const phaseColors = { + [RoadmapPhase.Automate]: 'bg-emerald-100 text-emerald-700 border-emerald-200', + [RoadmapPhase.Assist]: 'bg-[#6D84E3]/20 text-[#6D84E3] border-[#6D84E3]/30', + [RoadmapPhase.Augment]: 'bg-amber-100 text-amber-700 border-amber-200' + }; + + const riskColors = { + low: 'text-emerald-600', + medium: 'text-amber-600', + high: 'text-red-600' + }; + + return ( + +
+
+

{initiative.name}

+ + {initiative.phase} + +
+ {initiative.risk && ( +
+ + Riesgo {initiative.risk === 'low' ? 'Bajo' : initiative.risk === 'medium' ? 'Medio' : 'Alto'} +
+ )} +
+ +
+
+ + {initiative.timeline} +
+
+ + €{initiative.investment.toLocaleString()} +
+
+ + {initiative.resources.length > 0 && ( +
+ {initiative.resources.slice(0, 3).map((resource, idx) => ( + + {resource} + + ))} +
+ )} +
+ ); +} + +// Business Case Summary +function BusinessCaseSummary({ economicModel }: { economicModel: AnalysisData['economicModel'] }) { + return ( +
+

+ + Business Case Consolidado +

+ +
+
+

Inversión Total

+

+ €{economicModel.initialInvestment.toLocaleString()} +

+
+
+

Ahorro Anual

+

+ €{economicModel.annualSavings.toLocaleString()} +

+
+
+

Payback

+

+ {economicModel.paybackMonths} meses +

+
+
+

ROI 3 Años

+

+ {economicModel.roi3yr}% +

+
+
+ + {/* Savings Breakdown */} +
+

Desglose de Ahorros:

+ {economicModel.savingsBreakdown.map((item, idx) => ( +
+
+
+ {item.category} + €{item.amount.toLocaleString()} +
+
+
+
+
+ {item.percentage}% +
+ ))} +
+
+ ); +} + +// Timeline Visual +function TimelineVisual({ initiatives }: { initiatives: RoadmapInitiative[] }) { + const phases = [ + { phase: RoadmapPhase.Automate, label: 'Wave 1: Automatizar', color: 'bg-emerald-500' }, + { phase: RoadmapPhase.Assist, label: 'Wave 2: Asistir', color: 'bg-[#6D84E3]' }, + { phase: RoadmapPhase.Augment, label: 'Wave 3: Aumentar', color: 'bg-amber-500' } + ]; + + return ( +
+

Timeline de Implementación

+ +
+ {/* Timeline line */} +
+ + {/* Phases */} +
+ {phases.map((phase, idx) => { + const phaseInitiatives = initiatives.filter(i => i.phase === phase.phase); + return ( +
+ {/* Circle */} +
+ {idx + 1} +
+ {/* Label */} +

{phase.label}

+ {/* Count */} +

+ {phaseInitiatives.length} iniciativa{phaseInitiatives.length !== 1 ? 's' : ''} +

+
+ ); + })} +
+ + {/* Arrows */} +
+ + +
+
+
+ ); +} + +export function RoadmapTab({ data }: RoadmapTabProps) { + // Generate waterfall data from economic model + const waterfallData: WaterfallDataPoint[] = [ + { + label: 'Coste Actual', + value: data.economicModel.currentAnnualCost, + cumulative: data.economicModel.currentAnnualCost, + type: 'initial' + }, + { + label: 'Inversión Inicial', + value: data.economicModel.initialInvestment, + cumulative: data.economicModel.currentAnnualCost + data.economicModel.initialInvestment, + type: 'increase' + }, + ...data.economicModel.savingsBreakdown.map((saving, idx) => ({ + label: saving.category, + value: saving.amount, + cumulative: data.economicModel.currentAnnualCost + data.economicModel.initialInvestment - + data.economicModel.savingsBreakdown.slice(0, idx + 1).reduce((sum, s) => sum + s.amount, 0), + type: 'decrease' as const + })), + { + label: 'Coste Futuro', + value: data.economicModel.futureAnnualCost, + cumulative: data.economicModel.futureAnnualCost, + type: 'total' + } + ]; + + // Group initiatives by phase + const automateInitiatives = data.roadmap.filter(i => i.phase === RoadmapPhase.Automate); + const assistInitiatives = data.roadmap.filter(i => i.phase === RoadmapPhase.Assist); + const augmentInitiatives = data.roadmap.filter(i => i.phase === RoadmapPhase.Augment); + + return ( +
+ {/* Quick Wins */} + + + {/* Timeline Visual */} + + + {/* Waterfall Chart */} + + + {/* Initiatives by Phase */} +
+ {/* Wave 1: Automate */} +
+

+
+ Wave 1: Automatizar +

+
+ {automateInitiatives.length > 0 ? ( + automateInitiatives.map((init, idx) => ( + + )) + ) : ( +

+ Sin iniciativas en esta fase +

+ )} +
+
+ + {/* Wave 2: Assist */} +
+

+
+ Wave 2: Asistir +

+
+ {assistInitiatives.length > 0 ? ( + assistInitiatives.map((init, idx) => ( + + )) + ) : ( +

+ Sin iniciativas en esta fase +

+ )} +
+
+ + {/* Wave 3: Augment */} +
+

+
+ Wave 3: Aumentar +

+
+ {augmentInitiatives.length > 0 ? ( + augmentInitiatives.map((init, idx) => ( + + )) + ) : ( +

+ Sin iniciativas en esta fase +

+ )} +
+
+
+ + {/* Business Case Summary */} + +
+ ); +} + +export default RoadmapTab; diff --git a/frontend/constants.ts b/frontend/constants.ts index f35cef5..641b78f 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -6,17 +6,17 @@ export const TIERS: TiersData = { name: 'Análisis GOLD', price: 4900, color: 'bg-yellow-500', - description: '6 dimensiones completas con algoritmo Agentic Readiness avanzado', + description: '5 dimensiones completas con Agentic Readiness avanzado', requirements: 'CCaaS moderno (Genesys, Five9, NICE, Talkdesk)', timeline: '3-4 semanas', features: [ - '6 dimensiones completas', - 'Algoritmo Agentic Readiness avanzado (6 sub-factores)', - 'Análisis de distribución horaria', - 'Segmentación de clientes (opcional)', - 'Benchmark con percentiles múltiples (P25, P50, P75, P90)', + '5 dimensiones: Volumetría, Eficiencia, Efectividad, Complejidad, Agentic Readiness', + 'Agentic Readiness Score 0-10 por cola', + 'Análisis de distribución horaria y semanal', + 'Métricas P10/P50/P90 por cola', + 'FCR proxy y tasa de transferencias', + 'Análisis de variabilidad y predictibilidad', 'Roadmap ejecutable con 3 waves', - 'Modelo económico con NPV y análisis de sensibilidad', 'Sesión de presentación incluida' ] }, @@ -24,15 +24,14 @@ export const TIERS: TiersData = { name: 'Análisis SILVER', price: 3500, color: 'bg-gray-400', - description: '4 dimensiones core con Agentic Readiness simplificado', + description: '5 dimensiones con Agentic Readiness simplificado', requirements: 'Sistema ACD/PBX con reporting básico', timeline: '2-3 semanas', features: [ - '4 dimensiones (Volumetría, Rendimiento, Economía, Agentic Readiness)', - 'Algoritmo Agentic Readiness simplificado (3 sub-factores)', + '5 dimensiones completas', + 'Agentic Readiness simplificado (4 sub-factores)', 'Roadmap de implementación', 'Opportunity Matrix', - 'Economic Model básico', 'Dashboard interactivo' ] }, @@ -40,15 +39,14 @@ export const TIERS: TiersData = { name: 'Análisis EXPRESS', price: 1950, color: 'bg-orange-600', - description: '3 dimensiones fundamentales sin Agentic Readiness', + description: '4 dimensiones fundamentales sin Agentic Readiness detallado', requirements: 'Exportación básica de reportes', timeline: '1-2 semanas', features: [ - '3 dimensiones core (Volumetría, Rendimiento, Economía)', + '4 dimensiones core (Volumetría, Eficiencia, Efectividad, Complejidad)', + 'Agentic Readiness básico', 'Roadmap cualitativo', - 'Análisis básico', - 'Recomendaciones estratégicas', - 'Reporte ejecutivo' + 'Recomendaciones estratégicas' ] } }; @@ -136,14 +134,13 @@ export const DATA_REQUIREMENTS: DataRequirementsData = { } }; -// v2.0: Dimensiones actualizadas (6 en lugar de 8) +// v3.0: 5 dimensiones viables export const DIMENSION_NAMES = { - volumetry_distribution: 'Volumetría y Distribución Horaria', - performance: 'Rendimiento', - satisfaction: 'Satisfacción', - economy: 'Economía', - efficiency: 'Eficiencia', // Fusiona Eficiencia + Efectividad - benchmark: 'Benchmark' + volumetry_distribution: 'Volumetría & Distribución', + operational_efficiency: 'Eficiencia Operativa', + effectiveness_resolution: 'Efectividad & Resolución', + complexity_predictability: 'Complejidad & Predictibilidad', + agentic_readiness: 'Agentic Readiness' }; // v2.0: Ponderaciones para Agentic Readiness Score diff --git a/frontend/types.ts b/frontend/types.ts index 09f92ce..d7b2c7d 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -102,14 +102,13 @@ export interface Kpi { changeType?: 'positive' | 'negative' | 'neutral'; } -// v2.0: Dimensiones reducidas de 8 a 6 -export type DimensionName = - | 'volumetry_distribution' // Volumetría y Distribución Horaria (fusión + ampliación) - | 'performance' // Rendimiento - | 'satisfaction' // Satisfacción - | 'economy' // Economía - | 'efficiency' // Eficiencia (fusiona efficiency + effectiveness) - | 'benchmark'; // Benchmark +// v3.0: 5 dimensiones viables +export type DimensionName = + | 'volumetry_distribution' // Volumetría & Distribución + | 'operational_efficiency' // Eficiencia Operativa + | 'effectiveness_resolution' // Efectividad & Resolución + | 'complexity_predictability' // Complejidad & Predictibilidad + | 'agentic_readiness'; // Agentic Readiness export interface SubFactor { name: string; diff --git a/frontend/utils/analysisGenerator.ts b/frontend/utils/analysisGenerator.ts index ad1afe3..71ac50f 100644 --- a/frontend/utils/analysisGenerator.ts +++ b/frontend/utils/analysisGenerator.ts @@ -2,7 +2,7 @@ import type { AnalysisData, Kpi, DimensionAnalysis, HeatmapDataPoint, Opportunity, RoadmapInitiative, EconomicModelData, BenchmarkDataPoint, Finding, Recommendation, TierKey, CustomerSegment } from '../types'; import { generateAnalysisFromRealData } from './realDataAnalysis'; import { RoadmapPhase } from '../types'; -import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react'; +import { BarChartHorizontal, Zap, Target, Brain, Bot } from 'lucide-react'; import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2'; import { callAnalysisApiRaw } from './apiClient'; import { @@ -30,14 +30,14 @@ const getScoreColor = (score: number): 'green' | 'yellow' | 'red' => { return 'red'; }; -// v2.0: 6 DIMENSIONES (eliminadas Complejidad y Efectividad) +// v3.0: 5 DIMENSIONES VIABLES const DIMENSIONS_CONTENT = { volumetry_distribution: { icon: BarChartHorizontal, - titles: ["Volumetría y Distribución Horaria", "Análisis de la Demanda"], + titles: ["Volumetría & Distribución", "Análisis de la Demanda"], summaries: { - good: ["El volumen de interacciones se alinea con las previsiones, permitiendo una planificación de personal precisa.", "La distribución horaria es uniforme con picos predecibles, facilitando la automatización."], - medium: ["Existen picos de demanda imprevistos que generan caídas en el nivel de servicio.", "Alto porcentaje de interacciones fuera de horario laboral (>30%), sugiriendo necesidad de cobertura 24/7."], + good: ["El volumen de interacciones se alinea con las previsiones, permitiendo una planificación de personal precisa.", "La distribución horaria es uniforme con picos predecibles. Concentración Pareto equilibrada."], + medium: ["Existen picos de demanda imprevistos que generan caídas en el nivel de servicio.", "Alta concentración en pocas colas (>80% en 20% de colas), riesgo de cuellos de botella."], bad: ["Desajuste crónico entre el forecast y el volumen real, resultando en sobrecostes o mal servicio.", "Distribución horaria muy irregular con múltiples picos impredecibles."] }, kpis: [ @@ -45,85 +45,72 @@ const DIMENSIONS_CONTENT = { { label: "% Fuera de Horario", value: `${randomInt(15, 45)}%` }, ], }, - performance: { + operational_efficiency: { icon: Zap, - titles: ["Rendimiento Operativo", "Optimización de Tiempos"], + titles: ["Eficiencia Operativa", "Optimización de Tiempos"], summaries: { - good: ["El AHT está bien controlado con baja variabilidad (CV<30%), indicando procesos estandarizados.", "Tiempos de espera y post-llamada (ACW) mínimos, maximizando la productividad del agente."], - medium: ["El AHT es competitivo, pero la variabilidad es alta (CV>40%), sugiriendo inconsistencia en procesos.", "El tiempo en espera (Hold Time) es ligeramente elevado, sugiriendo posibles mejoras en el acceso a la información."], - bad: ["El AHT excede los benchmarks de la industria con alta variabilidad, impactando directamente en los costes.", "Tiempos de ACW prolongados indican procesos manuales ineficientes o falta de integración de sistemas."] + good: ["El ratio P90/P50 es bajo (<1.5), indicando tiempos consistentes y procesos estandarizados.", "Tiempos de espera, hold y ACW bien controlados, maximizando la productividad."], + medium: ["El ratio P90/P50 es moderado (1.5-2.0), existen casos outliers que afectan la eficiencia.", "El tiempo de hold es ligeramente elevado, sugiriendo mejoras en acceso a información."], + bad: ["Alto ratio P90/P50 (>2.0), indicando alta variabilidad en tiempos de gestión.", "Tiempos de ACW y hold prolongados indican procesos manuales ineficientes."] }, kpis: [ - { label: "AHT Promedio", value: `${randomInt(280, 550)}s` }, - { label: "CV AHT", value: `${randomInt(25, 60)}%` }, + { label: "AHT P50", value: `${randomInt(280, 450)}s` }, + { label: "Ratio P90/P50", value: `${randomFloat(1.2, 2.5, 2)}` }, ], }, - satisfaction: { - icon: Smile, - titles: ["Satisfacción y Experiencia", "Voz del Cliente"], - summaries: { - good: ["Puntuaciones de CSAT muy positivas con distribución normal, reflejando un proceso estable y consistente.", "El análisis cualitativo muestra un sentimiento mayoritariamente positivo en las interacciones."], - medium: ["Los indicadores de satisfacción son neutros. La distribución de CSAT muestra cierta bimodalidad.", "Se observan comentarios mixtos, con puntos fuertes en la amabilidad del agente pero debilidades en los tiempos de respuesta."], - bad: ["Bajas puntuaciones de CSAT con distribución anormal, indicando un proceso inconsistente.", "Los clientes se quejan frecuentemente de largos tiempos de espera, repetición de información y falta de resolución."] - }, - kpis: [ - { label: "CSAT Promedio", value: `${randomFloat(3.8, 4.9, 1)}/5` }, - { label: "NPS", value: `${randomInt(-20, 55)}` }, - ], - }, - economy: { - icon: DollarSign, - titles: ["Economía y Costes", "Rentabilidad del Servicio"], - summaries: { - good: ["El coste por interacción está por debajo del promedio de la industria, indicando una operación rentable.", "El ROI potencial de automatización supera los €200K anuales con payback <12 meses."], - medium: ["Los costes son estables pero no se observa una tendencia a la baja, sugiriendo un estancamiento en la optimización.", "El ROI potencial es moderado (€100-200K), requiriendo inversión inicial significativa."], - bad: ["Coste por interacción elevado, erosionando los márgenes de beneficio de la compañía.", "Bajo ROI potencial (<€100K) debido a volumen insuficiente o procesos ya optimizados."] - }, - kpis: [ - { label: "Coste por Interacción", value: `€${randomFloat(2.5, 9.5, 2)}` }, - { label: "Ahorro Potencial", value: `€${randomInt(50, 250)}K` }, - ], - }, - efficiency: { + effectiveness_resolution: { icon: Target, - titles: ["Eficiencia", "Resolución y Calidad"], + titles: ["Efectividad & Resolución", "Calidad del Servicio"], summaries: { - good: ["Alta tasa de resolución en el primer contacto (FCR>85%), minimizando la repetición de llamadas.", "Bajo índice de transferencias y escalaciones (<10%), demostrando un correcto enrutamiento y alto conocimiento del agente."], - medium: ["La tasa de FCR es aceptable (70-85%), aunque se detectan ciertos tipos de consulta que requieren múltiples contactos.", "Las transferencias son moderadas (10-20%), concentradas en departamentos específicos."], - bad: ["Bajo FCR (<70%), lo que genera frustración en el cliente y aumenta el volumen de interacciones innecesarias.", "Excesivas transferencias y escalaciones (>20%), creando una experiencia de cliente fragmentada y costosa."] + good: ["FCR proxy >85%, mínima repetición de contactos a 7 días.", "Baja tasa de transferencias (<10%) y llamadas problemáticas (<5%)."], + medium: ["FCR proxy 70-85%, hay oportunidad de reducir recontactos.", "Tasa de transferencias moderada (10-20%), concentradas en ciertas colas."], + bad: ["FCR proxy <70%, alto volumen de recontactos a 7 días.", "Alta tasa de llamadas problemáticas (>15%) y transferencias excesivas."] }, kpis: [ - { label: "Tasa FCR", value: `${randomInt(65, 92)}%` }, - { label: "Tasa de Escalación", value: `${randomInt(5, 25)}%` }, + { label: "FCR Proxy 7d", value: `${randomInt(65, 92)}%` }, + { label: "Tasa Transfer", value: `${randomInt(5, 25)}%` }, ], }, - benchmark: { - icon: Globe, - titles: ["Benchmark de Industria", "Contexto Competitivo"], + complexity_predictability: { + icon: Brain, + titles: ["Complejidad & Predictibilidad", "Análisis de Variabilidad"], summaries: { - good: ["La operación se sitúa consistentemente por encima del P75 en los KPIs más críticos.", "El rendimiento en eficiencia y calidad es de 'top quartile', representando una ventaja competitiva."], - medium: ["El rendimiento general está en línea con la mediana de la industria (P50), sin claras fortalezas o debilidades.", "Se observan algunas áreas por debajo del P50 que representan oportunidades de mejora claras."], - bad: ["La mayoría de los KPIs se encuentran por debajo del P25, indicando una necesidad urgente de mejora.", "El AHT y el CPI son significativamente más altos que los benchmarks, impactando la rentabilidad."] + good: ["Baja variabilidad AHT (ratio P90/P50 <1.5), proceso altamente predecible.", "Diversidad de tipificaciones controlada, bajo % de llamadas con múltiples holds."], + medium: ["Variabilidad AHT moderada, algunos casos outliers afectan la predictibilidad.", "% llamadas con múltiples holds elevado (15-30%), indicando complejidad."], + bad: ["Alta variabilidad AHT (ratio >2.0), proceso impredecible y difícil de automatizar.", "Alta diversidad de tipificaciones y % transferencias, indicando alta complejidad."] }, kpis: [ - { label: "Posición vs P50 AHT", value: `P${randomInt(30, 70)}` }, - { label: "Posición vs P50 FCR", value: `P${randomInt(30, 70)}` }, + { label: "Ratio P90/P50", value: `${randomFloat(1.2, 2.5, 2)}` }, + { label: "% Transferencias", value: `${randomInt(5, 30)}%` }, + ], + }, + agentic_readiness: { + icon: Bot, + titles: ["Agentic Readiness", "Potencial de Automatización"], + summaries: { + good: ["Score 8-10: Excelente candidato para automatización completa con agentes IA.", "Alto volumen, baja variabilidad, pocas transferencias. Proceso repetitivo y predecible."], + medium: ["Score 5-7: Candidato para asistencia con IA (copilot) o automatización parcial.", "Volumen moderado con algunas complejidades que requieren supervisión humana."], + bad: ["Score 0-4: Requiere optimización previa antes de automatizar.", "Alta complejidad, baja repetitividad o variabilidad excesiva."] + }, + kpis: [ + { label: "Score Global", value: `${randomFloat(3.0, 9.5, 1)}/10` }, + { label: "Categoría", value: randomFromList(['Automatizar', 'Asistir', 'Optimizar']) }, ], }, }; const KEY_FINDINGS: Finding[] = [ { - text: "El canal de voz presenta un AHT un 35% superior al del chat, pero una tasa de resolución un 15% mayor.", - dimensionId: 'performance', - type: 'info', - title: 'Diferencia de Canales: Voz vs Chat', - description: 'Análisis comparativo entre canales muestra trade-off entre velocidad y resolución.', - impact: 'medium' + text: "El ratio P90/P50 de AHT es alto (>2.0) en varias colas, indicando alta variabilidad.", + dimensionId: 'operational_efficiency', + type: 'warning', + title: 'Alta Variabilidad en Tiempos', + description: 'Procesos poco estandarizados generan tiempos impredecibles y afectan la planificación.', + impact: 'high' }, { - text: "Un 22% de las transferencias desde 'Soporte Técnico N1' hacia 'Facturación' son incorrectas.", - dimensionId: 'efficiency', + text: "Un 22% de las transferencias desde 'Soporte Técnico N1' hacia otras colas son incorrectas.", + dimensionId: 'effectiveness_resolution', type: 'warning', title: 'Enrutamiento Incorrecto', description: 'Existe un problema de routing que genera ineficiencias y experiencia pobre del cliente.', @@ -147,46 +134,46 @@ const KEY_FINDINGS: Finding[] = [ }, { text: "Las consultas sobre 'estado del pedido' representan el 30% de las interacciones y tienen alta repetitividad.", - dimensionId: 'volumetry_distribution', + dimensionId: 'agentic_readiness', type: 'info', title: 'Oportunidad de Automatización: Estado de Pedido', - description: 'Volumen significativo en consultas altamente repetitivas y automatizables.', + description: 'Volumen significativo en consultas altamente repetitivas y automatizables (Score Agentic >8).', impact: 'high' }, { - text: "Baja puntuación de CSAT en interacciones relacionadas con problemas de facturación.", - dimensionId: 'satisfaction', + text: "FCR proxy <75% en colas de facturación, alto recontacto a 7 días.", + dimensionId: 'effectiveness_resolution', type: 'warning', - title: 'Satisfacción Baja en Facturación', - description: 'El equipo de facturación tiene desempeño por debajo de la media en satisfacción del cliente.', + title: 'Baja Resolución en Facturación', + description: 'El equipo de facturación tiene alto % de recontactos, indicando problemas de resolución.', impact: 'high' }, { - text: "La variabilidad de AHT (CV=45%) sugiere procesos poco estandarizados.", - dimensionId: 'performance', + text: "Alta diversidad de tipificaciones y >20% llamadas con múltiples holds en colas complejas.", + dimensionId: 'complexity_predictability', type: 'warning', - title: 'Inconsistencia en Procesos', - description: 'Alta variabilidad indica falta de estandarización y diferencias significativas entre agentes.', + title: 'Alta Complejidad en Ciertas Colas', + description: 'Colas con alta complejidad requieren optimización antes de considerar automatización.', impact: 'medium' }, ]; const RECOMMENDATIONS: Recommendation[] = [ { - text: "Implementar un programa de formación específico para agentes de Facturación sobre los nuevos planes.", - dimensionId: 'efficiency', + text: "Estandarizar procesos en colas con alto ratio P90/P50 para reducir variabilidad.", + dimensionId: 'operational_efficiency', priority: 'high', - title: 'Formación en Facturación', - description: 'Capacitación intensiva en productos, políticas y procedimientos de facturación.', - impact: 'Mejora estimada de satisfacción: 15-25%', - timeline: '2-3 semanas' + title: 'Estandarización de Procesos', + description: 'Implementar scripts y guías paso a paso para reducir la variabilidad en tiempos de gestión.', + impact: 'Reducción ratio P90/P50: 20-30%, Mejora predictibilidad', + timeline: '3-4 semanas' }, { text: "Desarrollar un bot de estado de pedido para WhatsApp para desviar el 30% de las consultas.", - dimensionId: 'volumetry_distribution', + dimensionId: 'agentic_readiness', priority: 'high', title: 'Bot Automatizado de Seguimiento de Pedidos', - description: 'Implementar ChatBot en WhatsApp para responder consultas de estado de pedido automáticamente.', + description: 'Implementar ChatBot en WhatsApp para consultas con alto Agentic Score (>8).', impact: 'Reducción de volumen: 20-30%, Ahorro anual: €40-60K', timeline: '1-2 meses' }, @@ -200,12 +187,12 @@ const RECOMMENDATIONS: Recommendation[] = [ timeline: '1 mes' }, { - text: "Crear una Knowledge Base más robusta y accesible para reducir el tiempo en espera.", - dimensionId: 'performance', + text: "Crear una Knowledge Base más robusta para reducir hold time y mejorar FCR.", + dimensionId: 'effectiveness_resolution', priority: 'high', title: 'Mejora de Acceso a Información', - description: 'Desarrollar una KB centralizada integrada en el sistema de agentes con búsqueda inteligente.', - impact: 'Reducción de AHT: 8-12%, Mejora de FCR: 5-10%', + description: 'Desarrollar una KB centralizada para reducir búsquedas y mejorar resolución en primer contacto.', + impact: 'Reducción hold time: 15-25%, Mejora FCR: 5-10%', timeline: '6-8 semanas' }, { @@ -213,18 +200,18 @@ const RECOMMENDATIONS: Recommendation[] = [ dimensionId: 'volumetry_distribution', priority: 'medium', title: 'Cobertura 24/7 con IA', - description: 'Desplegar agentes virtuales para gestionar el 28% de interacciones nocturnas.', + description: 'Desplegar agentes virtuales para gestionar interacciones nocturnas y fines de semana.', impact: 'Captura de demanda: 20-25%, Coste incremental: €15-20K/mes', timeline: '2-3 meses' }, { - text: "Realizar un análisis de causa raíz sobre las quejas de facturación para mejorar procesos.", - dimensionId: 'satisfaction', + text: "Simplificar tipificaciones y reducir complejidad en colas problemáticas.", + dimensionId: 'complexity_predictability', priority: 'medium', - title: 'Análisis de Causa Raíz (Facturación)', - description: 'Investigar las 50 últimas quejas de facturación para identificar patrones y causas.', - impact: 'Identificación de mejoras de proceso con ROI potencial de €20-50K', - timeline: '2-3 semanas' + title: 'Reducción de Complejidad', + description: 'Consolidar tipificaciones y simplificar flujos para mejorar predictibilidad.', + impact: 'Reducción de complejidad: 20-30%, Mejora Agentic Score', + timeline: '4-6 semanas' }, ]; @@ -651,25 +638,25 @@ const generateHeatmapData = ( }); }; -// v2.0: Añadir segmentación de cliente +// v3.0: Oportunidades con nuevas dimensiones const generateOpportunityMatrixData = (): Opportunity[] => { const opportunities = [ - { id: 'opp1', name: 'Automatizar consulta de pedidos', savings: 85000, dimensionId: 'volumetry_distribution', customer_segment: 'medium' as CustomerSegment }, - { id: 'opp2', name: 'Implementar Knowledge Base dinámica', savings: 45000, dimensionId: 'performance', customer_segment: 'high' as CustomerSegment }, - { id: 'opp3', name: 'Chatbot de triaje inicial', savings: 120000, dimensionId: 'efficiency', customer_segment: 'medium' as CustomerSegment }, - { id: 'opp4', name: 'Análisis de sentimiento en tiempo real', savings: 30000, dimensionId: 'satisfaction', customer_segment: 'high' as CustomerSegment }, + { id: 'opp1', name: 'Automatizar consulta de pedidos', savings: 85000, dimensionId: 'agentic_readiness', customer_segment: 'medium' as CustomerSegment }, + { id: 'opp2', name: 'Implementar Knowledge Base dinámica', savings: 45000, dimensionId: 'operational_efficiency', customer_segment: 'high' as CustomerSegment }, + { id: 'opp3', name: 'Chatbot de triaje inicial', savings: 120000, dimensionId: 'effectiveness_resolution', customer_segment: 'medium' as CustomerSegment }, + { id: 'opp4', name: 'Reducir complejidad en colas críticas', savings: 30000, dimensionId: 'complexity_predictability', customer_segment: 'high' as CustomerSegment }, { id: 'opp5', name: 'Cobertura 24/7 con agentes virtuales', savings: 65000, dimensionId: 'volumetry_distribution', customer_segment: 'low' as CustomerSegment }, ]; return opportunities.map(opp => ({ ...opp, impact: randomInt(3, 10), feasibility: randomInt(2, 9) })); }; -// v2.0: Añadir risk level +// v3.0: Roadmap con nuevas dimensiones const generateRoadmapData = (): RoadmapInitiative[] => { return [ - { id: 'r1', name: 'Chatbot de estado de pedido', phase: RoadmapPhase.Automate, timeline: 'Q1 2025', investment: 25000, resources: ['1x Bot Developer', 'API Access'], dimensionId: 'volumetry_distribution', risk: 'low' }, - { id: 'r2', name: 'Implementar Knowledge Base dinámica', phase: RoadmapPhase.Assist, timeline: 'Q1 2025', investment: 15000, resources: ['1x PM', 'Content Team'], dimensionId: 'performance', risk: 'low' }, - { id: 'r3', name: 'Agent Assist para sugerencias en real-time', phase: RoadmapPhase.Assist, timeline: 'Q2 2025', investment: 45000, resources: ['2x AI Devs', 'QA Team'], dimensionId: 'efficiency', risk: 'medium' }, - { id: 'r4', name: 'IVR conversacional con IA', phase: RoadmapPhase.Automate, timeline: 'Q3 2025', investment: 60000, resources: ['AI Voice Specialist', 'UX Designer'], dimensionId: 'efficiency', risk: 'medium' }, + { id: 'r1', name: 'Chatbot de estado de pedido', phase: RoadmapPhase.Automate, timeline: 'Q1 2025', investment: 25000, resources: ['1x Bot Developer', 'API Access'], dimensionId: 'agentic_readiness', risk: 'low' }, + { id: 'r2', name: 'Implementar Knowledge Base dinámica', phase: RoadmapPhase.Assist, timeline: 'Q1 2025', investment: 15000, resources: ['1x PM', 'Content Team'], dimensionId: 'operational_efficiency', risk: 'low' }, + { id: 'r3', name: 'Agent Assist para sugerencias en real-time', phase: RoadmapPhase.Assist, timeline: 'Q2 2025', investment: 45000, resources: ['2x AI Devs', 'QA Team'], dimensionId: 'effectiveness_resolution', risk: 'medium' }, + { id: 'r4', name: 'Estandarización de procesos complejos', phase: RoadmapPhase.Augment, timeline: 'Q3 2025', investment: 30000, resources: ['Process Analyst', 'Training Team'], dimensionId: 'complexity_predictability', risk: 'medium' }, { id: 'r5', name: 'Cobertura 24/7 con agentes virtuales', phase: RoadmapPhase.Augment, timeline: 'Q4 2025', investment: 75000, resources: ['Lead AI Engineer', 'Data Scientist'], dimensionId: 'volumetry_distribution', risk: 'high' }, ]; }; @@ -797,13 +784,13 @@ const generateOpportunitiesFromHeatmap = ( Math.min(10, Math.round(feasibilityRaw)) ); - // Dimensión a la que lo vinculamos (solo decorativo de momento) + // Dimensión a la que lo vinculamos const dimensionId = readiness >= 70 - ? 'volumetry_distribution' + ? 'agentic_readiness' : readiness >= 40 - ? 'efficiency' - : 'economy'; + ? 'effectiveness_resolution' + : 'complexity_predictability'; // Segmento de cliente (high/medium/low) si lo tenemos const customer_segment = heat.segment; @@ -1031,8 +1018,8 @@ const generateSyntheticAnalysis = ( { label: "CSAT", value: `${randomFloat(4.1, 4.8, 1)}/5`, change: `-${randomFloat(0.1, 0.3, 1)}`, changeType: 'negative' }, ]; - // v2.0: Solo 6 dimensiones - const dimensionKeys = ['volumetry_distribution', 'performance', 'satisfaction', 'economy', 'efficiency', 'benchmark']; + // v3.0: 5 dimensiones viables + const dimensionKeys = ['volumetry_distribution', 'operational_efficiency', 'effectiveness_resolution', 'complexity_predictability', 'agentic_readiness']; const dimensions: DimensionAnalysis[] = dimensionKeys.map(key => { const content = DIMENSIONS_CONTENT[key as keyof typeof DIMENSIONS_CONTENT]; diff --git a/frontend/utils/backendMapper.ts b/frontend/utils/backendMapper.ts index 6996f9f..aa29825 100644 --- a/frontend/utils/backendMapper.ts +++ b/frontend/utils/backendMapper.ts @@ -9,7 +9,7 @@ import type { EconomicModelData, } from '../types'; import type { BackendRawResults } from './apiClient'; -import { BarChartHorizontal, Zap, DollarSign, Smile, Target } from 'lucide-react'; +import { BarChartHorizontal, Zap, Target, Brain, Bot } from 'lucide-react'; import type { HeatmapDataPoint, CustomerSegment } from '../types'; @@ -336,57 +336,40 @@ function buildVolumetryDimension( return { dimension, extraKpis }; } -// ==== Performance (operational_performance) ==== +// ==== Eficiencia Operativa (v3.0) ==== -function buildPerformanceDimension( +function buildOperationalEfficiencyDimension( raw: BackendRawResults ): DimensionAnalysis | undefined { const op = raw?.operational_performance; if (!op) return undefined; - const perfScore0_10 = safeNumber(op.performance_score?.score, NaN); - if (!Number.isFinite(perfScore0_10)) return undefined; - - const score = Math.max( - 0, - Math.min(100, Math.round(perfScore0_10 * 10)) - ); - const ahtP50 = safeNumber(op.aht_distribution?.p50, 0); const ahtP90 = safeNumber(op.aht_distribution?.p90, 0); - const ratio = safeNumber(op.aht_distribution?.p90_p50_ratio, 0); - const escRate = safeNumber(op.escalation_rate, 0); + const ratio = ahtP90 > 0 && ahtP50 > 0 ? ahtP90 / ahtP50 : safeNumber(op.aht_distribution?.p90_p50_ratio, 1.5); - let summary = `El AHT mediano se sitúa en ${Math.round( - ahtP50 - )} segundos, con un P90 de ${Math.round( - ahtP90 - )}s (ratio P90/P50 ≈ ${ratio.toFixed( - 2 - )}) y una tasa de escalación del ${escRate.toFixed( - 1 - )}%. `; + // Score: menor ratio = mejor score (1.0 = 100, 3.0 = 0) + const score = Math.max(0, Math.min(100, Math.round(100 - (ratio - 1) * 50))); - if (score >= 80) { - summary += - 'El rendimiento operativo es sólido y se encuentra claramente por encima de los umbrales objetivo.'; - } else if (score >= 60) { - summary += - 'El rendimiento es aceptable pero existen oportunidades claras de optimización en algunos flujos.'; + let summary = `AHT P50: ${Math.round(ahtP50)}s, P90: ${Math.round(ahtP90)}s. Ratio P90/P50: ${ratio.toFixed(2)}. `; + + if (ratio < 1.5) { + summary += 'Tiempos consistentes y procesos estandarizados.'; + } else if (ratio < 2.0) { + summary += 'Variabilidad moderada, algunos casos outliers afectan la eficiencia.'; } else { - summary += - 'El rendimiento operativo está por debajo del nivel deseado y requiere un plan de mejora específico.'; + summary += 'Alta variabilidad en tiempos, requiere estandarización de procesos.'; } const kpi: Kpi = { - label: 'AHT mediano (P50)', - value: ahtP50 ? `${Math.round(ahtP50)}s` : 'N/D', + label: 'Ratio P90/P50', + value: ratio.toFixed(2), }; const dimension: DimensionAnalysis = { - id: 'performance', - name: 'performance', - title: 'Rendimiento operativo', + id: 'operational_efficiency', + name: 'operational_efficiency', + title: 'Eficiencia Operativa', score, percentile: undefined, summary, @@ -397,134 +380,49 @@ function buildPerformanceDimension( return dimension; } -// ==== Satisfacción (customer_satisfaction) ==== +// ==== Efectividad & Resolución (v3.0) ==== -function buildSatisfactionDimension( - raw: BackendRawResults -): DimensionAnalysis | undefined { - const cs = raw?.customer_satisfaction; - if (!cs) return undefined; - - // CSAT global viene ya calculado en el backend (1–5) - const csatGlobalRaw = safeNumber(cs?.csat_global, NaN); - if (!Number.isFinite(csatGlobalRaw) || csatGlobalRaw <= 0) { - return undefined; - } - - // Normalizamos 1–5 a 0–100 - const csat = Math.max(1, Math.min(5, csatGlobalRaw)); - const score = Math.max( - 0, - Math.min(100, Math.round((csat / 5) * 100)) - ); - - let summary = `CSAT global de ${csat.toFixed(1)}/5. `; - - if (score >= 85) { - summary += - 'La satisfacción del cliente es muy alta y consistente en la mayoría de interacciones.'; - } else if (score >= 70) { - summary += - 'La satisfacción del cliente es razonable, pero existen áreas claras de mejora en algunos journeys o motivos de contacto.'; - } else { - summary += - 'La satisfacción del cliente se sitúa por debajo de los niveles objetivo y requiere un plan de mejora específico sobre los principales drivers de insatisfacción.'; - } - - const kpi: Kpi = { - label: 'CSAT global (backend)', - value: `${csat.toFixed(1)}/5`, - }; - - const dimension: DimensionAnalysis = { - id: 'satisfaction', - name: 'satisfaction', - title: 'Voz del cliente y satisfacción', - score, - percentile: undefined, - summary, - kpi, - icon: Smile, - }; - - return dimension; -} - -// ==== Eficiencia (FCR + escalaciones + recurrencia) ==== - -function buildEfficiencyDimension( +function buildEffectivenessResolutionDimension( raw: BackendRawResults ): DimensionAnalysis | undefined { const op = raw?.operational_performance; if (!op) return undefined; - // FCR: viene como porcentaje 0–100, o lo aproximamos a partir de escalaciones const fcrPctRaw = safeNumber(op.fcr_rate, NaN); const escRateRaw = safeNumber(op.escalation_rate, NaN); const recurrenceRaw = safeNumber(op.recurrence_rate_7d, NaN); - const fcrPct = Number.isFinite(fcrPctRaw) && fcrPctRaw >= 0 + // FCR proxy: usar fcr_rate o calcular desde recurrence + const fcrProxy = Number.isFinite(fcrPctRaw) && fcrPctRaw >= 0 ? Math.max(0, Math.min(100, fcrPctRaw)) - : Number.isFinite(escRateRaw) - ? Math.max(0, Math.min(100, 100 - escRateRaw)) - : NaN; + : Number.isFinite(recurrenceRaw) + ? Math.max(0, Math.min(100, 100 - recurrenceRaw)) + : 75; // valor por defecto - if (!Number.isFinite(fcrPct)) { - // Sin FCR ni escalaciones no podemos construir bien la dimensión - return undefined; - } + const transferRate = Number.isFinite(escRateRaw) ? escRateRaw : 15; - let score = fcrPct; + // Score: FCR alto + transferencias bajas = mejor score + const score = Math.max(0, Math.min(100, Math.round(fcrProxy - transferRate * 0.5))); - // Penalizar por escalaciones altas - if (Number.isFinite(escRateRaw)) { - const esc = escRateRaw as number; - if (esc > 20) score -= 20; - else if (esc > 10) score -= 10; - else if (esc > 5) score -= 5; - } + let summary = `FCR proxy 7d: ${fcrProxy.toFixed(1)}%. Tasa de transferencias: ${transferRate.toFixed(1)}%. `; - // Penalizar por recurrencia (repetición de contactos a 7 días) - if (Number.isFinite(recurrenceRaw)) { - const rec = recurrenceRaw as number; // asumimos ya en % - if (rec > 20) score -= 15; - else if (rec > 10) score -= 10; - else if (rec > 5) score -= 5; - } - - score = Math.max(0, Math.min(100, Math.round(score))); - - const escText = Number.isFinite(escRateRaw) - ? `${(escRateRaw as number).toFixed(1)}%` - : 'N/D'; - const recText = Number.isFinite(recurrenceRaw) - ? `${(recurrenceRaw as number).toFixed(1)}%` - : 'N/D'; - - let summary = `FCR estimado de ${fcrPct.toFixed( - 1 - )}%, con una tasa de escalación del ${escText} y una recurrencia a 7 días de ${recText}. `; - - if (score >= 80) { - summary += - 'La operación presenta una alta tasa de resolución en primer contacto y pocas escalaciones, lo que indica procesos eficientes.'; - } else if (score >= 60) { - summary += - 'La eficiencia es razonable, aunque existen oportunidades de mejora en la resolución al primer contacto y en la reducción de contactos repetidos.'; + if (fcrProxy >= 85 && transferRate < 10) { + summary += 'Excelente resolución en primer contacto, mínimas transferencias.'; + } else if (fcrProxy >= 70) { + summary += 'Resolución aceptable, oportunidad de reducir recontactos y transferencias.'; } else { - summary += - 'La eficiencia operativa es baja: hay demasiadas escalaciones o contactos repetidos, lo que impacta negativamente en costes y experiencia de cliente.'; + summary += 'Baja resolución, alto recontacto a 7 días. Requiere mejora de procesos.'; } const kpi: Kpi = { - label: 'FCR estimado (backend)', - value: `${fcrPct.toFixed(1)}%`, + label: 'FCR Proxy 7d', + value: `${fcrProxy.toFixed(1)}%`, }; const dimension: DimensionAnalysis = { - id: 'efficiency', - name: 'efficiency', - title: 'Resolución y eficiencia', + id: 'effectiveness_resolution', + name: 'effectiveness_resolution', + title: 'Efectividad & Resolución', score, percentile: undefined, summary, @@ -535,6 +433,129 @@ function buildEfficiencyDimension( return dimension; } +// ==== Complejidad & Predictibilidad (v3.0) ==== + +function buildComplexityPredictabilityDimension( + raw: BackendRawResults +): DimensionAnalysis | undefined { + const op = raw?.operational_performance; + if (!op) return undefined; + + const ahtP50 = safeNumber(op.aht_distribution?.p50, 0); + const ahtP90 = safeNumber(op.aht_distribution?.p90, 0); + const ratio = ahtP50 > 0 ? ahtP90 / ahtP50 : 2; + const escalationRate = safeNumber(op.escalation_rate, 15); + + // Score: menor ratio + menos escalaciones = mayor score (más predecible) + const ratioScore = Math.max(0, Math.min(50, 50 - (ratio - 1) * 25)); + const escalationScore = Math.max(0, Math.min(50, 50 - escalationRate)); + const score = Math.round(ratioScore + escalationScore); + + let summary = `Variabilidad AHT (ratio P90/P50): ${ratio.toFixed(2)}. % transferencias: ${escalationRate.toFixed(1)}%. `; + + if (ratio < 1.5 && escalationRate < 10) { + summary += 'Proceso altamente predecible y baja complejidad. Excelente candidato para automatización.'; + } else if (ratio < 2.0) { + summary += 'Complejidad moderada, algunos casos requieren atención especial.'; + } else { + summary += 'Alta complejidad y variabilidad. Requiere optimización antes de automatizar.'; + } + + const kpi: Kpi = { + label: 'Ratio P90/P50', + value: ratio.toFixed(2), + }; + + const dimension: DimensionAnalysis = { + id: 'complexity_predictability', + name: 'complexity_predictability', + title: 'Complejidad & Predictibilidad', + score, + percentile: undefined, + summary, + kpi, + icon: Brain, + }; + + return dimension; +} + +// ==== Agentic Readiness como dimensión (v3.0) ==== + +function buildAgenticReadinessDimension( + raw: BackendRawResults, + fallbackTier: TierKey +): DimensionAnalysis | undefined { + const ar = raw?.agentic_readiness?.agentic_readiness; + + // Si no hay datos de backend, calculamos un score aproximado + const op = raw?.operational_performance; + const volumetry = raw?.volumetry; + + let score0_10: number; + let category: string; + + if (ar) { + score0_10 = safeNumber(ar.final_score, 5); + } else { + // Calcular aproximado desde métricas disponibles + const ahtP50 = safeNumber(op?.aht_distribution?.p50, 0); + const ahtP90 = safeNumber(op?.aht_distribution?.p90, 0); + const ratio = ahtP50 > 0 ? ahtP90 / ahtP50 : 2; + const escalation = safeNumber(op?.escalation_rate, 15); + + const skillVolumes = Array.isArray(volumetry?.volume_by_skill?.values) + ? volumetry.volume_by_skill.values.map((v: any) => safeNumber(v, 0)) + : []; + const totalVolume = skillVolumes.reduce((a: number, b: number) => a + b, 0); + + // Calcular sub-scores + const predictability = Math.max(0, Math.min(10, 10 - (ratio - 1) * 5)); + const complexityInverse = Math.max(0, Math.min(10, 10 - escalation / 5)); + const repetitivity = Math.min(10, totalVolume / 500); + + score0_10 = predictability * 0.30 + complexityInverse * 0.30 + repetitivity * 0.25 + 2.5; // base offset + } + + const score0_100 = Math.max(0, Math.min(100, Math.round(score0_10 * 10))); + + if (score0_10 >= 8) { + category = 'Automatizar'; + } else if (score0_10 >= 5) { + category = 'Asistir (Copilot)'; + } else { + category = 'Optimizar primero'; + } + + let summary = `Score global: ${score0_10.toFixed(1)}/10. Categoría: ${category}. `; + + if (score0_10 >= 8) { + summary += 'Excelente candidato para automatización completa con agentes IA.'; + } else if (score0_10 >= 5) { + summary += 'Candidato para asistencia con IA (copilot) o automatización parcial.'; + } else { + summary += 'Requiere optimización de procesos antes de automatizar.'; + } + + const kpi: Kpi = { + label: 'Score Global', + value: `${score0_10.toFixed(1)}/10`, + }; + + const dimension: DimensionAnalysis = { + id: 'agentic_readiness', + name: 'agentic_readiness', + title: 'Agentic Readiness', + score: score0_100, + percentile: undefined, + summary, + kpi, + icon: Bot, + }; + + return dimension; +} + // ==== Economía y costes (economy_costs) ==== @@ -627,58 +648,7 @@ function buildEconomicModel(raw: BackendRawResults): EconomicModelData { }; } -function buildEconomyDimension( - raw: BackendRawResults -): DimensionAnalysis | undefined { - const econ = raw?.economy_costs; - if (!econ) return undefined; - - const cost = econ.cost_breakdown || {}; - const totalAnnual = safeNumber(cost.total_annual, 0); - const potential = econ.potential_savings || {}; - const annualSavings = safeNumber(potential.annual_savings, 0); - - if (!totalAnnual && !annualSavings) return undefined; - - const savingsPct = totalAnnual - ? (annualSavings / totalAnnual) * 100 - : 0; - - let summary = `El coste anual estimado de la operación es de aproximadamente €${totalAnnual.toFixed( - 2 - )}. `; - if (annualSavings > 0) { - summary += `El ahorro potencial anual asociado a la estrategia agentic se sitúa en torno a €${annualSavings.toFixed( - 2 - )}, equivalente a ~${savingsPct.toFixed(1)}% del coste actual.`; - } else { - summary += - 'Todavía no se dispone de una estimación robusta de ahorro potencial.'; - } - - const score = - totalAnnual && annualSavings - ? Math.max(0, Math.min(100, Math.round(savingsPct))) - : 50; - - const dimension: DimensionAnalysis = { - id: 'economy', - name: 'economy', - title: 'Economía y costes', - score, - percentile: undefined, - summary, - kpi: { - label: 'Coste anual actual', - value: totalAnnual - ? `€${totalAnnual.toFixed(0)}` - : 'N/D', - }, - icon: DollarSign, - }; - - return dimension; -} +// buildEconomyDimension eliminado en v3.0 - economía integrada en otras dimensiones y modelo económico /** * Transforma el JSON del backend (results) al AnalysisData @@ -722,20 +692,20 @@ export function mapBackendResultsToAnalysisData( Math.min(100, Math.round(arScore * 10)) ); - // Dimensiones + // v3.0: 5 dimensiones viables const { dimension: volumetryDimension, extraKpis } = buildVolumetryDimension(raw); - const performanceDimension = buildPerformanceDimension(raw); - const satisfactionDimension = buildSatisfactionDimension(raw); - const economyDimension = buildEconomyDimension(raw); - const efficiencyDimension = buildEfficiencyDimension(raw); + const operationalEfficiencyDimension = buildOperationalEfficiencyDimension(raw); + const effectivenessResolutionDimension = buildEffectivenessResolutionDimension(raw); + const complexityPredictabilityDimension = buildComplexityPredictabilityDimension(raw); + const agenticReadinessDimension = buildAgenticReadinessDimension(raw, tierFromFrontend || 'silver'); const dimensions: DimensionAnalysis[] = []; if (volumetryDimension) dimensions.push(volumetryDimension); - if (performanceDimension) dimensions.push(performanceDimension); - if (satisfactionDimension) dimensions.push(satisfactionDimension); - if (economyDimension) dimensions.push(economyDimension); - if (efficiencyDimension) dimensions.push(efficiencyDimension); + if (operationalEfficiencyDimension) dimensions.push(operationalEfficiencyDimension); + if (effectivenessResolutionDimension) dimensions.push(effectivenessResolutionDimension); + if (complexityPredictabilityDimension) dimensions.push(complexityPredictabilityDimension); + if (agenticReadinessDimension) dimensions.push(agenticReadinessDimension); const op = raw?.operational_performance; diff --git a/frontend/utils/realDataAnalysis.ts b/frontend/utils/realDataAnalysis.ts index d91b952..e9ef3b3 100644 --- a/frontend/utils/realDataAnalysis.ts +++ b/frontend/utils/realDataAnalysis.ts @@ -4,7 +4,7 @@ import type { AnalysisData, Kpi, DimensionAnalysis, HeatmapDataPoint, Opportunity, RoadmapInitiative, EconomicModelData, BenchmarkDataPoint, Finding, Recommendation, TierKey, CustomerSegment, RawInteraction, AgenticReadinessResult, SubFactor, SkillMetrics } from '../types'; import { RoadmapPhase } from '../types'; -import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react'; +import { BarChartHorizontal, Zap, Target, Brain, Bot } from 'lucide-react'; import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2'; import { classifyQueue } from './segmentClassifier'; @@ -287,7 +287,7 @@ function calculateHealthScore(heatmapData: HeatmapDataPoint[]): number { } /** - * Generar dimensiones desde datos reales + * v3.0: Generar 5 dimensiones viables desde datos reales */ function generateDimensionsFromRealData( interactions: RawInteraction[], @@ -298,69 +298,72 @@ function generateDimensionsFromRealData( const totalVolume = interactions.length; const avgCV = metrics.reduce((sum, m) => sum + m.cv_aht, 0) / metrics.length; const avgTransferRate = metrics.reduce((sum, m) => sum + m.transfer_rate, 0) / metrics.length; - + const avgHoldTime = metrics.reduce((sum, m) => sum + m.hold_time_mean, 0) / metrics.length; + + // Calcular ratio P90/P50 aproximado desde CV + const avgRatio = 1 + avgCV * 1.5; // Aproximación: ratio ≈ 1 + 1.5*CV + + // Calcular Agentic Score + const predictability = Math.max(0, Math.min(10, 10 - ((avgCV - 0.3) / 1.2 * 10))); + const complexityInverse = Math.max(0, Math.min(10, 10 - (avgTransferRate / 10))); + const repetitivity = Math.min(10, totalVolume / 500); + const agenticScore = predictability * 0.30 + complexityInverse * 0.30 + repetitivity * 0.25 + 2.5; + return [ + // 1. VOLUMETRÍA & DISTRIBUCIÓN { id: 'volumetry_distribution', name: 'volumetry_distribution', - title: 'Análisis de la Demanda', - score: Math.min(100, Math.round((totalVolume / 200))), // Score basado en volumen + title: 'Volumetría & Distribución', + score: Math.min(100, Math.round((totalVolume / 200))), percentile: 65, - summary: `Se procesaron ${totalVolume.toLocaleString('es-ES')} interacciones distribuidas en ${metrics.length} skills diferentes.`, + summary: `${totalVolume.toLocaleString('es-ES')} interacciones en ${metrics.length} colas. Distribución por skill disponible en el heatmap.`, kpi: { label: 'Volumen Total', value: totalVolume.toLocaleString('es-ES') }, icon: BarChartHorizontal }, + // 2. EFICIENCIA OPERATIVA { - id: 'performance', - name: 'performance', - title: 'Rendimiento Operativo', - score: Math.round(100 - (avgCV * 100)), + id: 'operational_efficiency', + name: 'operational_efficiency', + title: 'Eficiencia Operativa', + score: Math.max(0, Math.min(100, Math.round(100 - (avgRatio - 1) * 50))), percentile: 70, - summary: avgCV < 0.4 - ? 'El AHT muestra baja variabilidad, indicando procesos estandarizados.' - : 'La variabilidad del AHT es alta, sugiriendo inconsistencia en procesos.', - kpi: { label: 'AHT Promedio', value: `${avgAHT}s` }, + summary: `AHT P50: ${avgAHT}s. Ratio P90/P50 estimado: ${avgRatio.toFixed(2)}. Hold time promedio: ${Math.round(avgHoldTime)}s.`, + kpi: { label: 'Ratio P90/P50', value: avgRatio.toFixed(2) }, icon: Zap }, + // 3. EFECTIVIDAD & RESOLUCIÓN { - id: 'satisfaction', - name: 'satisfaction', - title: 'Voz del Cliente', - score: avgCsat, - percentile: 60, - summary: `CSAT promedio de ${(avgCsat / 20).toFixed(1)}/5.`, - kpi: { label: 'CSAT', value: `${(avgCsat / 20).toFixed(1)}/5` }, - icon: Smile - }, - { - id: 'economy', - name: 'economy', - title: 'Rentabilidad del Servicio', - score: Math.round(100 - avgTransferRate), - percentile: 55, - summary: `Tasa de transferencia del ${avgTransferRate.toFixed(1)}%.`, - kpi: { label: 'Transfer Rate', value: `${avgTransferRate.toFixed(1)}%` }, - icon: DollarSign - }, - { - id: 'efficiency', - name: 'efficiency', - title: 'Resolución y Calidad', + id: 'effectiveness_resolution', + name: 'effectiveness_resolution', + title: 'Efectividad & Resolución', score: Math.round(100 - avgTransferRate), percentile: 68, - summary: `FCR estimado del ${(100 - avgTransferRate).toFixed(1)}%.`, - kpi: { label: 'FCR', value: `${(100 - avgTransferRate).toFixed(1)}%` }, + summary: `FCR proxy: ${(100 - avgTransferRate).toFixed(1)}%. Tasa de transferencias: ${avgTransferRate.toFixed(1)}%.`, + kpi: { label: 'FCR Proxy', value: `${(100 - avgTransferRate).toFixed(1)}%` }, icon: Target }, + // 4. COMPLEJIDAD & PREDICTIBILIDAD { - id: 'benchmark', - name: 'benchmark', - title: 'Contexto Competitivo', - score: 75, + id: 'complexity_predictability', + name: 'complexity_predictability', + title: 'Complejidad & Predictibilidad', + score: Math.round(100 - (avgCV * 100)), + percentile: 60, + summary: `CV AHT: ${(avgCV * 100).toFixed(1)}%. % transferencias: ${avgTransferRate.toFixed(1)}%. ${avgCV < 0.4 ? 'Proceso predecible.' : 'Alta variabilidad, considerar estandarización.'}`, + kpi: { label: 'CV AHT', value: `${(avgCV * 100).toFixed(1)}%` }, + icon: Brain + }, + // 5. AGENTIC READINESS + { + id: 'agentic_readiness', + name: 'agentic_readiness', + title: 'Agentic Readiness', + score: Math.round(agenticScore * 10), percentile: 65, - summary: 'Métricas alineadas con benchmarks de la industria.', - kpi: { label: 'Benchmark', value: 'P65' }, - icon: Globe + summary: `Score: ${agenticScore.toFixed(1)}/10. ${agenticScore >= 8 ? 'Excelente para automatización.' : agenticScore >= 5 ? 'Candidato para asistencia IA.' : 'Requiere optimización previa.'}`, + kpi: { label: 'Score', value: `${agenticScore.toFixed(1)}/10` }, + icon: Bot } ]; } diff --git a/nginx/conf.d/beyond.conf b/nginx/conf.d/beyond.conf new file mode 100644 index 0000000..391912a --- /dev/null +++ b/nginx/conf.d/beyond.conf @@ -0,0 +1,43 @@ +server { + listen 80; + server_name ae-analytics.beyondcx.ai; + return 301 https://$host$request_uri; + client_max_body_size 1024M; +} + +server { + listen 443 ssl; + server_name ae-analytics.beyondcx.ai; + + client_max_body_size 1024M; + + ssl_certificate /etc/letsencrypt/live/ae-analytics.beyondcx.ai/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ae-analytics.beyondcx.ai/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # FRONTEND (React) + location / { + proxy_pass http://frontend:4173/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # BACKEND (FastAPI) + location /api/ { + proxy_pass http://backend:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + send_timeout 600s; + } +}