- |
+ |
No hay pedidos registrados.
|
@@ -462,21 +545,31 @@ foreach ($videos as $v) {
-
-
-
-
-
+ |
+
+
+ |
+
+
+
|
-
-
-
+
+
@@ -555,7 +641,7 @@ foreach ($videos as $v) {
-
+
Gestionar
@@ -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]);