880 lines
52 KiB
PHP
880 lines
52 KiB
PHP
<?php
|
|
session_start();
|
|
require_once 'db/config.php';
|
|
require_once 'includes/callcenter_test_helpers.php';
|
|
require_once 'includes/drive_test_orders.php';
|
|
|
|
$pageTitle = 'Call Center de Prueba | Bandejas de gestión';
|
|
$pageDescription = 'Bandejas diarias de Call Center con estados reales, próxima llamada, fecha de entrega, historial y edición de datos del pedido importado desde Drive.';
|
|
|
|
function cc_test_parse_datetime(?string $value): ?DateTimeImmutable
|
|
{
|
|
$value = trim((string) $value);
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return new DateTimeImmutable($value);
|
|
} catch (Throwable $exception) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function cc_test_format_datetime(?string $value, string $fallback = 'Sin programar'): string
|
|
{
|
|
$date = cc_test_parse_datetime($value);
|
|
return $date ? $date->format('d/m/Y h:i A') : $fallback;
|
|
}
|
|
|
|
function cc_test_format_datetime_input(?string $value): string
|
|
{
|
|
$date = cc_test_parse_datetime($value);
|
|
return $date ? $date->format('Y-m-d\TH:i') : '';
|
|
}
|
|
|
|
function cc_test_format_date(?string $value, string $fallback = 'Sin fecha'): string
|
|
{
|
|
$date = cc_test_parse_datetime($value);
|
|
return $date ? $date->format('d/m/Y') : $fallback;
|
|
}
|
|
|
|
function cc_test_format_date_input(?string $value): string
|
|
{
|
|
$date = cc_test_parse_datetime($value);
|
|
return $date ? $date->format('Y-m-d') : '';
|
|
}
|
|
|
|
function cc_test_format_price(?string $value): string
|
|
{
|
|
$value = trim((string) $value);
|
|
return $value !== '' ? 'S/ ' . $value : 'No registrado';
|
|
}
|
|
|
|
function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string
|
|
{
|
|
$value = trim((string) $value);
|
|
return $value !== '' ? $value : $fallback;
|
|
}
|
|
|
|
function cc_test_order_label(array $order): string
|
|
{
|
|
$codigo = trim((string) ($order['codigo'] ?? ''));
|
|
if ($codigo !== '') {
|
|
return '#' . ltrim($codigo, '#');
|
|
}
|
|
|
|
return 'Sin número';
|
|
}
|
|
|
|
function cc_test_badge_class(string $estado): string
|
|
{
|
|
return match ($estado) {
|
|
'CONFIRMADO CONTRAENTREGA', 'CONFIRMADO CONTRAENTREGA FECHA', 'CONFIRMADO ENVIO', 'CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO' => 'bg-success-subtle text-success-emphasis',
|
|
'DEVOLVER LLAMADA' => 'bg-info-subtle text-info-emphasis',
|
|
'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis',
|
|
'CANCELADO' => 'bg-danger-subtle text-danger-emphasis',
|
|
'REPETIDO', 'ENVIO REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis',
|
|
'SE ENVIO NUMERO DE CUENTA' => 'bg-primary-subtle text-primary-emphasis',
|
|
default => 'bg-dark-subtle text-dark-emphasis',
|
|
};
|
|
}
|
|
|
|
function cc_test_order_time(array $order): int
|
|
{
|
|
foreach (['proxima_llamada_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
|
|
$date = cc_test_parse_datetime($order[$field] ?? null);
|
|
if ($date) {
|
|
return $date->getTimestamp();
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
$view = $_GET['view'] ?? 'pendientes_hoy';
|
|
$allowedViews = [
|
|
'pendientes_hoy' => 'Pendientes de hoy',
|
|
'nuevos_hoy' => 'Nuevos de hoy',
|
|
'confirmados' => 'Confirmados',
|
|
'observados' => 'Observados',
|
|
'cerrados' => 'Cerrados / descartados',
|
|
'todos' => 'Todos los pedidos cargados',
|
|
];
|
|
if (!isset($allowedViews[$view])) {
|
|
$view = 'pendientes_hoy';
|
|
}
|
|
|
|
$errorMessage = null;
|
|
$orders = [];
|
|
$visibleOrders = [];
|
|
$modalsHtml = [];
|
|
$totalRows = 0;
|
|
$loadLimit = 10;
|
|
$stats = [
|
|
'total' => 0,
|
|
'pendientes_hoy' => 0,
|
|
'nuevos_hoy' => 0,
|
|
'confirmados' => 0,
|
|
'observados' => 0,
|
|
'cerrados' => 0,
|
|
];
|
|
|
|
try {
|
|
$pdo = db();
|
|
cc_test_ensure_tracking_table($pdo);
|
|
cc_test_ensure_historial_llamadas_table($pdo);
|
|
|
|
$preview = drive_test_fetch_orders($loadLimit);
|
|
$totalRows = (int) ($preview['total_rows'] ?? 0);
|
|
$orders = $preview['orders'] ?? [];
|
|
|
|
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
|
|
$orders = drive_test_merge_tracking($orders, $tracking);
|
|
|
|
$sourceKeys = array_column($orders, 'source_key');
|
|
$callCounts = [];
|
|
if (!empty($sourceKeys)) {
|
|
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
|
$stmtCalls = $pdo->prepare("SELECT pedido_id, COUNT(*) as total FROM historial_llamadas WHERE pedido_id IN ($placeholders) GROUP BY pedido_id");
|
|
$stmtCalls->execute($sourceKeys);
|
|
$callCounts = $stmtCalls->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
}
|
|
|
|
$todayStart = new DateTimeImmutable('today');
|
|
$todayEnd = $todayStart->setTime(23, 59, 59);
|
|
$openStates = cc_test_open_states();
|
|
$closedStates = cc_test_closed_states();
|
|
|
|
foreach ($orders as &$order) {
|
|
$order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0);
|
|
$importDate = cc_test_parse_datetime($order['import_id'] ?? null);
|
|
$proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null);
|
|
|
|
$order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
|
|
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true)
|
|
&& ($proximaDate === null || $proximaDate <= $todayEnd);
|
|
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
|
|
|
|
$stats['total']++;
|
|
if ($order['es_nuevo_hoy']) {
|
|
$stats['nuevos_hoy']++;
|
|
}
|
|
if ($order['es_pendiente_hoy']) {
|
|
$stats['pendientes_hoy']++;
|
|
}
|
|
if (in_array($order['estado'], cc_test_confirmed_states(), true)) {
|
|
$stats['confirmados']++;
|
|
}
|
|
if ($order['estado'] === 'OBSERVADO') {
|
|
$stats['observados']++;
|
|
}
|
|
if ($order['es_cerrado']) {
|
|
$stats['cerrados']++;
|
|
}
|
|
}
|
|
unset($order);
|
|
|
|
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool {
|
|
return match ($view) {
|
|
'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false),
|
|
'nuevos_hoy' => (bool) ($order['es_nuevo_hoy'] ?? false),
|
|
'confirmados' => in_array(($order['estado'] ?? ''), cc_test_confirmed_states(), true),
|
|
'observados' => ($order['estado'] ?? '') === 'OBSERVADO',
|
|
'cerrados' => (bool) ($order['es_cerrado'] ?? false),
|
|
default => true,
|
|
};
|
|
}));
|
|
|
|
usort($visibleOrders, static function (array $a, array $b) use ($view): int {
|
|
if ($view === 'pendientes_hoy') {
|
|
$aDue = cc_test_parse_datetime($a['proxima_llamada_at'] ?? null);
|
|
$bDue = cc_test_parse_datetime($b['proxima_llamada_at'] ?? null);
|
|
if ($aDue && $bDue && $aDue != $bDue) {
|
|
return $aDue <=> $bDue;
|
|
}
|
|
if ($aDue && !$bDue) {
|
|
return 1;
|
|
}
|
|
if (!$aDue && $bDue) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return cc_test_order_time($b) <=> cc_test_order_time($a);
|
|
});
|
|
} catch (Throwable $exception) {
|
|
$errorMessage = $exception->getMessage();
|
|
}
|
|
|
|
require_once 'layout_header.php';
|
|
?>
|
|
|
|
<main class="container-fluid py-4">
|
|
<section class="mb-4">
|
|
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
|
|
<div>
|
|
<h1 class="h2 fw-bold mb-1"><i class="bi bi-headset text-primary"></i> Call Center de prueba</h1>
|
|
<p class="text-muted mb-0">Ahora el panel trabaja por <strong>bandejas</strong>: <strong>Nuevos de hoy</strong>, <strong>Pendientes de hoy</strong>, <strong>Confirmados</strong>, <strong>Observados</strong> y <strong>Cerrados</strong>. Así no se mezcla todo cuando entran pedidos diarios.</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
|
|
<span class="badge rounded-pill text-bg-light border px-3 py-2">Carga operativa: últimos <?php echo (int) $loadLimit; ?> registros</span>
|
|
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if ($errorMessage !== null): ?>
|
|
<section class="alert alert-danger" role="alert">
|
|
<strong>No se pudo cargar el panel.</strong><br>
|
|
<?php echo htmlspecialchars($errorMessage); ?>
|
|
</section>
|
|
<?php else: ?>
|
|
<section class="alert alert-primary border-0 shadow-sm mb-4">
|
|
<div class="row g-3 align-items-center">
|
|
<div class="col-lg-8">
|
|
<h2 class="h5 fw-bold mb-2">Flujo recomendado ya listo para prueba</h2>
|
|
<p class="mb-0">Los pedidos nuevos entran desde Drive. Luego tú gestionas cada cliente con estado, próxima llamada y observaciones internas. Los casos abiertos siguen apareciendo en <strong>Pendientes de hoy</strong> o <strong>Observados</strong> hasta que los cierres o confirmes.</p>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="small text-muted">Campos editables del módulo</div>
|
|
<div class="fw-semibold">Dirección, referencia, sede / ID, ciudad, distrito, DNI y observaciones</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="row g-3 mb-4">
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=pendientes_hoy" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['pendientes_hoy']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=nuevos_hoy" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'nuevos_hoy' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase text-muted mb-2">Nuevos de hoy</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['nuevos_hoy']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=confirmados" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'confirmados' ? 'bg-success-subtle border border-success' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase text-muted mb-2">Confirmados</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['confirmados']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=observados" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'observados' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase text-muted mb-2">Observados</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['observados']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=cerrados" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'cerrados' ? 'bg-secondary-subtle border border-secondary' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase text-muted mb-2">Cerrados</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['cerrados']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6 col-xl-2">
|
|
<a href="?view=todos" class="text-decoration-none">
|
|
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-light border' : 'bg-white'; ?>">
|
|
<div class="card-body">
|
|
<div class="small text-uppercase text-muted mb-2">Todos</div>
|
|
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['total']; ?></div>
|
|
</div>
|
|
</article>
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
|
|
<div>
|
|
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
|
|
<p class="text-muted small mb-1">Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega 📅, Confirmado envío, Cancelado y Repetido.</p>
|
|
<p class="small text-primary mb-0"><i class="bi bi-phone"></i> El botón <strong>Llamar / AirDroid</strong> registra el intento, copia el número y muestra la ayuda visual para pegar en tu app de AirDroid.</p>
|
|
</div>
|
|
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>N° Pedido</th>
|
|
<th>Cliente</th>
|
|
<th>Ubicación editable</th>
|
|
<th>Pedido</th>
|
|
<th>Gestión</th>
|
|
<th class="text-center">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($visibleOrders)): ?>
|
|
<tr>
|
|
<td colspan="6" class="text-center py-5 text-muted">
|
|
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
|
No hay pedidos en esta bandeja por ahora.
|
|
</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($visibleOrders as $order): ?>
|
|
<?php
|
|
$modalId = 'modalDriveTest' . $order['source_key'];
|
|
$badgeClass = cc_test_badge_class($order['estado']);
|
|
$historial = cc_test_fetch_historial(db(), $order['source_key']);
|
|
ob_start();
|
|
?>
|
|
<tr data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
|
|
<td>
|
|
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
|
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Sin nombre')); ?></div>
|
|
<div class="small text-muted mb-1">Celular: <?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin número')); ?></div>
|
|
<div class="small text-muted">DNI: <?php echo htmlspecialchars(cc_test_display_value($order['dni'], 'Sin DNI')); ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="small"><strong>Dirección:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion'])); ?></div>
|
|
<div class="small"><strong>Referencia:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia'])); ?></div>
|
|
<div class="small"><strong>Ciudad / Distrito:</strong> <?php echo htmlspecialchars(cc_test_display_value(trim(($order['ciudad'] ?? '') . ' / ' . ($order['distrito'] ?? '')), 'No registrado')); ?></div>
|
|
<div class="small"><strong>Sede / ID:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['sede'])); ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?></div>
|
|
<div class="small text-muted">Cantidad: <?php echo htmlspecialchars(cc_test_display_value($order['cantidad'])); ?> · <?php echo htmlspecialchars(cc_test_format_price($order['precio'])); ?></div>
|
|
<div class="small text-muted">Ingreso Drive: <?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha en Drive')); ?></div>
|
|
<div class="small text-muted">Observación Drive: <?php echo htmlspecialchars(cc_test_display_value($order['observaciones'], 'Sin observaciones')); ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<span class="badge rounded-pill <?php echo htmlspecialchars($badgeClass); ?>"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></span>
|
|
<span class="badge rounded-pill bg-light text-dark border"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span> llamadas</span>
|
|
</div>
|
|
<div class="small text-muted">Próxima llamada: <?php echo htmlspecialchars(cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
|
|
<?php if (!empty($order['fecha_entrega_programada'])): ?>
|
|
<div class="small text-muted">Entrega programada 📅: <?php echo htmlspecialchars(cc_test_format_date($order['fecha_entrega_programada'] ?? null)); ?></div>
|
|
<?php endif; ?>
|
|
<div class="small text-muted">Última gestión: <?php echo htmlspecialchars(cc_test_format_datetime($order['ultima_gestion_at'] ?? ($order['seguimiento_actualizado'] ?? null), 'Aún no gestionado')); ?></div>
|
|
<div class="small text-muted mt-1">Nota: <?php echo htmlspecialchars(cc_test_display_value($order['nota_seguimiento'], 'Sin nota interna')); ?></div>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="d-flex flex-column gap-2 align-items-stretch">
|
|
<?php if (!empty($order['telefono_url'])): ?>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-primary"
|
|
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
|
|
data-phone="<?php echo htmlspecialchars((string) ($order['celular'] ?? '')); ?>"
|
|
data-order-label="<?php echo htmlspecialchars(cc_test_order_label($order)); ?>"
|
|
data-client-name="<?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?>"
|
|
onclick="return registrarLlamada(event, this)">
|
|
<i class="bi bi-telephone-outbound"></i> Llamar / AirDroid
|
|
</button>
|
|
<?php else: ?>
|
|
<button type="button" class="btn btn-sm btn-primary" disabled>Sin teléfono</button>
|
|
<?php endif; ?>
|
|
<?php if (!empty($order['whatsapp_url'])): ?>
|
|
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success">
|
|
<i class="bi bi-whatsapp"></i> WhatsApp
|
|
</a>
|
|
<?php endif; ?>
|
|
<button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
|
|
<i class="bi bi-sliders"></i> Gestionar
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
|
|
|
|
<?php ob_start(); ?>
|
|
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div>
|
|
<div class="small text-primary fw-semibold mb-1"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
|
<h3 class="modal-title h5 mb-1"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?></h3>
|
|
<div class="small text-muted"><?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?> · <?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin celular')); ?></div>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-lg-3 col-md-6">
|
|
<div class="border rounded p-3 h-100 bg-light-subtle">
|
|
<div class="small text-muted">Estado actual</div>
|
|
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6">
|
|
<div class="border rounded p-3 h-100 bg-light-subtle">
|
|
<div class="small text-muted">Total llamadas</div>
|
|
<div class="fw-semibold"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6">
|
|
<div class="border rounded p-3 h-100 bg-light-subtle">
|
|
<div class="small text-muted"><?php echo cc_test_requires_delivery_date($order['estado']) ? 'Entrega programada' : 'Próxima llamada'; ?></div>
|
|
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_requires_delivery_date($order['estado']) ? cc_test_format_date($order['fecha_entrega_programada'] ?? null) : cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6">
|
|
<div class="border rounded p-3 h-100 bg-light-subtle">
|
|
<div class="small text-muted">Ingreso en Drive</div>
|
|
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha')); ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-lg-7">
|
|
<h4 class="h6 fw-bold mb-3">Gestión comercial</h4>
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label for="estado-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Estado</label>
|
|
<select class="form-select" id="estado-<?php echo htmlspecialchars($order['source_key']); ?>" onchange="toggleAgendaFields('<?php echo htmlspecialchars($order['source_key']); ?>')">
|
|
<?php foreach (cc_test_valid_states() as $estadoOption): ?>
|
|
<option value="<?php echo htmlspecialchars($estadoOption); ?>" <?php echo $order['estado'] === $estadoOption ? 'selected' : ''; ?>>
|
|
<?php echo htmlspecialchars(cc_test_state_label($estadoOption)); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4 js-next-call-group" id="next-call-group-<?php echo htmlspecialchars($order['source_key']); ?>">
|
|
<label for="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Próxima llamada</label>
|
|
<input type="datetime-local" class="form-control" id="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_datetime_input($order['proxima_llamada_at'] ?? null)); ?>">
|
|
</div>
|
|
<div class="col-md-4 js-delivery-group <?php echo cc_test_requires_delivery_date($order['estado']) ? '' : 'd-none'; ?>" id="delivery-group-<?php echo htmlspecialchars($order['source_key']); ?>">
|
|
<label for="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Fecha de entrega 📅</label>
|
|
<input type="date" class="form-control" id="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_date_input($order['fecha_entrega_programada'] ?? null)); ?>">
|
|
<div class="form-text">Úsalo cuando el cliente quede en Confirmado fecha.</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Resumen</label>
|
|
<div class="border rounded px-3 py-2 bg-light-subtle small h-100 d-flex align-items-center">Los estados abiertos vuelven a <strong class="ms-1">Pendientes de hoy</strong>; Confirmado contraentrega 📅 te permite programar la entrega.</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label for="nota-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Nota interna</label>
|
|
<textarea class="form-control" id="nota-<?php echo htmlspecialchars($order['source_key']); ?>" rows="3" placeholder="Ejemplo: cliente pidió volver a llamar después de las 5 pm."><?php echo htmlspecialchars($order['nota_seguimiento']); ?></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h4 class="h6 fw-bold mb-3">Datos editables del pedido</h4>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label for="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Dirección</label>
|
|
<textarea class="form-control" id="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['direccion'] ?? '')); ?></textarea>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Referencia</label>
|
|
<textarea class="form-control" id="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['referencia'] ?? '')); ?></textarea>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="sede-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Sede / ID</label>
|
|
<input type="text" class="form-control" id="sede-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['sede'] ?? '')); ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Ciudad</label>
|
|
<input type="text" class="form-control" id="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['ciudad'] ?? '')); ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="distrito-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Distrito</label>
|
|
<input type="text" class="form-control" id="distrito-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['distrito'] ?? '')); ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">DNI</label>
|
|
<input type="text" class="form-control" id="dni-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['dni'] ?? '')); ?>">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label for="observaciones-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Observaciones del pedido</label>
|
|
<textarea class="form-control" id="observaciones-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['observaciones'] ?? '')); ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-5">
|
|
<h4 class="h6 fw-bold mb-3">Historial de llamadas</h4>
|
|
<?php if (empty($historial)): ?>
|
|
<div class="border rounded p-3 text-muted small">Aún no hay llamadas registradas para este cliente.</div>
|
|
<?php else: ?>
|
|
<div class="list-group list-group-flush border rounded overflow-hidden">
|
|
<?php foreach ($historial as $h): ?>
|
|
<div class="list-group-item small py-3">
|
|
<div class="d-flex justify-content-between gap-2">
|
|
<span class="fw-semibold"><?php echo htmlspecialchars($h['asesor'] ?? 'Asesor'); ?></span>
|
|
<span class="text-muted"><?php echo htmlspecialchars(date('d/m/Y H:i', strtotime($h['fecha_llamada']))); ?></span>
|
|
</div>
|
|
<div class="text-primary fw-semibold mt-1"><?php echo htmlspecialchars($h['resultado']); ?></div>
|
|
<?php if (!empty($h['observacion'])): ?>
|
|
<div class="text-muted mt-1"><?php echo htmlspecialchars($h['observacion']); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="mt-4 border rounded p-3 bg-light-subtle small">
|
|
<div class="fw-semibold mb-2">Origen vs edición</div>
|
|
<div><strong>Dirección Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion_drive'] ?? null)); ?></div>
|
|
<div><strong>Referencia Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia_drive'] ?? null)); ?></div>
|
|
<div><strong>Observación Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['observaciones_drive'] ?? null)); ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer d-flex justify-content-between">
|
|
<div class="small text-muted">Los cambios se guardan en la base local del módulo de prueba.</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
|
<button type="button" class="btn btn-primary" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Guardar gestión</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php $modalsHtml[] = ob_get_clean(); ?>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if (!empty($modalsHtml)): ?>
|
|
<?php echo implode("
|
|
", $modalsHtml); ?>
|
|
<?php endif; ?>
|
|
|
|
<div class="modal fade" id="airDroidAssistModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div>
|
|
<div class="small text-primary fw-semibold">Llamada preparada para AirDroid</div>
|
|
<h3 class="modal-title h5 mb-0">Número listo para pegar</h3>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="rounded-4 border bg-light-subtle p-3 mb-3">
|
|
<div class="small text-muted">Pedido</div>
|
|
<div class="fw-semibold" id="airDroidOrderLabel">-</div>
|
|
<div class="small text-muted mt-2">Cliente</div>
|
|
<div class="fw-semibold" id="airDroidClientName">-</div>
|
|
<div class="small text-muted mt-2">Número</div>
|
|
<div class="display-6 fw-bold lh-1" id="airDroidPhoneNumber">-</div>
|
|
</div>
|
|
<div class="alert alert-primary py-2 small mb-3" id="airDroidStatusBox">Intento registrado. Pega el número en tu aplicación de AirDroid para iniciar la llamada.</div>
|
|
<ol class="small ps-3 mb-0">
|
|
<li>Abre tu aplicación de AirDroid en la PC.</li>
|
|
<li>Pega el número copiado en el marcador.</li>
|
|
<li>Realiza la llamada y luego vuelve aquí para guardar el resultado.</li>
|
|
</ol>
|
|
</div>
|
|
<div class="modal-footer d-flex flex-wrap justify-content-between gap-2">
|
|
<div class="small text-muted" id="airDroidExtraStatus">Número copiado al portapapeles.</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-outline-secondary" id="airDroidCopyButton">Copiar número</button>
|
|
<button type="button" class="btn btn-outline-primary" id="airDroidOpenButton">Abrir AirDroid Web</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<script>
|
|
function toggleAgendaFields(sourceKey) {
|
|
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
|
|
const nextCallGroup = document.getElementById('next-call-group-' + sourceKey);
|
|
const deliveryGroup = document.getElementById('delivery-group-' + sourceKey);
|
|
const nextCallInput = document.getElementById('proxima-' + sourceKey);
|
|
const deliveryInput = document.getElementById('fecha-entrega-' + sourceKey);
|
|
const needsDeliveryDate = estado === 'CONFIRMADO CONTRAENTREGA' || estado === 'CONFIRMADO CONTRAENTREGA FECHA' || estado === 'CONFIRMADO FECHA';
|
|
const needsNextCall = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'].includes(estado);
|
|
|
|
if (nextCallGroup) {
|
|
nextCallGroup.classList.toggle('d-none', !needsNextCall);
|
|
}
|
|
if (deliveryGroup) {
|
|
deliveryGroup.classList.toggle('d-none', !needsDeliveryDate);
|
|
}
|
|
if (!needsNextCall && nextCallInput) {
|
|
nextCallInput.value = '';
|
|
}
|
|
if (!needsDeliveryDate && deliveryInput) {
|
|
deliveryInput.value = '';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.querySelectorAll('select[id^="estado-"]').forEach(select => {
|
|
toggleAgendaFields(select.id.replace('estado-', ''));
|
|
});
|
|
});
|
|
|
|
function guardarGestion(sourceKey, trigger) {
|
|
const body = new URLSearchParams({
|
|
source_key: sourceKey,
|
|
estado: document.getElementById('estado-' + sourceKey)?.value || 'POR LLAMAR',
|
|
proxima_llamada_at: document.getElementById('proxima-' + sourceKey)?.value || '',
|
|
fecha_entrega_programada: document.getElementById('fecha-entrega-' + sourceKey)?.value || '',
|
|
nota_seguimiento: document.getElementById('nota-' + sourceKey)?.value || '',
|
|
direccion: document.getElementById('direccion-' + sourceKey)?.value || '',
|
|
referencia: document.getElementById('referencia-' + sourceKey)?.value || '',
|
|
sede: document.getElementById('sede-' + sourceKey)?.value || '',
|
|
ciudad: document.getElementById('ciudad-' + sourceKey)?.value || '',
|
|
distrito: document.getElementById('distrito-' + sourceKey)?.value || '',
|
|
dni: document.getElementById('dni-' + sourceKey)?.value || '',
|
|
observaciones: document.getElementById('observaciones-' + sourceKey)?.value || ''
|
|
});
|
|
|
|
if (trigger) {
|
|
trigger.disabled = true;
|
|
}
|
|
|
|
fetch('update_callcenter_test_tracking.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
body: body.toString()
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
throw new Error(data.message || 'No se pudo guardar la gestión');
|
|
}
|
|
window.location.reload();
|
|
})
|
|
.catch(error => {
|
|
alert(error.message || 'Ocurrió un error al guardar la gestión.');
|
|
})
|
|
.finally(() => {
|
|
if (trigger) {
|
|
trigger.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function actualizarContadorLlamadas(sourceKey, total) {
|
|
document.querySelectorAll('.js-call-count-number[data-source-key="' + sourceKey + '"]').forEach(node => {
|
|
node.textContent = String(total);
|
|
});
|
|
}
|
|
|
|
function normalizarNumeroTelefono(value) {
|
|
return String(value || '').replace(/\D+/g, '');
|
|
}
|
|
|
|
function copyTextLegacy(text) {
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = text;
|
|
input.setAttribute('readonly', 'readonly');
|
|
input.style.position = 'fixed';
|
|
input.style.opacity = '0';
|
|
document.body.appendChild(input);
|
|
input.select();
|
|
input.setSelectionRange(0, input.value.length);
|
|
let copied = false;
|
|
|
|
try {
|
|
copied = document.execCommand('copy');
|
|
} catch (error) {
|
|
copied = false;
|
|
}
|
|
|
|
document.body.removeChild(input);
|
|
return copied;
|
|
}
|
|
|
|
function copyPhoneToClipboard(phone) {
|
|
if (!phone) {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(phone)
|
|
.then(() => true)
|
|
.catch(() => copyTextLegacy(phone));
|
|
}
|
|
|
|
return Promise.resolve(copyTextLegacy(phone));
|
|
}
|
|
|
|
function openAirDroidWeb() {
|
|
return window.open('https://web.airdroid.com/', '_blank', 'noopener');
|
|
}
|
|
|
|
function showAirDroidHelper(details) {
|
|
const phone = details.phone || '-';
|
|
const orderLabel = details.orderLabel || 'Sin número';
|
|
const clientName = details.clientName || 'Cliente sin nombre';
|
|
const registered = !!details.registered;
|
|
const copied = !!details.copied;
|
|
|
|
const orderNode = document.getElementById('airDroidOrderLabel');
|
|
const clientNode = document.getElementById('airDroidClientName');
|
|
const phoneNode = document.getElementById('airDroidPhoneNumber');
|
|
const statusNode = document.getElementById('airDroidStatusBox');
|
|
const extraNode = document.getElementById('airDroidExtraStatus');
|
|
const copyButton = document.getElementById('airDroidCopyButton');
|
|
const openButton = document.getElementById('airDroidOpenButton');
|
|
const modalElement = document.getElementById('airDroidAssistModal');
|
|
|
|
if (!modalElement) {
|
|
return;
|
|
}
|
|
|
|
if (orderNode) orderNode.textContent = orderLabel;
|
|
if (clientNode) clientNode.textContent = clientName;
|
|
if (phoneNode) phoneNode.textContent = phone;
|
|
|
|
if (statusNode) {
|
|
statusNode.className = 'alert ' + (registered ? 'alert-primary' : 'alert-warning') + ' py-2 small mb-3';
|
|
statusNode.textContent = registered
|
|
? 'Intento de llamada registrado. Ahora pega el número en tu app de AirDroid.'
|
|
: 'Se abrió la ayuda, pero el intento no se pudo registrar en el CRM.';
|
|
}
|
|
|
|
if (extraNode) {
|
|
extraNode.textContent = copied ? 'Número copiado correctamente.' : 'No se pudo copiar automáticamente; usa el botón Copiar número.';
|
|
}
|
|
|
|
if (copyButton) {
|
|
copyButton.dataset.phone = phone !== '-' ? phone : '';
|
|
}
|
|
|
|
if (openButton) {
|
|
openButton.onclick = function () {
|
|
openAirDroidWeb();
|
|
};
|
|
}
|
|
|
|
if (window.bootstrap && window.bootstrap.Modal) {
|
|
window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
|
|
}
|
|
}
|
|
|
|
function registrarLlamada(event, trigger) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
const sourceKey = trigger?.dataset?.sourceKey || '';
|
|
const phone = normalizarNumeroTelefono(trigger?.dataset?.phone || '');
|
|
const orderLabel = trigger?.dataset?.orderLabel || '';
|
|
const clientName = trigger?.dataset?.clientName || '';
|
|
|
|
if (!sourceKey) {
|
|
alert('No se encontró el pedido para registrar la llamada.');
|
|
return false;
|
|
}
|
|
|
|
if (trigger) {
|
|
trigger.classList.add('disabled');
|
|
trigger.setAttribute('aria-disabled', 'true');
|
|
}
|
|
|
|
const copyPromise = copyPhoneToClipboard(phone).catch(() => false);
|
|
|
|
const body = new URLSearchParams({
|
|
pedido_id: sourceKey,
|
|
resultado: 'Llamada iniciada - AirDroid',
|
|
observacion: 'Clic en botón Llamar / AirDroid desde el panel'
|
|
});
|
|
|
|
const savePromise = fetch('save_llamada.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
body: body.toString(),
|
|
keepalive: true
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'No se pudo registrar la llamada');
|
|
}
|
|
|
|
if (typeof data.total_llamadas !== 'undefined') {
|
|
actualizarContadorLlamadas(sourceKey, Number(data.total_llamadas) || 0);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
Promise.allSettled([savePromise, copyPromise]).then(results => {
|
|
const registerResult = results[0];
|
|
const copyResult = results[1];
|
|
const registered = registerResult.status === 'fulfilled' && registerResult.value === true;
|
|
const copied = copyResult.status === 'fulfilled' && copyResult.value === true;
|
|
|
|
if (!registered) {
|
|
const error = registerResult.reason;
|
|
console.error('Error al registrar llamada:', error);
|
|
alert((error && error.message) || 'No se pudo registrar la llamada.');
|
|
}
|
|
|
|
showAirDroidHelper({
|
|
phone,
|
|
orderLabel,
|
|
clientName,
|
|
registered,
|
|
copied
|
|
});
|
|
}).finally(() => {
|
|
if (trigger) {
|
|
trigger.classList.remove('disabled');
|
|
trigger.removeAttribute('aria-disabled');
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
const airDroidCopyButton = document.getElementById('airDroidCopyButton');
|
|
if (airDroidCopyButton) {
|
|
airDroidCopyButton.addEventListener('click', function () {
|
|
const phone = this.dataset.phone || '';
|
|
copyPhoneToClipboard(phone).then(copied => {
|
|
if (!copied) {
|
|
alert('No se pudo copiar el número automáticamente.');
|
|
return;
|
|
}
|
|
|
|
const extraNode = document.getElementById('airDroidExtraStatus');
|
|
if (extraNode) {
|
|
extraNode.textContent = 'Número copiado correctamente. Si la pestaña no se abrió antes, usa Abrir AirDroid.';
|
|
}
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<?php require_once 'layout_footer.php'; ?>
|