Autosave: 20260525-070710

This commit is contained in:
Flatlogic Bot 2026-05-25 07:07:14 +00:00
parent 623d4cd358
commit 891893f221
3 changed files with 243 additions and 39 deletions

View File

@ -70,11 +70,11 @@ function cc_test_order_label(array $order): string
function cc_test_badge_class(string $estado): string
{
return match ($estado) {
'CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO' => 'bg-success-subtle text-success-emphasis',
'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',
'ENVIO REPETIDO' => 'bg-secondary-subtle text-secondary-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',
};
@ -97,6 +97,7 @@ $allowedViews = [
'pendientes_hoy' => 'Pendientes de hoy',
'nuevos_hoy' => 'Nuevos de hoy',
'confirmados' => 'Confirmados',
'observados' => 'Observados',
'cerrados' => 'Cerrados / descartados',
'todos' => 'Todos los pedidos cargados',
];
@ -115,6 +116,7 @@ $stats = [
'pendientes_hoy' => 0,
'nuevos_hoy' => 0,
'confirmados' => 0,
'observados' => 0,
'cerrados' => 0,
];
@ -164,6 +166,9 @@ try {
if (in_array($order['estado'], cc_test_confirmed_states(), true)) {
$stats['confirmados']++;
}
if ($order['estado'] === 'OBSERVADO') {
$stats['observados']++;
}
if ($order['es_cerrado']) {
$stats['cerrados']++;
}
@ -175,6 +180,7 @@ try {
'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,
};
@ -209,7 +215,7 @@ require_once 'layout_header.php';
<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> y <strong>Cerrados</strong>. Así no se mezcla todo cuando entran pedidos diarios.</p>
<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>
@ -229,7 +235,7 @@ require_once 'layout_header.php';
<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 gestionas cada cliente con estado, próxima llamada y observaciones internas. Los casos abiertos siguen apareciendo en <strong>Pendientes de hoy</strong> hasta que los cierres o confirmes.</p>
<p class="mb-0">Los pedidos nuevos entran desde Drive. Luego 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>
@ -269,6 +275,16 @@ require_once 'layout_header.php';
</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'; ?>">
@ -279,13 +295,12 @@ require_once 'layout_header.php';
</article>
</a>
</div>
<div class="col-md-6 col-xl-4">
<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-warning-subtle border border-warning' : 'bg-white'; ?>">
<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 los pedidos cargados</div>
<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 class="small text-muted mt-2">Usa esta vista solo para revisión general.</div>
</div>
</article>
</a>
@ -296,7 +311,8 @@ require_once 'layout_header.php';
<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-0">Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado fecha 📅, Contraentrega confirmado, Cancelado y Envío repetido.</p>
<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>
@ -366,9 +382,16 @@ require_once 'layout_header.php';
<td class="text-center">
<div class="d-flex flex-column gap-2 align-items-stretch">
<?php if (!empty($order['telefono_url'])): ?>
<a href="<?php echo htmlspecialchars($order['telefono_url']); ?>" class="btn btn-sm btn-primary" onclick="return registrarLlamada(event, '<?php echo htmlspecialchars($order['source_key']); ?>', '<?php echo htmlspecialchars($order['telefono_url']); ?>', this)">
<i class="bi bi-telephone-outbound"></i> Llamar
</a>
<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; ?>
@ -450,7 +473,7 @@ require_once 'layout_header.php';
</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 fecha 📅 te permite programar la entrega.</div>
<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>
@ -546,6 +569,43 @@ require_once 'layout_header.php';
<?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>
@ -556,15 +616,16 @@ function toggleAgendaFields(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 FECHA';
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', needsDeliveryDate);
nextCallGroup.classList.toggle('d-none', !needsNextCall);
}
if (deliveryGroup) {
deliveryGroup.classList.toggle('d-none', !needsDeliveryDate);
}
if (needsDeliveryDate && nextCallInput) {
if (!needsNextCall && nextCallInput) {
nextCallInput.value = '';
}
if (!needsDeliveryDate && deliveryInput) {
@ -626,23 +687,129 @@ function actualizarContadorLlamadas(sourceKey, total) {
});
}
function registrarLlamada(event, sourceKey, phoneUrl, trigger) {
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',
observacion: 'Clic en botón llamar desde el panel'
resultado: 'Llamada iniciada - AirDroid',
observacion: 'Clic en botón Llamar / AirDroid desde el panel'
});
fetch('save_llamada.php', {
const savePromise = fetch('save_llamada.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
@ -657,24 +824,56 @@ function registrarLlamada(event, sourceKey, phoneUrl, trigger) {
if (typeof data.total_llamadas !== 'undefined') {
actualizarContadorLlamadas(sourceKey, Number(data.total_llamadas) || 0);
}
})
.catch(error => {
console.error('Error al registrar llamada:', error);
alert(error.message || 'No se pudo registrar la llamada.');
})
.finally(() => {
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');
}
if (phoneUrl) {
window.location.href = phoneUrl;
}
});
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'; ?>

View File

@ -91,10 +91,10 @@ function cc_test_valid_states(): array
'DEVOLVER LLAMADA',
'OBSERVADO',
'SE ENVIO NUMERO DE CUENTA',
'CONFIRMADO FECHA',
'CONTRAENTREGA CONFIRMADO',
'CONFIRMADO CONTRAENTREGA',
'CONFIRMADO ENVIO',
'CANCELADO',
'ENVIO REPETIDO',
'REPETIDO',
];
}
@ -105,25 +105,30 @@ function cc_test_open_states(): array
function cc_test_confirmed_states(): array
{
return ['CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO'];
return ['CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO', 'CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO'];
}
function cc_test_closed_states(): array
{
return ['CANCELADO', 'ENVIO REPETIDO'];
return ['CANCELADO', 'REPETIDO'];
}
function cc_test_requires_delivery_date(string $estado): bool
{
return $estado === 'CONFIRMADO FECHA';
return in_array($estado, ['CONFIRMADO CONTRAENTREGA', 'CONFIRMADO FECHA'], true);
}
function cc_test_state_label(string $estado): string
{
return match ($estado) {
'SE ENVIO NUMERO DE CUENTA' => 'SE ENVIÓ NÚMERO DE CUENTA',
'CONFIRMADO FECHA' => 'CONFIRMADO FECHA 📅',
'ENVIO REPETIDO' => 'ENVÍO REPETIDO',
'CONFIRMADO CONTRAENTREGA' => 'CONFIRMADO CONTRAENTREGA 📅',
'CONFIRMADO CONTRAENTREGA FECHA' => 'CONFIRMADO CONTRAENTREGA 📅',
'CONFIRMADO FECHA' => 'CONFIRMADO CONTRAENTREGA 📅',
'CONFIRMADO ENVIO' => 'CONFIRMADO ENVIO',
'CONTRAENTREGA CONFIRMADO' => 'CONFIRMADO ENVIO',
'REPETIDO' => 'REPETIDO',
'ENVIO REPETIDO' => 'REPETIDO',
default => $estado,
};
}

View File

@ -78,7 +78,7 @@ try {
}
if (cc_test_requires_delivery_date($estado) && $fechaEntrega === null) {
throw new RuntimeException('Debes seleccionar la fecha de entrega para CONFIRMADO FECHA 📅.');
throw new RuntimeException('Debes seleccionar la fecha de entrega para CONFIRMADO CONTRAENTREGA 📅.');
}
if (!in_array($estado, cc_test_open_states(), true)) {