diff --git a/configuracion.php b/configuracion.php index 424874ac..9726346d 100644 --- a/configuracion.php +++ b/configuracion.php @@ -30,9 +30,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_visibility'])) { exit(); } +// Guardar Shalom API Key (para que tracking funcione sin editar .env) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_shalom_api_key'])) { + $shalomApiKey = trim($_POST['shalom_api_key'] ?? ''); + if ($shalomApiKey === '') { + $_SESSION['error_message'] = 'Debes ingresar la SHALOM_API_KEY.'; + } else { + $stmt = $conn->prepare("INSERT INTO configuracion (clave, valor) VALUES ('SHALOM_API_KEY', :valor) ON DUPLICATE KEY UPDATE valor = :valor"); + $stmt->bindParam(':valor', $shalomApiKey); + if ($stmt->execute()) { + $_SESSION['success_message'] = 'Shalom API key guardada correctamente.'; + } else { + $_SESSION['error_message'] = 'Error al guardar la Shalom API key.'; + } + } + header("Location: configuracion.php"); + exit(); +} + // Sincronizar sedes de Shalom if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_shalom'])) { - $apiKey = 'sk_mlq1j3na_4a676ewvaop'; + $apiKey = getenv('SHALOM_API_KEY'); + + if (!$apiKey) { + $stmtKey = $conn->prepare("SELECT valor FROM configuracion WHERE clave = 'SHALOM_API_KEY' LIMIT 1"); + $stmtKey->execute(); + $apiKey = $stmtKey->fetchColumn(); + } + + if (!$apiKey) { + $_SESSION['error_message'] = 'Falta SHALOM_API_KEY. Configúrala en Configuración General (Administrador) antes de sincronizar sedes.'; + header("Location: configuracion.php"); + exit(); + } $url = "https://shalom-api.lat/api/listar"; $ch = curl_init(); @@ -127,6 +157,19 @@ if (isset($_SESSION['error_message'])) {

Conéctate directamente con la API de Shalom para actualizar el listado de agencias disponibles en el sistema.

+ +
+ +
+ + + Se usará para el tracking y la sincronización de sedes. (No se muestra la clave actual por seguridad.) +
+ +
+
@@ -265,7 +346,7 @@ foreach ($videos as $v) {
En Proceso
-

+

@@ -274,7 +355,7 @@ foreach ($videos as $v) {
Terminados
-

+

@@ -283,7 +364,7 @@ foreach ($videos as $v) {
Aprobados
-

+

@@ -292,7 +373,8 @@ foreach ($videos as $v) {

Producción de Video

-

Gestión de activos y flujo de trabajo creativo

+

Gestión de activos y flujo de trabajo creativo

+

La columna Observación se guarda al salir del campo y Estado al cambiar la opción.

@@ -337,6 +419,7 @@ foreach ($videos as $v) { Link Video Link Landing Link Flyer + Observación Estado Acción @@ -344,7 +427,7 @@ foreach ($videos as $v) { - + No hay pedidos registrados. @@ -462,21 +545,31 @@ foreach ($videos as $v) { - - - - - + + +
+ + + +
-
- - + +
@@ -555,7 +641,7 @@ foreach ($videos as $v) {
-
@@ -838,9 +924,210 @@ function switchView(view) { } } +function getStatusBadgeClass(status) { + switch (status) { + case 'PENDIENTE': + return 'bg-warning text-dark'; + case 'EN PROCESO': + return 'bg-info text-white'; + case 'TERMINADO': + return 'bg-primary text-white'; + case 'OBSERVADO': + return 'bg-danger text-white'; + case 'APROBADO': + return 'bg-success text-white'; + case 'PUBLICADO': + return 'bg-dark text-white'; + default: + return 'bg-secondary text-white'; + } +} + +function updateCardStatusBadge(videoId, status) { + document.querySelectorAll(`.video-card-status-badge[data-video-id="${videoId}"]`).forEach(badge => { + badge.className = `badge status-badge video-card-status-badge ${getStatusBadgeClass(status)} shadow-sm`; + badge.textContent = status || '-'; + }); +} + +function updateStatusCounter(status, delta) { + const counters = { + 'PENDIENTE': 'status-count-pendiente', + 'EN PROCESO': 'status-count-en-proceso', + 'TERMINADO': 'status-count-terminado', + 'APROBADO': 'status-count-aprobado' + }; + + const counterId = counters[status]; + if (!counterId) { + return; + } + + const counter = document.getElementById(counterId); + if (!counter) { + return; + } + + const currentValue = parseInt(counter.textContent, 10); + if (Number.isNaN(currentValue)) { + return; + } + + counter.textContent = Math.max(0, currentValue + delta); +} + +function syncVideoPayload(videoId, field, value) { + document.querySelectorAll(`[data-video-id="${videoId}"][data-video-payload]`).forEach(button => { + try { + const payload = JSON.parse(button.dataset.videoPayload); + payload[field] = value; + button.dataset.videoPayload = JSON.stringify(payload); + } catch (error) { + console.error('No se pudo sincronizar el payload del video.', error); + } + }); +} + +function gestionarVideoFromButton(button) { + if (!button) { + return; + } + + const rawPayload = button.getAttribute('data-video-payload'); + if (!rawPayload) { + return; + } + + try { + gestionarVideo(JSON.parse(rawPayload)); + } catch (error) { + console.error('No se pudo abrir el modal del video.', error); + } +} + document.addEventListener('DOMContentLoaded', function() { const savedView = localStorage.getItem('video_production_view') || 'table'; switchView(savedView); + + const observationInputs = document.querySelectorAll('.observation-input'); + observationInputs.forEach(input => { + let lastSavedValue = input.value; + const status = document.getElementById(`observation-status-${input.dataset.id}`); + + const setStatus = (message, className = 'text-muted') => { + if (!status) return; + status.className = `small mt-1 observation-status ${className}`; + status.textContent = message; + }; + + const saveObservation = () => { + const newValue = input.value; + if (newValue === lastSavedValue) { + return; + } + + setStatus('Guardando...', 'text-primary'); + + fetch('update_marketing_video_field.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: input.dataset.id, + field: 'observacion', + value: newValue + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + lastSavedValue = newValue; + setStatus('Guardado', 'text-success'); + setTimeout(() => { + if (status && status.textContent === 'Guardado') { + setStatus('', 'text-muted'); + } + }, 2000); + } else { + setStatus('Error al guardar', 'text-danger'); + alert('Error: ' + data.error); + } + }) + .catch(() => { + setStatus('Error al guardar', 'text-danger'); + }); + }; + + input.addEventListener('blur', saveObservation); + input.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + input.blur(); + } + }); + }); + + const statusSelects = document.querySelectorAll('.status-select'); + statusSelects.forEach(select => { + let lastSavedValue = select.value; + const feedback = document.getElementById(`status-feedback-${select.dataset.id}`); + + const setFeedback = (message, className = 'text-muted') => { + if (!feedback) return; + feedback.className = `small mt-1 status-feedback ${className}`; + feedback.textContent = message; + }; + + select.addEventListener('change', function() { + const newValue = select.value; + const previousValue = lastSavedValue; + + if (newValue === previousValue) { + return; + } + + select.disabled = true; + select.dataset.status = newValue; + setFeedback('Guardando...', 'text-primary'); + + fetch('update_marketing_video_field.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: select.dataset.id, + field: 'estado', + value: newValue + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + lastSavedValue = newValue; + syncVideoPayload(select.dataset.id, 'estado', newValue); + updateCardStatusBadge(select.dataset.id, newValue); + updateStatusCounter(previousValue, -1); + updateStatusCounter(newValue, 1); + setFeedback('Guardado', 'text-success'); + setTimeout(() => { + if (feedback && feedback.textContent === 'Guardado') { + setFeedback('', 'text-muted'); + } + }, 2000); + } else { + select.value = previousValue; + select.dataset.status = previousValue; + setFeedback('Error al guardar', 'text-danger'); + alert('Error: ' + data.error); + } + }) + .catch(() => { + select.value = previousValue; + select.dataset.status = previousValue; + setFeedback('Error al guardar', 'text-danger'); + }) + .finally(() => { + select.disabled = false; + }); + }); + }); }); function addLinkInput(containerId, inputName, value = '') { @@ -978,6 +1265,7 @@ document.addEventListener('DOMContentLoaded', function() { if (data.success) { // Actualizar data-value para la próxima edición this.setAttribute('data-value', newValue); + syncVideoPayload(id, field, newValue); if (field === 'estado') { let badgeClass = 'bg-secondary'; diff --git a/pedidos_en_transito.php b/pedidos_en_transito.php index d4d82136..aaf94910 100644 --- a/pedidos_en_transito.php +++ b/pedidos_en_transito.php @@ -250,6 +250,7 @@ include 'layout_header.php';
🚚 + 💬
@@ -522,6 +523,14 @@ document.addEventListener('DOMContentLoaded', function() { verifyButton.innerHTML = ' Verificando...'; } + const setStatusCell = (cell, badgeClass, text, title = '') => { + cell.innerHTML = ''; + const span = document.createElement('span'); + span.className = `badge ${badgeClass}`; + span.textContent = text; + if (title) span.title = title; + cell.appendChild(span); + }; rows.forEach((row, index) => { setTimeout(() => { @@ -533,65 +542,121 @@ document.addEventListener('DOMContentLoaded', function() { if (!statusCell) return; + const reenableIfLast = () => { + if (index === rows.length - 1 && verifyButton) { + verifyButton.disabled = false; + verifyButton.innerHTML = 'Verificar Estados Ahora'; + } + }; + if (orderCode === '26') { - statusCell.innerHTML = 'OLVA COURIER'; + setStatusCell(statusCell, 'bg-info', 'OLVA COURIER'); + reenableIfLast(); return; } if (!orderNumber || !orderCode || orderNumber === 'N/A' || orderCode === 'N/A') { - statusCell.innerHTML = 'Sin datos'; + setStatusCell(statusCell, 'bg-light text-dark', 'Sin datos'); + reenableIfLast(); return; } - statusCell.innerHTML = 'Verificando...'; + setStatusCell(statusCell, 'bg-info text-dark', 'Verificando...'); fetch(`shalom_api.php?orderNumber=${encodeURIComponent(orderNumber)}&orderCode=${encodeURIComponent(orderCode)}`) - .then(response => { - if (!response.ok) { throw new Error('Network response was not ok.'); } - return response.json(); + .then(async response => { + let data = null; + try { data = await response.json(); } catch (e) { /* ignore */ } + + if (!response.ok) { + const msg = (data && data.error) ? data.error : `HTTP ${response.status}`; + const details = (data && data.details) ? data.details : ''; + let detailsText = ''; + + if (details) { + detailsText = typeof details === 'string' ? details : JSON.stringify(details); + } + + // Devolvemos el error dentro de JSON para que el siguiente then lo pinte como Error + return { + error: msg, + details: detailsText, + http_code: response.status + }; + } + + return data; }) .then(data => { - if (data.error) { - statusCell.innerHTML = `Error`; - } else if (data.statuses && data.statuses.message) { + if (data && data.error) { + const details = data.details + ? (typeof data.details === 'string' ? data.details : JSON.stringify(data.details)) + : ''; + + const title = details ? `${data.error} - ${details}` : data.error; + + const errText = (data.error || '').toString(); + + const codeMatch = errText.match(/c[oó]digo\s*(\d{3,5})/i); + const codeMatch2 = !codeMatch ? errText.match(/Shalom\s*(\d{3,5})/i) : null; + + let badgeText = 'Error Shalom'; + if (codeMatch) { + badgeText = `Shalom ${codeMatch[1]}`; + } else if (codeMatch2) { + badgeText = `Shalom ${codeMatch2[1]}`; + } else { + const isTimeout = /timed out|timeout/i.test(errText) || /timed out|timeout/i.test(details); + if (isTimeout) { + badgeText = 'Shalom timeout'; + } else if (/comunicaci[oó]n/i.test(errText)) { + badgeText = 'Error API Shalom'; + } else { + const shortErr = errText.length > 22 ? errText.slice(0, 22) + '…' : errText; + badgeText = shortErr || 'Error Shalom'; + } + } + + setStatusCell(statusCell, 'bg-danger', badgeText, title); + } else if (data && data.statuses && data.statuses.message) { const statusMessage = data.statuses.message; let badgeClass = 'bg-secondary'; let whatsappClass = 'btn-secondary'; - if (statusMessage.toUpperCase().includes('EN DESTINO')) { + const upper = (statusMessage || '').toUpperCase(); + + if (upper.includes('EN DESTINO')) { badgeClass = 'bg-success'; - whatsappClass = 'btn-success'; // Green - } else if (statusMessage.toUpperCase().includes('EN TRANSITO')) { + whatsappClass = 'btn-success'; + } else if (upper.includes('EN TRANSITO')) { badgeClass = 'bg-primary'; - } else if (statusMessage.toUpperCase().includes('REGISTRADO')) { + } else if (upper.includes('REGISTRADO')) { badgeClass = 'bg-warning text-dark'; } - - statusCell.innerHTML = `${statusMessage}`; - + + setStatusCell(statusCell, badgeClass, statusMessage); + if (whatsappIcon) { whatsappIcon.classList.remove('btn-secondary', 'btn-success'); whatsappIcon.classList.add(whatsappClass); } } else { - statusCell.innerHTML = 'Inválido'; + setStatusCell(statusCell, 'bg-warning text-dark', 'Inválido'); } }) .catch(error => { console.error('Error fetching status:', error); - statusCell.innerHTML = 'Fallo'; + + const fullMessage = error && error.message ? error.message : 'Error al consultar'; + const shortErr = fullMessage.length > 22 ? fullMessage.slice(0, 22) + '…' : fullMessage; + setStatusCell(statusCell, 'bg-danger', shortErr, fullMessage); }) .finally(() => { - // Re-enable button after the last request is done - if (index === rows.length - 1 && verifyButton) { - verifyButton.disabled = false; - verifyButton.innerHTML = 'Verificar Estados Ahora'; - } + reenableIfLast(); }); }, index * 500); // Stagger requests to avoid overwhelming the server/API }); } - // Initial check on page load verificarEstados(); @@ -600,6 +665,20 @@ document.addEventListener('DOMContentLoaded', function() { if (verifyButton) { verifyButton.addEventListener('click', verificarEstados); } + + // Copiar resumen desde el icono de la columna Celular + document.addEventListener('click', function (e) { + const btn = e.target && e.target.closest ? e.target.closest('.copy-summary-icon') : null; + if (!btn) return; + + const message = btn.getAttribute('data-whatsapp-message') || ''; + if (!message) { + alert('No hay resumen disponible para copiar.'); + return; + } + + copyStatusToClipboard(message); + }); }); function copyStatusToClipboard(message) { diff --git a/shalom_api.php b/shalom_api.php index bead4744..2dd3f5c4 100644 --- a/shalom_api.php +++ b/shalom_api.php @@ -1,63 +1,164 @@ 'Número de orden y código de orden son requeridos.']); exit; } -// 2. Configurar la llamada a la API de Shalom -$apiKey = 'sk_mlq1j3na_4a676ewvaop'; -$url = "https://shalom-api.lat/api/track"; // Endpoint correcto para POST +// Load env from executor/.env (helps when Apache/PHP doesn't export .env vars) +function load_dotenv_if_needed(array $keys): void { + $missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); + if (empty($missing)) { + return; + } -// Datos para el cuerpo de la solicitud POST -$postData = [ - 'orderNumber' => $orderNumber, - 'orderCode' => $orderCode -]; -$jsonData = json_encode($postData); + static $loaded = false; + if ($loaded) return; -$ch = curl_init(); + $envPath = realpath(__DIR__ . '/../.env'); // executor/.env + if ($envPath && is_readable($envPath)) { + $lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($lines as $line) { + $trimmed = trim($line); + if ($trimmed === '' || $trimmed[0] === '#') { + continue; + } + if (!str_contains($trimmed, '=')) continue; -curl_setopt($ch, CURLOPT_URL, $url); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_POST, true); // Especificar que es una solicitud POST -curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); // Enviar los datos en formato JSON -curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Accept: application/json', - "Authorization: Bearer {$apiKey}" -]); + [$k, $v] = array_map('trim', explode('=', $trimmed, 2)); + if ($k === '') continue; -// 3. Ejecutar la llamada y obtener la respuesta -$response = curl_exec($ch); -$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); -$error = curl_error($ch); -curl_close($ch); + // Strip potential surrounding quotes + $v = trim($v, "\"' "); -// 4. Manejar la respuesta -if ($error) { + // Do not override existing process env + if (getenv($k) === false || getenv($k) === '') { + putenv("{$k}={$v}"); + } + } + $loaded = true; + } +} + +load_dotenv_if_needed(['SHALOM_API_KEY']); + +// API +$apiKey = getenv('SHALOM_API_KEY'); + +// Fallback: allow setting the key from the DB (so the app works even if you can't edit .env) +if (!$apiKey) { + try { + require_once __DIR__ . '/db/config.php'; + $pdo = db(); + $stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1'); + $stmt->execute(['SHALOM_API_KEY']); + $apiKey = $stmt->fetchColumn(); + if (is_string($apiKey)) { + $apiKey = trim($apiKey); + } else { + $apiKey = null; + } + } catch (Throwable $e) { + $apiKey = null; + } +} + +if (!$apiKey) { http_response_code(500); - echo json_encode(['error' => 'Error en la comunicación con la API de Shalom: ' . $error]); + echo json_encode([ + 'error' => 'Falta SHALOM_API_KEY. Configura la clave en Configuración General (Administrador) o en tu .env para poder consultar el tracking.', + ]); exit; } +$url = 'https://shalom-api.lat/api/track'; + +$postData = [ + 'orderNumber' => $orderNumber, + 'orderCode' => $orderCode, +]; + +$jsonData = json_encode($postData, JSON_UNESCAPED_UNICODE); +if ($jsonData === false) { + http_response_code(500); + echo json_encode(['error' => 'Error serializando la solicitud a Shalom.']); + exit; +} + +$ch = curl_init(); + +curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $jsonData, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: Bearer {$apiKey}", + ], + // Timeouts: evitamos que la UI quede esperando indefinidamente + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 25, + CURLOPT_FOLLOWLOCATION => true, +]); + +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlError = curl_error($ch); + +curl_close($ch); + +if ($curlError) { + http_response_code(502); + echo json_encode([ + 'error' => 'Error en la comunicación con la API de Shalom', + 'details' => $curlError, + ]); + exit; +} + +if ($response === false || $response === null || $response === '') { + http_response_code(502); + echo json_encode([ + 'error' => 'Shalom no devolvió respuesta', + 'http_code' => $httpCode ?: null, + ]); + exit; +} + +// Error HTTP del proveedor if ($httpCode >= 400) { http_response_code($httpCode); - // Intentar decodificar el cuerpo del error si Shalom lo envía en JSON + $errorBody = json_decode($response, true); - if (json_last_error() === JSON_ERROR_NONE && isset($errorBody['message'])) { - echo json_encode(['error' => "Error de la API de Shalom: " . $errorBody['message']]); + $shMessage = null; + + if (json_last_error() === JSON_ERROR_NONE && is_array($errorBody)) { + $shMessage = $errorBody['message'] ?? $errorBody['error'] ?? null; + } + + if ($shMessage) { + echo json_encode([ + 'error' => "Error de la API de Shalom: {$shMessage}", + 'http_code' => $httpCode, + 'details' => $errorBody ?: $response, + ]); } else { - echo json_encode(['error' => "Error de la API de Shalom (código {$httpCode}).", 'details' => $response]); + echo json_encode([ + 'error' => "Error de la API de Shalom (código {$httpCode}).", + 'http_code' => $httpCode, + 'details' => $response, + ]); } exit; } -// 5. Devolver la respuesta exitosa al cliente +// OK: devolvemos tal cual lo que responde Shalom (la UI espera JSON con search/statuses) echo $response; diff --git a/update_marketing_video_field.php b/update_marketing_video_field.php index a54e7cbc..8ddf9de6 100644 --- a/update_marketing_video_field.php +++ b/update_marketing_video_field.php @@ -1,6 +1,8 @@ 'ID de video no válido.']); + exit; +} + +if (is_array($value) || is_object($value)) { + http_response_code(400); + echo json_encode(['error' => 'Valor no válido.']); + exit; +} + +$value = is_string($value) ? trim($value) : (string)$value; + // Whitelist allowed fields for security -$allowed_fields = ['material', 'estado', 'angulo_video']; -if (!in_array($field, $allowed_fields)) { +$allowed_fields = ['material', 'estado', 'angulo_video', 'observacion']; +if (!in_array($field, $allowed_fields, true)) { http_response_code(400); echo json_encode(['error' => 'Campo no permitido.']); exit; } +if ($field === 'estado') { + $allowedStatuses = ['PENDIENTE', 'EN PROCESO', 'TERMINADO', 'OBSERVADO', 'APROBADO', 'PUBLICADO']; + if (!in_array($value, $allowedStatuses, true)) { + http_response_code(400); + echo json_encode(['error' => 'Estado no permitido.']); + exit; + } +} + try { $pdo = db(); - + marketing_ensure_schema($pdo); + $sql = "UPDATE marketing_videos SET $field = ? WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$value, $video_id]);