feat: Simplificar página de entrada de datos
- Cabecera estilo dashboard (AIR EUROPA + fecha) - Eliminar selección de tier (usar gold por defecto) - Campos manuales vacíos por defecto - Solo opción de subir archivo CSV/Excel - Eliminar tabla de campos, plantilla y datos sintéticos Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
// components/DataInputRedesigned.tsx
|
// components/DataInputRedesigned.tsx
|
||||||
// Interfaz de entrada de datos rediseñada y organizada
|
// Interfaz de entrada de datos simplificada
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Download, CheckCircle, AlertCircle, FileText, Database,
|
AlertCircle, FileText, Database,
|
||||||
UploadCloud, File, Sheet, Loader2, Sparkles, Table,
|
UploadCloud, File, Loader2, Info, X
|
||||||
Info, ExternalLink, X
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -31,9 +30,9 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
onAnalyze,
|
onAnalyze,
|
||||||
isAnalyzing
|
isAnalyzing
|
||||||
}) => {
|
}) => {
|
||||||
// Estados para datos manuales
|
// Estados para datos manuales - valores vacíos por defecto
|
||||||
const [costPerHour, setCostPerHour] = useState<number>(20);
|
const [costPerHour, setCostPerHour] = useState<string>('');
|
||||||
const [avgCsat, setAvgCsat] = useState<number>(85);
|
const [avgCsat, setAvgCsat] = useState<string>('');
|
||||||
|
|
||||||
// Estados para mapeo de segmentación
|
// Estados para mapeo de segmentación
|
||||||
const [highValueQueues, setHighValueQueues] = useState<string>('');
|
const [highValueQueues, setHighValueQueues] = useState<string>('');
|
||||||
@@ -41,41 +40,9 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
const [lowValueQueues, setLowValueQueues] = useState<string>('');
|
const [lowValueQueues, setLowValueQueues] = useState<string>('');
|
||||||
|
|
||||||
// Estados para carga de datos
|
// Estados para carga de datos
|
||||||
const [uploadMethod, setUploadMethod] = useState<'file' | 'url' | 'synthetic' | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [sheetUrl, setSheetUrl] = useState<string>('');
|
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
// Campos CSV requeridos
|
|
||||||
const csvFields = [
|
|
||||||
{ name: 'interaction_id', type: 'String único', example: 'call_8842910', required: true },
|
|
||||||
{ name: 'datetime_start', type: 'DateTime', example: '2024-10-01 09:15:22', required: true },
|
|
||||||
{ name: 'queue_skill', type: 'String', example: 'Soporte_Nivel1, Ventas', required: true },
|
|
||||||
{ name: 'channel', type: 'String', example: 'Voice, Chat, WhatsApp', required: true },
|
|
||||||
{ name: 'duration_talk', type: 'Segundos', example: '345', required: true },
|
|
||||||
{ name: 'hold_time', type: 'Segundos', example: '45', required: true },
|
|
||||||
{ name: 'wrap_up_time', type: 'Segundos', example: '30', required: true },
|
|
||||||
{ name: 'agent_id', type: 'String', example: 'Agente_045', required: true },
|
|
||||||
{ name: 'transfer_flag', type: 'Boolean', example: 'TRUE / FALSE', required: true },
|
|
||||||
{ name: 'caller_id', type: 'String (hash)', example: 'Hash_99283', required: false },
|
|
||||||
{ name: 'csat_score', type: 'Float', example: '4', required: false }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleDownloadTemplate = () => {
|
|
||||||
const headers = csvFields.map(f => f.name).join(',');
|
|
||||||
const exampleRow = csvFields.map(f => f.example).join(',');
|
|
||||||
const csvContent = `${headers}\n${exampleRow}\n`;
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = URL.createObjectURL(blob);
|
|
||||||
link.download = 'plantilla_beyond_diagnostic.csv';
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
toast.success('Plantilla CSV descargada', { icon: '📥' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (selectedFile: File | null) => {
|
const handleFileChange = (selectedFile: File | null) => {
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
const allowedTypes = [
|
const allowedTypes = [
|
||||||
@@ -84,14 +51,13 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
];
|
];
|
||||||
if (allowedTypes.includes(selectedFile.type) ||
|
if (allowedTypes.includes(selectedFile.type) ||
|
||||||
selectedFile.name.endsWith('.csv') ||
|
selectedFile.name.endsWith('.csv') ||
|
||||||
selectedFile.name.endsWith('.xlsx') ||
|
selectedFile.name.endsWith('.xlsx') ||
|
||||||
selectedFile.name.endsWith('.xls')) {
|
selectedFile.name.endsWith('.xls')) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setUploadMethod('file');
|
|
||||||
toast.success(`Archivo "${selectedFile.name}" cargado`, { icon: '📄' });
|
toast.success(`Archivo "${selectedFile.name}" cargado`, { icon: '📄' });
|
||||||
} else {
|
} else {
|
||||||
toast.error('Tipo de archivo no válido. Sube un CSV.', { icon: '❌' });
|
toast.error('Tipo de archivo no válido. Sube un CSV o Excel.', { icon: '❌' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -119,41 +85,40 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGenerateSynthetic = () => {
|
const canAnalyze = file !== null && costPerHour !== '' && parseFloat(costPerHour) > 0;
|
||||||
setIsGenerating(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setUploadMethod('synthetic');
|
|
||||||
setIsGenerating(false);
|
|
||||||
toast.success('Datos sintéticos generados para demo', { icon: '✨' });
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSheetUrlSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (sheetUrl.trim()) {
|
// Preparar segment_mapping
|
||||||
setUploadMethod('url');
|
const segmentMapping = (highValueQueues || mediumValueQueues || lowValueQueues) ? {
|
||||||
toast.success('URL de Google Sheets conectada', { icon: '🔗' });
|
high_value_queues: (highValueQueues || '').split(',').map(q => q.trim()).filter(q => q),
|
||||||
} else {
|
medium_value_queues: (mediumValueQueues || '').split(',').map(q => q.trim()).filter(q => q),
|
||||||
toast.error('Introduce una URL válida', { icon: '❌' });
|
low_value_queues: (lowValueQueues || '').split(',').map(q => q.trim()).filter(q => q)
|
||||||
}
|
} : undefined;
|
||||||
};
|
|
||||||
|
|
||||||
const canAnalyze = uploadMethod !== null && costPerHour > 0;
|
onAnalyze({
|
||||||
|
costPerHour: parseFloat(costPerHour) || 0,
|
||||||
|
avgCsat: parseFloat(avgCsat) || 0,
|
||||||
|
segmentMapping,
|
||||||
|
file: file || undefined,
|
||||||
|
useSynthetic: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
{/* Sección 1: Datos Manuales */}
|
{/* Sección 1: Datos Manuales */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="bg-white rounded-xl shadow-lg p-8 border-2 border-slate-200"
|
className="bg-white rounded-lg shadow-sm p-6 border border-slate-200"
|
||||||
>
|
>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-2xl font-bold text-slate-900 mb-2 flex items-center gap-2">
|
<h2 className="text-lg font-semibold text-slate-800 mb-1 flex items-center gap-2">
|
||||||
<Database size={24} className="text-[#6D84E3]" />
|
<Database size={20} className="text-[#6D84E3]" />
|
||||||
1. Datos Manuales
|
Configuración Manual
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-600 text-sm">
|
<p className="text-slate-500 text-sm">
|
||||||
Introduce los parámetros de configuración para tu análisis
|
Introduce los parámetros de configuración para tu análisis
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,375 +126,184 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* Coste por Hora */}
|
{/* Coste por Hora */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-2 flex items-center gap-2">
|
<label className="block text-sm font-medium text-slate-700 mb-2 flex items-center gap-2">
|
||||||
Coste por Hora Agente (Fully Loaded)
|
Coste por Hora Agente (Fully Loaded)
|
||||||
<span className="inline-flex items-center gap-1 text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-semibold">
|
<span className="inline-flex items-center gap-1 text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-medium">
|
||||||
<AlertCircle size={10} />
|
<AlertCircle size={10} />
|
||||||
Obligatorio
|
Obligatorio
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 font-semibold text-lg">€</span>
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">€</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={costPerHour}
|
value={costPerHour}
|
||||||
onChange={(e) => setCostPerHour(parseFloat(e.target.value) || 0)}
|
onChange={(e) => setCostPerHour(e.target.value)}
|
||||||
min="0"
|
min="0"
|
||||||
step="0.5"
|
step="0.5"
|
||||||
className="w-full pl-10 pr-20 py-3 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition text-lg font-semibold"
|
className="w-full pl-8 pr-16 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
||||||
placeholder="20"
|
placeholder="Ej: 20"
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-slate-500 font-medium">€/hora</span>
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">€/hora</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-1 flex items-start gap-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
<Info size={12} className="mt-0.5 flex-shrink-0" />
|
|
||||||
<span>Tipo: <strong>Número (decimal)</strong> | Ejemplo: <code className="bg-slate-100 px-1 rounded">20</code></span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-600 mt-1">
|
|
||||||
Incluye salario, cargas sociales, infraestructura, etc.
|
Incluye salario, cargas sociales, infraestructura, etc.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CSAT Promedio */}
|
{/* CSAT Promedio */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-2 flex items-center gap-2">
|
<label className="block text-sm font-medium text-slate-700 mb-2 flex items-center gap-2">
|
||||||
CSAT Promedio
|
CSAT Promedio
|
||||||
<span className="inline-flex items-center gap-1 text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-semibold">
|
<span className="text-xs text-slate-400">(Opcional)</span>
|
||||||
<CheckCircle size={10} />
|
|
||||||
Opcional
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={avgCsat}
|
value={avgCsat}
|
||||||
onChange={(e) => setAvgCsat(parseFloat(e.target.value) || 0)}
|
onChange={(e) => setAvgCsat(e.target.value)}
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="1"
|
step="1"
|
||||||
className="w-full pr-16 py-3 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition text-lg font-semibold"
|
className="w-full pr-12 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
||||||
placeholder="85"
|
placeholder="Ej: 85"
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-slate-500 font-medium">/ 100</span>
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">/ 100</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-1 flex items-start gap-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
<Info size={12} className="mt-0.5 flex-shrink-0" />
|
|
||||||
<span>Tipo: <strong>Número (0-100)</strong> | Ejemplo: <code className="bg-slate-100 px-1 rounded">85</code></span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-600 mt-1">
|
|
||||||
Puntuación promedio de satisfacción del cliente
|
Puntuación promedio de satisfacción del cliente
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Segmentación por Cola/Skill */}
|
{/* Segmentación por Cola/Skill */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<div className="mb-4">
|
<div className="mb-3">
|
||||||
<h4 className="font-semibold text-slate-900 mb-2 flex items-center gap-2">
|
<h4 className="font-medium text-slate-700 mb-1 flex items-center gap-2">
|
||||||
<Database size={18} className="text-[#6D84E3]" />
|
|
||||||
Segmentación de Clientes por Cola/Skill
|
Segmentación de Clientes por Cola/Skill
|
||||||
<span className="inline-flex items-center gap-1 text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-semibold">
|
<span className="text-xs text-slate-400">(Opcional)</span>
|
||||||
<CheckCircle size={10} />
|
|
||||||
Opcional
|
|
||||||
</span>
|
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-500">
|
||||||
Identifica qué colas/skills corresponden a cada segmento de cliente. Separa múltiples colas con comas.
|
Identifica qué colas corresponden a cada segmento. Separa múltiples colas con comas.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{/* High Value */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
🟢 Clientes Alto Valor (High)
|
Alto Valor
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={highValueQueues}
|
value={highValueQueues}
|
||||||
onChange={(e) => setHighValueQueues(e.target.value)}
|
onChange={(e) => setHighValueQueues(e.target.value)}
|
||||||
placeholder="VIP, Premium, Enterprise"
|
placeholder="VIP, Premium, Enterprise"
|
||||||
className="w-full px-4 py-3 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition text-sm"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Colas para clientes de alto valor
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Medium Value */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
🟡 Clientes Valor Medio (Medium)
|
Valor Medio
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={mediumValueQueues}
|
value={mediumValueQueues}
|
||||||
onChange={(e) => setMediumValueQueues(e.target.value)}
|
onChange={(e) => setMediumValueQueues(e.target.value)}
|
||||||
placeholder="Soporte_General, Ventas"
|
placeholder="Soporte_General, Ventas"
|
||||||
className="w-full px-4 py-3 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition text-sm"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Colas para clientes estándar
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Low Value */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
🔴 Clientes Bajo Valor (Low)
|
Bajo Valor
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={lowValueQueues}
|
value={lowValueQueues}
|
||||||
onChange={(e) => setLowValueQueues(e.target.value)}
|
onChange={(e) => setLowValueQueues(e.target.value)}
|
||||||
placeholder="Basico, Trial, Freemium"
|
placeholder="Basico, Trial, Freemium"
|
||||||
className="w-full px-4 py-3 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition text-sm"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Colas para clientes de bajo valor
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
<p className="text-xs text-slate-500 mt-2 flex items-start gap-1">
|
||||||
<p className="text-xs text-blue-800 flex items-start gap-2">
|
<Info size={12} className="mt-0.5 flex-shrink-0" />
|
||||||
<Info size={14} className="mt-0.5 flex-shrink-0" />
|
Las colas no mapeadas se clasificarán como "Valor Medio" por defecto.
|
||||||
<span>
|
</p>
|
||||||
<strong>Nota:</strong> Las colas no mapeadas se clasificarán automáticamente como "Medium".
|
|
||||||
El matching es flexible (no distingue mayúsculas y permite coincidencias parciales).
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Sección 2: Datos CSV */}
|
{/* Sección 2: Subir Archivo */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="bg-white rounded-xl shadow-lg p-8 border-2 border-slate-200"
|
className="bg-white rounded-lg shadow-sm p-6 border border-slate-200"
|
||||||
>
|
>
|
||||||
<div className="mb-6">
|
<div className="mb-4">
|
||||||
<h2 className="text-2xl font-bold text-slate-900 mb-2 flex items-center gap-2">
|
<h2 className="text-lg font-semibold text-slate-800 mb-1 flex items-center gap-2">
|
||||||
<Table size={24} className="text-[#6D84E3]" />
|
<UploadCloud size={20} className="text-[#6D84E3]" />
|
||||||
2. Datos CSV (Raw Data de ACD)
|
Datos CSV
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-600 text-sm">
|
<p className="text-slate-500 text-sm">
|
||||||
Exporta estos campos desde tu sistema ACD/CTI (Genesys, Avaya, Talkdesk, Zendesk, etc.)
|
Sube el archivo exportado desde tu sistema ACD/CTI
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabla de campos requeridos */}
|
{/* Zona de subida */}
|
||||||
<div className="mb-6 overflow-x-auto">
|
<div
|
||||||
<table className="w-full text-sm border-collapse">
|
onDragOver={onDragOver}
|
||||||
<thead className="bg-slate-50">
|
onDragLeave={onDragLeave}
|
||||||
<tr>
|
onDrop={onDrop}
|
||||||
<th className="p-3 text-left font-semibold text-slate-700 border-b-2 border-slate-300">Campo</th>
|
className={clsx(
|
||||||
<th className="p-3 text-left font-semibold text-slate-700 border-b-2 border-slate-300">Tipo</th>
|
'border-2 border-dashed rounded-lg p-8 text-center transition-all cursor-pointer',
|
||||||
<th className="p-3 text-left font-semibold text-slate-700 border-b-2 border-slate-300">Ejemplo</th>
|
isDragging ? 'border-[#6D84E3] bg-blue-50' : 'border-slate-300 bg-slate-50 hover:border-slate-400'
|
||||||
<th className="p-3 text-center font-semibold text-slate-700 border-b-2 border-slate-300">Obligatorio</th>
|
)}
|
||||||
</tr>
|
>
|
||||||
</thead>
|
{file ? (
|
||||||
<tbody>
|
<div className="flex items-center justify-center gap-3">
|
||||||
{csvFields.map((field, index) => (
|
<File size={24} className="text-emerald-600" />
|
||||||
<tr key={field.name} className={clsx(
|
<div className="text-left">
|
||||||
'border-b border-slate-200',
|
<p className="font-medium text-slate-800">{file.name}</p>
|
||||||
index % 2 === 0 ? 'bg-white' : 'bg-slate-50'
|
<p className="text-xs text-slate-500">{(file.size / 1024).toFixed(1)} KB</p>
|
||||||
)}>
|
|
||||||
<td className="p-3 font-mono text-sm font-semibold text-slate-900">{field.name}</td>
|
|
||||||
<td className="p-3 text-slate-700">{field.type}</td>
|
|
||||||
<td className="p-3 font-mono text-xs text-slate-600">{field.example}</td>
|
|
||||||
<td className="p-3 text-center">
|
|
||||||
{field.required ? (
|
|
||||||
<span className="inline-flex items-center gap-1 text-xs bg-red-100 text-red-700 px-2 py-1 rounded-full font-semibold">
|
|
||||||
<AlertCircle size={10} />
|
|
||||||
Sí
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1 text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full font-semibold">
|
|
||||||
<CheckCircle size={10} />
|
|
||||||
No
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Botón de descarga de plantilla */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[#6D84E3] text-white rounded-lg hover:bg-[#5a6fc9] transition font-semibold"
|
|
||||||
>
|
|
||||||
<Download size={18} />
|
|
||||||
Descargar Plantilla CSV
|
|
||||||
</button>
|
|
||||||
<p className="text-xs text-slate-500 mt-2">
|
|
||||||
Descarga una plantilla con la estructura exacta de campos requeridos
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Opciones de carga */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="font-semibold text-slate-900 mb-3">Elige cómo proporcionar tus datos:</h3>
|
|
||||||
|
|
||||||
{/* Opción 1: Subir archivo */}
|
|
||||||
<div className={clsx(
|
|
||||||
'border-2 rounded-lg p-4 transition-all',
|
|
||||||
uploadMethod === 'file' ? 'border-[#6D84E3] bg-blue-50' : 'border-slate-300'
|
|
||||||
)}>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="uploadMethod"
|
|
||||||
checked={uploadMethod === 'file'}
|
|
||||||
onChange={() => setUploadMethod('file')}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-semibold text-slate-900 mb-2 flex items-center gap-2">
|
|
||||||
<UploadCloud size={18} className="text-[#6D84E3]" />
|
|
||||||
Subir Archivo CSV
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{uploadMethod === 'file' && (
|
|
||||||
<div
|
|
||||||
onDragOver={onDragOver}
|
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
onDrop={onDrop}
|
|
||||||
className={clsx(
|
|
||||||
'border-2 border-dashed rounded-lg p-6 text-center transition-all',
|
|
||||||
isDragging ? 'border-[#6D84E3] bg-blue-100' : 'border-slate-300 bg-slate-50'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{file ? (
|
|
||||||
<div className="flex items-center justify-center gap-3">
|
|
||||||
<File size={24} className="text-green-600" />
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="font-semibold text-slate-900">{file.name}</p>
|
|
||||||
<p className="text-xs text-slate-500">{(file.size / 1024).toFixed(1)} KB</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setFile(null)}
|
|
||||||
className="ml-auto p-1 hover:bg-slate-200 rounded"
|
|
||||||
>
|
|
||||||
<X size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<UploadCloud size={32} className="mx-auto text-slate-400 mb-2" />
|
|
||||||
<p className="text-sm text-slate-600 mb-2">
|
|
||||||
Arrastra tu archivo aquí o haz click para seleccionar
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".csv,.xlsx,.xls"
|
|
||||||
onChange={(e) => handleFileChange(e.target.files?.[0] || null)}
|
|
||||||
className="hidden"
|
|
||||||
id="file-upload"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="file-upload"
|
|
||||||
className="inline-block px-4 py-2 bg-[#6D84E3] text-white rounded-lg hover:bg-[#5a6fc9] transition cursor-pointer"
|
|
||||||
>
|
|
||||||
Seleccionar Archivo
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
className="ml-4 p-1.5 hover:bg-slate-200 rounded-full transition"
|
||||||
|
>
|
||||||
|
<X size={18} className="text-slate-500" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<>
|
||||||
|
<UploadCloud size={40} className="mx-auto text-slate-400 mb-3" />
|
||||||
{/* Opción 2: URL Google Sheets
|
<p className="text-slate-600 mb-2">
|
||||||
<div className={clsx(
|
Arrastra tu archivo aquí o haz click para seleccionar
|
||||||
'border-2 rounded-lg p-4 transition-all',
|
</p>
|
||||||
uploadMethod === 'url' ? 'border-[#6D84E3] bg-blue-50' : 'border-slate-300'
|
<p className="text-xs text-slate-400 mb-4">
|
||||||
)}>
|
Formatos aceptados: CSV, Excel (.xlsx, .xls)
|
||||||
<div className="flex items-start gap-3">
|
</p>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="file"
|
||||||
name="uploadMethod"
|
accept=".csv,.xlsx,.xls"
|
||||||
checked={uploadMethod === 'url'}
|
onChange={(e) => handleFileChange(e.target.files?.[0] || null)}
|
||||||
onChange={() => setUploadMethod('url')}
|
className="hidden"
|
||||||
className="mt-1"
|
id="file-upload"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<label
|
||||||
<h4 className="font-semibold text-slate-900 mb-2 flex items-center gap-2">
|
htmlFor="file-upload"
|
||||||
<Sheet size={18} className="text-[#6D84E3]" />
|
className="inline-block px-4 py-2 bg-[#6D84E3] text-white rounded-lg hover:bg-[#5a6fc9] transition cursor-pointer font-medium"
|
||||||
Conectar Google Sheets
|
>
|
||||||
</h4>
|
Seleccionar Archivo
|
||||||
|
</label>
|
||||||
{uploadMethod === 'url' && (
|
</>
|
||||||
<div className="flex gap-2">
|
)}
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={sheetUrl}
|
|
||||||
onChange={(e) => setSheetUrl(e.target.value)}
|
|
||||||
placeholder="https://docs.google.com/spreadsheets/d/..."
|
|
||||||
className="flex-1 px-4 py-2 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-[#6D84E3] focus:border-[#6D84E3] transition"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSheetUrlSubmit}
|
|
||||||
className="px-4 py-2 bg-[#6D84E3] text-white rounded-lg hover:bg-[#5a6fc9] transition font-semibold"
|
|
||||||
>
|
|
||||||
<ExternalLink size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
{/* Opción 3: Datos sintéticos */}
|
|
||||||
<div className={clsx(
|
|
||||||
'border-2 rounded-lg p-4 transition-all',
|
|
||||||
uploadMethod === 'synthetic' ? 'border-[#6D84E3] bg-blue-50' : 'border-slate-300'
|
|
||||||
)}>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="uploadMethod"
|
|
||||||
checked={uploadMethod === 'synthetic'}
|
|
||||||
onChange={() => setUploadMethod('synthetic')}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-semibold text-slate-900 mb-2 flex items-center gap-2">
|
|
||||||
<Sparkles size={18} className="text-[#6D84E3]" />
|
|
||||||
Generar Datos Sintéticos (Demo)
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{uploadMethod === 'synthetic' && (
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateSynthetic}
|
|
||||||
disabled={isGenerating}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg hover:from-purple-700 hover:to-pink-700 transition font-semibold disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isGenerating ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
Generando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Sparkles size={18} />
|
|
||||||
Generar Datos de Prueba
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -541,40 +315,23 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
|||||||
className="flex justify-center"
|
className="flex justify-center"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleSubmit}
|
||||||
// Preparar segment_mapping
|
|
||||||
const segmentMapping = (highValueQueues || mediumValueQueues || lowValueQueues) ? {
|
|
||||||
high_value_queues: (highValueQueues || '').split(',').map(q => q.trim()).filter(q => q),
|
|
||||||
medium_value_queues: (mediumValueQueues || '').split(',').map(q => q.trim()).filter(q => q),
|
|
||||||
low_value_queues: (lowValueQueues || '').split(',').map(q => q.trim()).filter(q => q)
|
|
||||||
} : undefined;
|
|
||||||
|
|
||||||
// Llamar a onAnalyze con todos los datos
|
|
||||||
onAnalyze({
|
|
||||||
costPerHour,
|
|
||||||
avgCsat,
|
|
||||||
segmentMapping,
|
|
||||||
file: uploadMethod === 'file' ? file || undefined : undefined,
|
|
||||||
sheetUrl: uploadMethod === 'url' ? sheetUrl : undefined,
|
|
||||||
useSynthetic: uploadMethod === 'synthetic'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={!canAnalyze || isAnalyzing}
|
disabled={!canAnalyze || isAnalyzing}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-8 py-4 rounded-xl font-bold text-lg transition-all flex items-center gap-3',
|
'px-8 py-3 rounded-lg font-semibold text-lg transition-all flex items-center gap-3',
|
||||||
canAnalyze && !isAnalyzing
|
canAnalyze && !isAnalyzing
|
||||||
? 'bg-gradient-to-r from-[#6D84E3] to-[#5a6fc9] text-white hover:scale-105 shadow-lg'
|
? 'bg-[#6D84E3] text-white hover:bg-[#5a6fc9] shadow-lg hover:shadow-xl'
|
||||||
: 'bg-slate-300 text-slate-500 cursor-not-allowed'
|
: 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isAnalyzing ? (
|
{isAnalyzing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 size={24} className="animate-spin" />
|
<Loader2 size={22} className="animate-spin" />
|
||||||
Analizando...
|
Analizando...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FileText size={24} />
|
<FileText size={22} />
|
||||||
Generar Análisis
|
Generar Análisis
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
// components/SinglePageDataRequestIntegrated.tsx
|
// components/SinglePageDataRequestIntegrated.tsx
|
||||||
// Versión integrada con DataInputRedesigned + Dashboard actual
|
// Versión simplificada con cabecera estilo dashboard
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { TierKey, AnalysisData } from '../types';
|
import { TierKey, AnalysisData } from '../types';
|
||||||
import TierSelectorEnhanced from './TierSelectorEnhanced';
|
|
||||||
import DataInputRedesigned from './DataInputRedesigned';
|
import DataInputRedesigned from './DataInputRedesigned';
|
||||||
import DashboardTabs from './DashboardTabs';
|
import DashboardTabs from './DashboardTabs';
|
||||||
import { generateAnalysis } from '../utils/analysisGenerator';
|
import { generateAnalysis } from '../utils/analysisGenerator';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useAuth } from '../utils/AuthContext';
|
import { useAuth } from '../utils/AuthContext';
|
||||||
|
|
||||||
|
// Función para formatear fecha como en el dashboard
|
||||||
|
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()}`;
|
||||||
|
};
|
||||||
|
|
||||||
const SinglePageDataRequestIntegrated: React.FC = () => {
|
const SinglePageDataRequestIntegrated: React.FC = () => {
|
||||||
const [selectedTier, setSelectedTier] = useState<TierKey>('silver');
|
|
||||||
const [view, setView] = useState<'form' | 'dashboard'>('form');
|
const [view, setView] = useState<'form' | 'dashboard'>('form');
|
||||||
const [analysisData, setAnalysisData] = useState<AnalysisData | null>(null);
|
const [analysisData, setAnalysisData] = useState<AnalysisData | null>(null);
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||||
|
|
||||||
const handleTierSelect = (tier: TierKey) => {
|
|
||||||
setSelectedTier(tier);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { authHeader, logout } = useAuth();
|
const { authHeader, logout } = useAuth();
|
||||||
|
|
||||||
|
|
||||||
const handleAnalyze = (config: {
|
const handleAnalyze = (config: {
|
||||||
costPerHour: number;
|
costPerHour: number;
|
||||||
avgCsat: number;
|
avgCsat: number;
|
||||||
@@ -37,21 +39,21 @@ const SinglePageDataRequestIntegrated: React.FC = () => {
|
|||||||
sheetUrl?: string;
|
sheetUrl?: string;
|
||||||
useSynthetic?: boolean;
|
useSynthetic?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
console.log('🚀 handleAnalyze called with config:', config);
|
// Validar que hay archivo
|
||||||
console.log('🎯 Selected tier:', selectedTier);
|
if (!config.file) {
|
||||||
console.log('📄 File:', config.file);
|
toast.error('Por favor, sube un archivo CSV o Excel.');
|
||||||
console.log('🔗 Sheet URL:', config.sheetUrl);
|
|
||||||
console.log('✨ Use Synthetic:', config.useSynthetic);
|
|
||||||
|
|
||||||
// Validar que hay datos
|
|
||||||
if (!config.file && !config.sheetUrl && !config.useSynthetic) {
|
|
||||||
toast.error('Por favor, sube un archivo, introduce una URL o genera datos sintéticos.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔐 Si usamos CSV real, exigir estar logado
|
// Validar coste por hora
|
||||||
if (config.file && !config.useSynthetic && !authHeader) {
|
if (!config.costPerHour || config.costPerHour <= 0) {
|
||||||
toast.error('Debes iniciar sesión para analizar datos reales.');
|
toast.error('Por favor, introduce el coste por hora del agente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exigir estar logado para analizar
|
||||||
|
if (!authHeader) {
|
||||||
|
toast.error('Debes iniciar sesión para analizar datos.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,19 +61,18 @@ const SinglePageDataRequestIntegrated: React.FC = () => {
|
|||||||
toast.loading('Generando análisis...', { id: 'analyzing' });
|
toast.loading('Generando análisis...', { id: 'analyzing' });
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('⏰ Generating analysis...');
|
|
||||||
try {
|
try {
|
||||||
|
// Usar tier 'gold' por defecto
|
||||||
const data = await generateAnalysis(
|
const data = await generateAnalysis(
|
||||||
selectedTier,
|
'gold' as TierKey,
|
||||||
config.costPerHour,
|
config.costPerHour,
|
||||||
config.avgCsat,
|
config.avgCsat || 0,
|
||||||
config.segmentMapping,
|
config.segmentMapping,
|
||||||
config.file,
|
config.file,
|
||||||
config.sheetUrl,
|
config.sheetUrl,
|
||||||
config.useSynthetic,
|
false, // No usar sintético
|
||||||
authHeader || undefined
|
authHeader || undefined
|
||||||
);
|
);
|
||||||
console.log('✅ Analysis generated successfully');
|
|
||||||
|
|
||||||
setAnalysisData(data);
|
setAnalysisData(data);
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
@@ -79,10 +80,9 @@ const SinglePageDataRequestIntegrated: React.FC = () => {
|
|||||||
toast.success('¡Análisis completado!', { icon: '🎉' });
|
toast.success('¡Análisis completado!', { icon: '🎉' });
|
||||||
setView('dashboard');
|
setView('dashboard');
|
||||||
|
|
||||||
// Scroll to top
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error generating analysis:', error);
|
console.error('Error generating analysis:', error);
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
toast.dismiss('analyzing');
|
toast.dismiss('analyzing');
|
||||||
|
|
||||||
@@ -106,14 +106,10 @@ const SinglePageDataRequestIntegrated: React.FC = () => {
|
|||||||
|
|
||||||
// Dashboard view
|
// Dashboard view
|
||||||
if (view === 'dashboard' && analysisData) {
|
if (view === 'dashboard' && analysisData) {
|
||||||
console.log('📊 Rendering dashboard with data:', analysisData);
|
|
||||||
console.log('📊 Heatmap data length:', analysisData.heatmapData?.length);
|
|
||||||
console.log('📊 Dimensions length:', analysisData.dimensions?.length);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return <DashboardTabs data={analysisData} onBack={handleBackToForm} />;
|
return <DashboardTabs data={analysisData} onBack={handleBackToForm} />;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error rendering dashboard:', error);
|
console.error('Error rendering dashboard:', error);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-red-50 p-8">
|
<div className="min-h-screen bg-red-50 p-8">
|
||||||
<div className="max-w-2xl mx-auto bg-white rounded-xl shadow-lg p-6">
|
<div className="max-w-2xl mx-auto bg-white rounded-xl shadow-lg p-6">
|
||||||
@@ -136,56 +132,34 @@ const SinglePageDataRequestIntegrated: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
|
|
||||||
<div className="w-full min-h-screen bg-gradient-to-br from-slate-50 via-[#E8EBFA] to-slate-100 font-sans">
|
<div className="min-h-screen bg-slate-50">
|
||||||
<div className="w-full max-w-7xl mx-auto p-6 space-y-8">
|
{/* Header estilo dashboard */}
|
||||||
{/* Header */}
|
<header className="sticky top-0 z-50 bg-white border-b border-slate-200 shadow-sm">
|
||||||
<motion.div
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<div className="flex items-center justify-between">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<h1 className="text-xl font-bold text-slate-800">
|
||||||
className="text-center"
|
AIR EUROPA - Beyond CX Analytics
|
||||||
>
|
</h1>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-3">
|
<div className="flex items-center gap-4">
|
||||||
Beyond Diagnostic
|
<span className="text-sm text-slate-500">{formatDate()}</span>
|
||||||
</h1>
|
<button
|
||||||
<p className="text-lg text-slate-600">
|
onClick={logout}
|
||||||
Análisis de Readiness Agéntico para Contact Centers
|
className="text-xs text-slate-500 hover:text-slate-800 underline"
|
||||||
</p>
|
>
|
||||||
<button
|
Cerrar sesión
|
||||||
onClick={logout}
|
</button>
|
||||||
className="text-xs text-slate-500 hover:text-slate-800 underline mt-1"
|
</div>
|
||||||
>
|
|
||||||
Cerrar sesión
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Tier Selection */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="bg-white rounded-xl shadow-lg p-8"
|
|
||||||
>
|
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-3xl font-bold text-slate-900 mb-2">
|
|
||||||
Selecciona tu Tier de Análisis
|
|
||||||
</h2>
|
|
||||||
<p className="text-slate-600">
|
|
||||||
Elige el nivel de profundidad que necesitas para tu diagnóstico
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<TierSelectorEnhanced
|
{/* Contenido principal */}
|
||||||
selectedTier={selectedTier}
|
<main className="max-w-7xl mx-auto px-6 py-6">
|
||||||
onSelectTier={handleTierSelect}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Data Input - Using redesigned component */}
|
|
||||||
<DataInputRedesigned
|
<DataInputRedesigned
|
||||||
onAnalyze={handleAnalyze}
|
onAnalyze={handleAnalyze}
|
||||||
isAnalyzing={isAnalyzing}
|
isAnalyzing={isAnalyzing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user