Ajustes en la conexión front-back y eliminación datos demo
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
"class": "beyond_metrics.dimensions.SatisfactionExperience.SatisfactionExperienceMetrics",
|
"class": "beyond_metrics.dimensions.SatisfactionExperience.SatisfactionExperienceMetrics",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"metrics": [
|
"metrics": [
|
||||||
|
"csat_global",
|
||||||
"csat_avg_by_skill_channel",
|
"csat_avg_by_skill_channel",
|
||||||
"nps_avg_by_skill_channel",
|
"nps_avg_by_skill_channel",
|
||||||
"ces_avg_by_skill_channel",
|
"ces_avg_by_skill_channel",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.axes import Axes
|
from matplotlib.axes import Axes
|
||||||
|
import math
|
||||||
|
|
||||||
REQUIRED_COLUMNS_OP: List[str] = [
|
REQUIRED_COLUMNS_OP: List[str] = [
|
||||||
"interaction_id",
|
"interaction_id",
|
||||||
@@ -165,21 +165,57 @@ class OperationalPerformanceMetrics:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def fcr_rate(self) -> float:
|
def fcr_rate(self) -> float:
|
||||||
"""
|
"""
|
||||||
FCR = % de interacciones resueltas en el primer contacto.
|
FCR proxy = 100 - escalation_rate.
|
||||||
|
|
||||||
Definido como % de filas con is_resolved == True.
|
Usamos la métrica de escalación ya calculada a partir de transfer_flag.
|
||||||
Si la columna no existe, devuelve NaN.
|
Si no se puede calcular escalation_rate, intentamos derivarlo
|
||||||
|
directamente de la columna transfer_flag. Si todo falla, devolvemos NaN.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
esc = self.escalation_rate()
|
||||||
|
except Exception:
|
||||||
|
esc = float("nan")
|
||||||
|
|
||||||
|
# Si escalation_rate es válido, usamos el proxy simple
|
||||||
|
if esc is not None and not math.isnan(esc):
|
||||||
|
fcr = 100.0 - esc
|
||||||
|
return float(max(0.0, min(100.0, round(fcr, 2))))
|
||||||
|
|
||||||
|
# Fallback: calcular directamente desde transfer_flag
|
||||||
df = self.df
|
df = self.df
|
||||||
if "is_resolved" not in df.columns:
|
if "transfer_flag" not in df.columns or len(df) == 0:
|
||||||
return float("nan")
|
return float("nan")
|
||||||
|
|
||||||
|
col = df["transfer_flag"]
|
||||||
|
|
||||||
|
# Normalizar a booleano: TRUE/FALSE, 1/0, etc.
|
||||||
|
if col.dtype == "O":
|
||||||
|
col_norm = (
|
||||||
|
col.astype(str)
|
||||||
|
.str.strip()
|
||||||
|
.str.lower()
|
||||||
|
.map({
|
||||||
|
"true": True,
|
||||||
|
"t": True,
|
||||||
|
"1": True,
|
||||||
|
"yes": True,
|
||||||
|
"y": True,
|
||||||
|
})
|
||||||
|
).fillna(False)
|
||||||
|
transfer_mask = col_norm
|
||||||
|
else:
|
||||||
|
transfer_mask = pd.to_numeric(col, errors="coerce").fillna(0) > 0
|
||||||
|
|
||||||
total = len(df)
|
total = len(df)
|
||||||
if total == 0:
|
transfers = int(transfer_mask.sum())
|
||||||
|
|
||||||
|
esc_rate = transfers / total if total > 0 else float("nan")
|
||||||
|
if math.isnan(esc_rate):
|
||||||
return float("nan")
|
return float("nan")
|
||||||
|
|
||||||
resolved = df["is_resolved"].sum()
|
fcr = 100.0 - esc_rate * 100.0
|
||||||
return float(round(resolved / total * 100, 2))
|
return float(max(0.0, min(100.0, round(fcr, 2))))
|
||||||
|
|
||||||
|
|
||||||
def escalation_rate(self) -> float:
|
def escalation_rate(self) -> float:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -160,6 +160,26 @@ class SatisfactionExperienceMetrics:
|
|||||||
.round(2)
|
.round(2)
|
||||||
)
|
)
|
||||||
return pivot
|
return pivot
|
||||||
|
|
||||||
|
def csat_global(self) -> float:
|
||||||
|
"""
|
||||||
|
CSAT medio global (todas las interacciones).
|
||||||
|
|
||||||
|
Usa la columna opcional `csat_score`:
|
||||||
|
- Si no existe, devuelve NaN.
|
||||||
|
- Si todos los valores son NaN / vacíos, devuelve NaN.
|
||||||
|
"""
|
||||||
|
df = self.df
|
||||||
|
if "csat_score" not in df.columns:
|
||||||
|
return float("nan")
|
||||||
|
|
||||||
|
series = pd.to_numeric(df["csat_score"], errors="coerce").dropna()
|
||||||
|
if series.empty:
|
||||||
|
return float("nan")
|
||||||
|
|
||||||
|
mean = series.mean()
|
||||||
|
return float(round(mean, 2))
|
||||||
|
|
||||||
|
|
||||||
def csat_aht_correlation(self) -> Dict[str, Any]:
|
def csat_aht_correlation(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
1001
backend/data/example/call_center_dataset.csv
Normal file
1001
backend/data/example/call_center_dataset.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,8 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
{ name: 'wrap_up_time', type: 'Segundos', example: '30', required: true },
|
{ name: 'wrap_up_time', type: 'Segundos', example: '30', required: true },
|
||||||
{ name: 'agent_id', type: 'String', example: 'Agente_045', required: true },
|
{ name: 'agent_id', type: 'String', example: 'Agente_045', required: true },
|
||||||
{ name: 'transfer_flag', type: 'Boolean', example: 'TRUE / FALSE', required: true },
|
{ name: 'transfer_flag', type: 'Boolean', example: 'TRUE / FALSE', required: true },
|
||||||
{ name: 'caller_id', type: 'String (hash)', example: 'Hash_99283', required: false }
|
{ name: 'caller_id', type: 'String (hash)', example: 'Hash_99283', required: false },
|
||||||
|
{ name: 'csat_score', type: 'Float', example: '4', required: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleDownloadTemplate = () => {
|
const handleDownloadTemplate = () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
export type TierKey = 'gold' | 'silver' | 'bronze';
|
export type TierKey = 'gold' | 'silver' | 'bronze';
|
||||||
|
export type AnalysisSource = 'synthetic' | 'backend' | 'fallback';
|
||||||
|
|
||||||
export interface Tier {
|
export interface Tier {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -269,4 +270,5 @@ export interface AnalysisData {
|
|||||||
benchmarkData: BenchmarkDataPoint[]; // Actualizado de benchmarkReport
|
benchmarkData: BenchmarkDataPoint[]; // Actualizado de benchmarkReport
|
||||||
agenticReadiness?: AgenticReadinessResult; // v2.0: Nuevo campo
|
agenticReadiness?: AgenticReadinessResult; // v2.0: Nuevo campo
|
||||||
staticConfig?: StaticConfig; // v2.0: Configuración estática usada
|
staticConfig?: StaticConfig; // v2.0: Configuración estática usada
|
||||||
|
source?: AnalysisSource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { RoadmapPhase } from '../types';
|
|||||||
import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react';
|
import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react';
|
||||||
import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2';
|
import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2';
|
||||||
import { callAnalysisApiRaw } from './apiClient';
|
import { callAnalysisApiRaw } from './apiClient';
|
||||||
import { mapBackendResultsToAnalysisData } from './backendMapper';
|
import {
|
||||||
|
mapBackendResultsToAnalysisData,
|
||||||
|
buildHeatmapFromBackend,
|
||||||
|
} from './backendMapper';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
@@ -568,8 +572,18 @@ export const generateAnalysis = async (
|
|||||||
mapped.recommendations = generateRecommendationsFromTemplates();
|
mapped.recommendations = generateRecommendationsFromTemplates();
|
||||||
mapped.opportunities = generateOpportunityMatrixData();
|
mapped.opportunities = generateOpportunityMatrixData();
|
||||||
mapped.roadmap = generateRoadmapData();
|
mapped.roadmap = generateRoadmapData();
|
||||||
mapped.benchmarkData = generateBenchmarkData();
|
|
||||||
mapped.heatmapData = generateHeatmapData(costPerHour, avgCsat, segmentMapping);
|
// Benchmark: de momento no tenemos datos reales -> no lo generamos en modo backend
|
||||||
|
mapped.benchmarkData = [];
|
||||||
|
|
||||||
|
// Heatmap: ahora se construye a partir de datos reales del backend
|
||||||
|
mapped.heatmapData = buildHeatmapFromBackend(
|
||||||
|
raw,
|
||||||
|
costPerHour,
|
||||||
|
avgCsat,
|
||||||
|
segmentMapping
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
console.log('✅ Usando resultados del backend mapeados + findings/benchmark del frontend');
|
console.log('✅ Usando resultados del backend mapeados + findings/benchmark del frontend');
|
||||||
return mapped;
|
return mapped;
|
||||||
@@ -734,6 +748,7 @@ const generateSyntheticAnalysis = (
|
|||||||
economicModel: generateEconomicModelData(),
|
economicModel: generateEconomicModelData(),
|
||||||
roadmap: generateRoadmapData(),
|
roadmap: generateRoadmapData(),
|
||||||
benchmarkData: generateBenchmarkData(),
|
benchmarkData: generateBenchmarkData(),
|
||||||
|
source: 'synthetic',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import type { BackendRawResults } from './apiClient';
|
import type { BackendRawResults } from './apiClient';
|
||||||
import { BarChartHorizontal, Zap, DollarSign } from 'lucide-react';
|
import { BarChartHorizontal, Zap, DollarSign } from 'lucide-react';
|
||||||
|
import type { HeatmapDataPoint, CustomerSegment } from '../types';
|
||||||
|
|
||||||
|
|
||||||
function safeNumber(value: any, fallback = 0): number {
|
function safeNumber(value: any, fallback = 0): number {
|
||||||
const n = typeof value === 'number' ? value : Number(value);
|
const n = typeof value === 'number' ? value : Number(value);
|
||||||
@@ -577,17 +579,67 @@ export function mapBackendResultsToAnalysisData(
|
|||||||
if (performanceDimension) dimensions.push(performanceDimension);
|
if (performanceDimension) dimensions.push(performanceDimension);
|
||||||
if (economyDimension) dimensions.push(economyDimension);
|
if (economyDimension) dimensions.push(economyDimension);
|
||||||
|
|
||||||
// KPIs de resumen
|
const op = raw?.operational_performance;
|
||||||
|
const cs = raw?.customer_satisfaction;
|
||||||
|
|
||||||
|
// FCR: viene ya como porcentaje 0-100
|
||||||
|
const fcrPctRaw = safeNumber(op?.fcr_rate, NaN);
|
||||||
|
const fcrPct =
|
||||||
|
Number.isFinite(fcrPctRaw) && fcrPctRaw >= 0
|
||||||
|
? Math.min(100, Math.max(0, fcrPctRaw))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const csatAvg = computeCsatAverage(cs);
|
||||||
|
|
||||||
|
// CSAT global (opcional)
|
||||||
|
const csatGlobalRaw = safeNumber(cs?.csat_global, NaN);
|
||||||
|
const csatGlobal =
|
||||||
|
Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0
|
||||||
|
? csatGlobalRaw
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
|
||||||
|
// KPIs de resumen (los 4 primeros son los que se ven en "Métricas de Contacto")
|
||||||
const summaryKpis: Kpi[] = [];
|
const summaryKpis: Kpi[] = [];
|
||||||
|
|
||||||
|
// 1) Interacciones Totales (volumen backend)
|
||||||
summaryKpis.push({
|
summaryKpis.push({
|
||||||
label: 'Volumen total (estimado)',
|
label: 'Interacciones Totales',
|
||||||
value:
|
value:
|
||||||
totalVolume > 0
|
totalVolume > 0
|
||||||
? totalVolume.toLocaleString('es-ES')
|
? totalVolume.toLocaleString('es-ES')
|
||||||
: 'N/A',
|
: 'N/D',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2) AHT Promedio (P50 de distribución de AHT)
|
||||||
|
const ahtP50 = safeNumber(op?.aht_distribution?.p50, 0);
|
||||||
|
summaryKpis.push({
|
||||||
|
label: 'AHT Promedio',
|
||||||
|
value: ahtP50
|
||||||
|
? `${Math.round(ahtP50)}s`
|
||||||
|
: 'N/D',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Tasa FCR
|
||||||
|
summaryKpis.push({
|
||||||
|
label: 'Tasa FCR',
|
||||||
|
value:
|
||||||
|
fcrPct !== undefined
|
||||||
|
? `${Math.round(fcrPct)}%`
|
||||||
|
: 'N/D',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) CSAT
|
||||||
|
summaryKpis.push({
|
||||||
|
label: 'CSAT',
|
||||||
|
value:
|
||||||
|
csatGlobal !== undefined
|
||||||
|
? `${csatGlobal.toFixed(1)}/5`
|
||||||
|
: 'N/D',
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- KPIs adicionales, usados en otras secciones ---
|
||||||
|
|
||||||
if (numChannels > 0) {
|
if (numChannels > 0) {
|
||||||
summaryKpis.push({
|
summaryKpis.push({
|
||||||
label: 'Canales analizados',
|
label: 'Canales analizados',
|
||||||
@@ -607,7 +659,7 @@ export function mapBackendResultsToAnalysisData(
|
|||||||
value: `${arScore.toFixed(1)}/10`,
|
value: `${arScore.toFixed(1)}/10`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// KPIs de economía
|
// KPIs de economía (backend)
|
||||||
const econ = raw?.economy_costs;
|
const econ = raw?.economy_costs;
|
||||||
const totalAnnual = safeNumber(
|
const totalAnnual = safeNumber(
|
||||||
econ?.cost_breakdown?.total_annual,
|
econ?.cost_breakdown?.total_annual,
|
||||||
@@ -649,5 +701,277 @@ export function mapBackendResultsToAnalysisData(
|
|||||||
benchmarkData: [],
|
benchmarkData: [],
|
||||||
agenticReadiness,
|
agenticReadiness,
|
||||||
staticConfig: undefined,
|
staticConfig: undefined,
|
||||||
|
source: 'backend',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildHeatmapFromBackend(
|
||||||
|
raw: BackendRawResults,
|
||||||
|
costPerHour: number,
|
||||||
|
avgCsat: number,
|
||||||
|
segmentMapping?: {
|
||||||
|
high_value_queues: string[];
|
||||||
|
medium_value_queues: string[];
|
||||||
|
low_value_queues: string[];
|
||||||
|
}
|
||||||
|
): HeatmapDataPoint[] {
|
||||||
|
const volumetry = raw?.volumetry;
|
||||||
|
const volumeBySkill = volumetry?.volume_by_skill;
|
||||||
|
|
||||||
|
const rawSkillLabels =
|
||||||
|
volumeBySkill?.labels ??
|
||||||
|
volumeBySkill?.skills ??
|
||||||
|
volumeBySkill?.skill_names ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
const skillLabels: string[] = Array.isArray(rawSkillLabels)
|
||||||
|
? rawSkillLabels.map((s: any) => String(s))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const skillVolumes: number[] = Array.isArray(volumeBySkill?.values)
|
||||||
|
? volumeBySkill.values.map((v: any) => safeNumber(v, 0))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const op = raw?.operational_performance;
|
||||||
|
const econ = raw?.economy_costs;
|
||||||
|
const cs = raw?.customer_satisfaction;
|
||||||
|
|
||||||
|
const talkHoldAcwBySkill = Array.isArray(
|
||||||
|
op?.talk_hold_acw_p50_by_skill
|
||||||
|
)
|
||||||
|
? op.talk_hold_acw_p50_by_skill
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const globalEscalation = safeNumber(op?.escalation_rate, 0);
|
||||||
|
const globalFcrPct = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(100, 100 - globalEscalation)
|
||||||
|
);
|
||||||
|
|
||||||
|
const csatGlobalRaw = safeNumber(cs?.csat_global, NaN);
|
||||||
|
const csatGlobal =
|
||||||
|
Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0
|
||||||
|
? csatGlobalRaw
|
||||||
|
: undefined;
|
||||||
|
const csatMetric0_100 = csatGlobal
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(100, Math.round((csatGlobal / 5) * 100))
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const ineffBySkill = Array.isArray(
|
||||||
|
econ?.inefficiency_cost_by_skill_channel
|
||||||
|
)
|
||||||
|
? econ.inefficiency_cost_by_skill_channel
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const COST_PER_SECOND = costPerHour / 3600;
|
||||||
|
|
||||||
|
if (!skillLabels.length) return [];
|
||||||
|
|
||||||
|
// Para normalizar la repetitividad según volumen
|
||||||
|
const volumesForNorm = skillVolumes.filter((v) => v > 0);
|
||||||
|
const minVol =
|
||||||
|
volumesForNorm.length > 0
|
||||||
|
? Math.min(...volumesForNorm)
|
||||||
|
: 0;
|
||||||
|
const maxVol =
|
||||||
|
volumesForNorm.length > 0
|
||||||
|
? Math.max(...volumesForNorm)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const heatmap: HeatmapDataPoint[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < skillLabels.length; i++) {
|
||||||
|
const skill = skillLabels[i];
|
||||||
|
const volume = safeNumber(skillVolumes[i], 0);
|
||||||
|
|
||||||
|
const talkHold = talkHoldAcwBySkill[i] || {};
|
||||||
|
const talk_p50 = safeNumber(talkHold.talk_p50, 0);
|
||||||
|
const hold_p50 = safeNumber(talkHold.hold_p50, 0);
|
||||||
|
const acw_p50 = safeNumber(talkHold.acw_p50, 0);
|
||||||
|
|
||||||
|
const aht_mean = talk_p50 + hold_p50 + acw_p50;
|
||||||
|
|
||||||
|
// Coste anual aproximado
|
||||||
|
const annual_volume = volume * 12;
|
||||||
|
const annual_cost = Math.round(
|
||||||
|
annual_volume * aht_mean * COST_PER_SECOND
|
||||||
|
);
|
||||||
|
|
||||||
|
const ineff = ineffBySkill[i] || {};
|
||||||
|
const aht_p50_backend = safeNumber(ineff.aht_p50, aht_mean);
|
||||||
|
const aht_p90_backend = safeNumber(ineff.aht_p90, aht_mean);
|
||||||
|
|
||||||
|
// Variabilidad proxy: aproximamos CV a partir de P90-P50
|
||||||
|
let cv_aht = 0;
|
||||||
|
if (aht_p50_backend > 0) {
|
||||||
|
cv_aht =
|
||||||
|
(aht_p90_backend - aht_p50_backend) / aht_p50_backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dimensiones agentic similares a las que tenías en generateHeatmapData,
|
||||||
|
// pero usando valores reales en lugar de aleatorios.
|
||||||
|
|
||||||
|
// 1) Predictibilidad (menor CV => mayor puntuación)
|
||||||
|
const predictability_score = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
10,
|
||||||
|
10 - ((cv_aht - 0.3) / 1.2) * 10
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) Complejidad inversa (usamos la tasa global de escalación como proxy)
|
||||||
|
const transfer_rate = globalEscalation; // %
|
||||||
|
const complexity_inverse_score = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
10,
|
||||||
|
10 - ((transfer_rate / 100 - 0.05) / 0.25) * 10
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3) Repetitividad (según volumen relativo)
|
||||||
|
let repetitivity_score = 5;
|
||||||
|
if (maxVol > minVol && volume > 0) {
|
||||||
|
repetitivity_score =
|
||||||
|
((volume - minVol) / (maxVol - minVol)) * 10;
|
||||||
|
} else if (volume === 0) {
|
||||||
|
repetitivity_score = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentic_readiness_score =
|
||||||
|
predictability_score * 0.4 +
|
||||||
|
complexity_inverse_score * 0.35 +
|
||||||
|
repetitivity_score * 0.25;
|
||||||
|
|
||||||
|
let readiness_category:
|
||||||
|
| 'automate_now'
|
||||||
|
| 'assist_copilot'
|
||||||
|
| 'optimize_first';
|
||||||
|
if (agentic_readiness_score >= 8.0) {
|
||||||
|
readiness_category = 'automate_now';
|
||||||
|
} else if (agentic_readiness_score >= 5.0) {
|
||||||
|
readiness_category = 'assist_copilot';
|
||||||
|
} else {
|
||||||
|
readiness_category = 'optimize_first';
|
||||||
|
}
|
||||||
|
|
||||||
|
const automation_readiness = Math.round(
|
||||||
|
agentic_readiness_score * 10
|
||||||
|
); // 0-100
|
||||||
|
|
||||||
|
// Métricas normalizadas 0-100 para el color del heatmap
|
||||||
|
const ahtMetric = aht_mean
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
100 - ((aht_mean - 240) / 310) * 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const holdMetric = hold_p50
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
100 - (hold_p50 / 120) * 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const transferMetric = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(100 - transfer_rate)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clasificación por segmento (si nos pasan mapeo)
|
||||||
|
let segment: CustomerSegment | undefined;
|
||||||
|
if (segmentMapping) {
|
||||||
|
const normalizedSkill = skill.toLowerCase();
|
||||||
|
if (
|
||||||
|
segmentMapping.high_value_queues.some((q) =>
|
||||||
|
normalizedSkill.includes(q.toLowerCase())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
segment = 'high';
|
||||||
|
} else if (
|
||||||
|
segmentMapping.low_value_queues.some((q) =>
|
||||||
|
normalizedSkill.includes(q.toLowerCase())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
segment = 'low';
|
||||||
|
} else {
|
||||||
|
segment = 'medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heatmap.push({
|
||||||
|
skill,
|
||||||
|
segment,
|
||||||
|
volume,
|
||||||
|
aht_seconds: aht_mean,
|
||||||
|
metrics: {
|
||||||
|
fcr: Math.round(globalFcrPct),
|
||||||
|
aht: ahtMetric,
|
||||||
|
csat: csatMetric0_100,
|
||||||
|
hold_time: holdMetric,
|
||||||
|
transfer_rate: transferMetric,
|
||||||
|
},
|
||||||
|
annual_cost,
|
||||||
|
variability: {
|
||||||
|
cv_aht: Math.round(cv_aht * 100), // %
|
||||||
|
cv_talk_time: 0,
|
||||||
|
cv_hold_time: 0,
|
||||||
|
transfer_rate,
|
||||||
|
},
|
||||||
|
automation_readiness,
|
||||||
|
dimensions: {
|
||||||
|
predictability: Math.round(predictability_score * 10) / 10,
|
||||||
|
complexity_inverse:
|
||||||
|
Math.round(complexity_inverse_score * 10) / 10,
|
||||||
|
repetitivity: Math.round(repetitivity_score * 10) / 10,
|
||||||
|
},
|
||||||
|
readiness_category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 Heatmap backend generado:', {
|
||||||
|
length: heatmap.length,
|
||||||
|
firstItem: heatmap[0],
|
||||||
|
});
|
||||||
|
|
||||||
|
return heatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeCsatAverage(customerSatisfaction: any): number | undefined {
|
||||||
|
const arr = customerSatisfaction?.csat_avg_by_skill_channel;
|
||||||
|
if (!Array.isArray(arr) || !arr.length) return undefined;
|
||||||
|
|
||||||
|
const values: number[] = arr
|
||||||
|
.map((item: any) =>
|
||||||
|
safeNumber(
|
||||||
|
item?.csat ??
|
||||||
|
item?.value ??
|
||||||
|
item?.score,
|
||||||
|
NaN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((v) => Number.isFinite(v));
|
||||||
|
|
||||||
|
if (!values.length) return undefined;
|
||||||
|
|
||||||
|
const sum = values.reduce((a, b) => a + b, 0);
|
||||||
|
return sum / values.length;
|
||||||
|
}
|
||||||
|
|||||||
8
notas.md
8
notas.md
@@ -9,4 +9,10 @@ python -m uvicorn beyond_api.main:app --reload --port 8000
|
|||||||
|
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
# Siguientes pasos: que revise todo el código y quitar todo lo random para que utilice datos reales
|
||||||
|
# Comparar los sintéticos con la demo y ver que ofrecen los mismos datos. Faltan cosas
|
||||||
|
# Hacer que funcione de alguna manera el selector de JSON
|
||||||
|
# Dockerizar
|
||||||
|
# Limpieza de código
|
||||||
Reference in New Issue
Block a user