Pantalla TV del turnero: playlist de videos e imágenes en vez de un solo video
Nueva tabla turnero_tv_media (tipo, url, orden, duracion_segundos) para soportar múltiples videos/imágenes reproducidos en secuencia y en bucle. El video que ya estaba cargado se migró como primer ítem de la playlist. - save_tv_media.php / delete_tv_media.php / reorder_tv_media.php reemplazan a save_tv_video.php (eliminado). - configuracion.php (tab TV): subida múltiple, lista reordenable por arrastre, duración editable por imagen. - display_global.php: el <video> único se reemplaza por un reproductor JS que recorre la playlist — video hasta 'ended', imagen por su duración configurada, y vuelve a empezar al terminar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2ba13add34
commit
6254e45e8f
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/api/delete_tv_media.php
|
||||
* Elimina un ítem de la playlist de la Pantalla TV (archivo + fila).
|
||||
* POST JSON: { id: int }
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!isUserLoggedIn()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'No autenticado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['ok' => false, 'error' => 'id requerido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT url FROM turnero_tv_media WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No encontrado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadDir = dirname(__DIR__, 3) . '/uploads/turnero/tv_media/';
|
||||
$filename = basename(parse_url($row['url'], PHP_URL_PATH));
|
||||
$filepath = $uploadDir . $filename;
|
||||
if (is_file($filepath)) {
|
||||
@unlink($filepath);
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM turnero_tv_media WHERE id = ?")->execute([$id]);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/api/reorder_tv_media.php
|
||||
* Guarda el orden y la duración (para imágenes) de la playlist de la Pantalla TV.
|
||||
* POST JSON: { items: [{ id, orden, duracion_segundos }, ...] }
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!isUserLoggedIn()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'No autenticado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$items = is_array($body['items'] ?? null) ? $body['items'] : [];
|
||||
|
||||
if (!$items) {
|
||||
echo json_encode(['ok' => false, 'error' => 'items requerido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE turnero_tv_media SET orden = ?, duracion_segundos = ? WHERE id = ?"
|
||||
);
|
||||
foreach ($items as $i => $it) {
|
||||
$id = (int)($it['id'] ?? 0);
|
||||
if (!$id) continue;
|
||||
$dur = max(1, (int)($it['duracion_segundos'] ?? 8));
|
||||
$stmt->execute([$i, $dur, $id]);
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/api/save_tv_media.php
|
||||
* Sube un video o imagen a la playlist de la Pantalla TV del turnero.
|
||||
* POST multipart: campo "media" → sube y agrega al final de la lista
|
||||
* Guarda el archivo en /uploads/turnero/tv_media/ y una fila en turnero_tv_media.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!isUserLoggedIn()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'No autenticado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$uploadDir = dirname(__DIR__, 3) . '/uploads/turnero/tv_media/';
|
||||
$publicBase = rtrim(BASE_URL, '/') . '/uploads/turnero/tv_media/';
|
||||
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se pudo crear el directorio de uploads']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_FILES['media'])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se recibió ningún archivo']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['media'];
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$errores = [
|
||||
UPLOAD_ERR_INI_SIZE => 'El archivo supera upload_max_filesize en php.ini',
|
||||
UPLOAD_ERR_FORM_SIZE => 'El archivo supera MAX_FILE_SIZE del formulario',
|
||||
UPLOAD_ERR_PARTIAL => 'El archivo se subió parcialmente',
|
||||
UPLOAD_ERR_NO_FILE => 'No se seleccionó ningún archivo',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Falta la carpeta temporal',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Error al escribir el archivo',
|
||||
];
|
||||
echo json_encode(['ok' => false, 'error' => $errores[$file['error']] ?? 'Error de subida desconocido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$maxBytes = 500 * 1024 * 1024;
|
||||
if ($file['size'] > $maxBytes) {
|
||||
echo json_encode(['ok' => false, 'error' => 'El archivo supera el límite de 500 MB']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mimeMap = [
|
||||
'video/mp4' => ['video', 'mp4'],
|
||||
'video/webm' => ['video', 'webm'],
|
||||
'video/ogg' => ['video', 'ogv'],
|
||||
'image/jpeg' => ['imagen', 'jpg'],
|
||||
'image/png' => ['imagen', 'png'],
|
||||
'image/webp' => ['imagen', 'webp'],
|
||||
];
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!isset($mimeMap[$mime])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Formato no admitido. Usa MP4, WebM, JPG, PNG o WEBP']);
|
||||
exit;
|
||||
}
|
||||
|
||||
[$tipo, $ext] = $mimeMap[$mime];
|
||||
|
||||
$destFilename = uniqid('tv_', true) . '.' . $ext;
|
||||
$destPath = $uploadDir . $destFilename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se pudo guardar el archivo']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$publicUrl = $publicBase . $destFilename;
|
||||
|
||||
$maxOrden = (int)($pdo->query("SELECT COALESCE(MAX(orden),-1) FROM turnero_tv_media")->fetchColumn());
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO turnero_tv_media (tipo, url, orden, duracion_segundos, activo)
|
||||
VALUES (?, ?, ?, 8, 1)"
|
||||
);
|
||||
$stmt->execute([$tipo, $publicUrl, $maxOrden + 1]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'item' => [
|
||||
'id' => (int)$pdo->lastInsertId(),
|
||||
'tipo' => $tipo,
|
||||
'url' => $publicUrl,
|
||||
'orden' => $maxOrden + 1,
|
||||
'duracion_segundos' => 8,
|
||||
],
|
||||
]);
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/api/save_tv_video.php
|
||||
* Sube o elimina el video de fondo de la Pantalla TV del turnero.
|
||||
*
|
||||
* POST multipart: campo "video" → sube y guarda
|
||||
* POST JSON: { "accion": "borrar" } → elimina el video actual
|
||||
*
|
||||
* Guarda el archivo en /uploads/turnero/tv_video.{ext}
|
||||
* y registra la URL en lab_config (clave: turnero_tv_video).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Solo administradores autenticados
|
||||
if (!isUserLoggedIn()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'No autenticado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$uploadDir = dirname(__DIR__, 3) . '/uploads/turnero/';
|
||||
$publicBase = rtrim(BASE_URL, '/') . '/uploads/turnero/';
|
||||
|
||||
// ── Crear carpeta si no existe ────────────────────────────────
|
||||
if (!is_dir($uploadDir)) {
|
||||
if (!mkdir($uploadDir, 0755, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se pudo crear el directorio de uploads']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Acción: borrar ────────────────────────────────────────────
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$bodyJson = $rawBody ? json_decode($rawBody, true) : null;
|
||||
|
||||
if (isset($bodyJson['accion']) && $bodyJson['accion'] === 'borrar') {
|
||||
// Obtener ruta guardada
|
||||
$stmt = $pdo->prepare("SELECT valor FROM lab_config WHERE clave = 'turnero_tv_video' LIMIT 1");
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row && $row['valor']) {
|
||||
// Derivar nombre de archivo de la URL guardada
|
||||
$filename = basename(parse_url($row['valor'], PHP_URL_PATH));
|
||||
$filepath = $uploadDir . $filename;
|
||||
if ($filepath && is_file($filepath)) {
|
||||
@unlink($filepath);
|
||||
}
|
||||
}
|
||||
|
||||
// Borrar de la BD
|
||||
$pdo->prepare("DELETE FROM lab_config WHERE clave = 'turnero_tv_video'")->execute();
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Acción: subir ──────────────────────────────────────────────
|
||||
if (!isset($_FILES['video'])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se recibió ningún archivo']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['video'];
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$errores = [
|
||||
UPLOAD_ERR_INI_SIZE => 'El archivo supera upload_max_filesize en php.ini',
|
||||
UPLOAD_ERR_FORM_SIZE => 'El archivo supera MAX_FILE_SIZE del formulario',
|
||||
UPLOAD_ERR_PARTIAL => 'El archivo se subió parcialmente',
|
||||
UPLOAD_ERR_NO_FILE => 'No se seleccionó ningún archivo',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Falta la carpeta temporal',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Error al escribir el archivo',
|
||||
];
|
||||
echo json_encode(['ok' => false, 'error' => $errores[$file['error']] ?? 'Error de subida desconocido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validar tamaño (máx 500 MB)
|
||||
$maxBytes = 500 * 1024 * 1024;
|
||||
if ($file['size'] > $maxBytes) {
|
||||
echo json_encode(['ok' => false, 'error' => 'El video supera el límite de 500 MB']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validar tipo MIME (solo MP4 y WebM)
|
||||
$allowedMime = ['video/mp4', 'video/webm', 'video/ogg'];
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, $allowedMime, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Solo se permiten videos MP4 o WebM']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Definir extensión segura desde el MIME real
|
||||
$extMap = ['video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/ogg' => 'ogv'];
|
||||
$ext = $extMap[$mime];
|
||||
|
||||
// Borrar video anterior si existe
|
||||
foreach (['mp4', 'webm', 'ogv'] as $oldExt) {
|
||||
$oldPath = $uploadDir . 'tv_video.' . $oldExt;
|
||||
if (is_file($oldPath)) {
|
||||
@unlink($oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
$destFilename = 'tv_video.' . $ext;
|
||||
$destPath = $uploadDir . $destFilename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No se pudo guardar el archivo']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$publicUrl = $publicBase . $destFilename;
|
||||
|
||||
// Guardar URL en lab_config (upsert)
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO lab_config (clave, valor) VALUES ('turnero_tv_video', ?)
|
||||
ON DUPLICATE KEY UPDATE valor = VALUES(valor)"
|
||||
);
|
||||
$stmt->execute([$publicUrl]);
|
||||
|
||||
echo json_encode(['ok' => true, 'url' => $publicUrl]);
|
||||
@@ -103,6 +103,13 @@ try {
|
||||
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
|
||||
} catch (\Throwable $e) { $_loadErrors[] = 'lab_config: ' . $e->getMessage(); }
|
||||
|
||||
$tvMedia = [];
|
||||
try {
|
||||
$tvMedia = $pdo->query(
|
||||
"SELECT id, tipo, url, orden, duracion_segundos FROM turnero_tv_media ORDER BY orden ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_tv_media: ' . $e->getMessage(); }
|
||||
|
||||
$plantillasAprobadas = [];
|
||||
$plantillasHabilitadas = [];
|
||||
try {
|
||||
@@ -1140,36 +1147,51 @@ $tab = $_GET['tab'] ?? 'lugares';
|
||||
TAB 5 — PANTALLA TV
|
||||
════════════════════════════════════════ -->
|
||||
<?php elseif ($tab === 'tv'): ?>
|
||||
<?php $tvVideo = $cfg['turnero_tv_video'] ?? ''; ?>
|
||||
|
||||
<p class="section-title"><i class="fas fa-film me-1"></i>Video de fondo / publicidad</p>
|
||||
<p class="text-muted" style="font-size:.85rem">El video se reproduce en bucle y sin sonido como fondo en la pantalla TV del turnero. Formatos admitidos: MP4, WebM. Máximo 500 MB.</p>
|
||||
<p class="section-title"><i class="fas fa-film me-1"></i>Playlist de la Pantalla TV</p>
|
||||
<p class="text-muted" style="font-size:.85rem">Videos e imágenes se reproducen en bucle, uno detrás de otro, sin sonido, como fondo en la pantalla TV del turnero. Formatos: MP4, WebM, JPG, PNG, WEBP — máximo 500 MB por archivo.</p>
|
||||
|
||||
<!-- Vista previa actual -->
|
||||
<div id="tv-preview-wrap" class="mb-4" <?= $tvVideo ? '' : 'style="display:none"' ?>>
|
||||
<p class="fw-semibold small mb-2">Video actual:</p>
|
||||
<video id="tv-preview" controls muted
|
||||
style="max-width:480px;width:100%;border-radius:10px;border:1px solid #e2e8f0"
|
||||
src="<?= htmlspecialchars($tvVideo) ?>"></video>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvVideo()">
|
||||
<i class="fas fa-trash me-1"></i>Quitar video
|
||||
<!-- Lista actual -->
|
||||
<div id="tv-media-list" class="mb-3 d-flex flex-column gap-2">
|
||||
<?php foreach ($tvMedia as $m): ?>
|
||||
<div class="tv-media-item d-flex align-items-center gap-3 p-2" data-id="<?= (int)$m['id'] ?>"
|
||||
style="border:1px solid #e2e8f0;border-radius:10px">
|
||||
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
|
||||
<?php if ($m['tipo'] === 'video'): ?>
|
||||
<video muted style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="<?= htmlspecialchars($m['url']) ?>"></video>
|
||||
<?php else: ?>
|
||||
<img style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="<?= htmlspecialchars($m['url']) ?>" alt="">
|
||||
<?php endif; ?>
|
||||
<div class="flex-grow-1">
|
||||
<div class="small fw-semibold"><i class="fas fa-<?= $m['tipo']==='video'?'film':'image' ?> me-1"></i><?= $m['tipo']==='video'?'Video':'Imagen' ?></div>
|
||||
<div class="text-muted small text-truncate" style="max-width:300px"><?= htmlspecialchars(basename($m['url'])) ?></div>
|
||||
</div>
|
||||
<?php if ($m['tipo'] === 'imagen'): ?>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<input type="number" min="1" class="form-control form-control-sm tv-media-dur"
|
||||
style="width:70px" value="<?= (int)$m['duracion_segundos'] ?>">
|
||||
<span class="small text-muted">seg</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(<?= (int)$m['id'] ?>)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$tvMedia): ?>
|
||||
<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Zona de subida -->
|
||||
<div id="tv-upload-wrap" <?= $tvVideo ? 'style="display:none"' : '' ?>>
|
||||
<label class="drop-zone" id="tv-drop" for="tv-file-input"
|
||||
style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:.6rem;padding:2.5rem 1rem;border:2px dashed #cbd5e1;border-radius:12px;
|
||||
cursor:pointer;text-align:center;transition:border-color .2s">
|
||||
<i class="fas fa-cloud-upload-alt fa-2x text-primary"></i>
|
||||
<span class="fw-semibold text-primary">Haz clic o arrastra tu video aquí</span>
|
||||
<span class="text-muted small">MP4 / WebM — máx 500 MB</span>
|
||||
</label>
|
||||
<input type="file" id="tv-file-input" accept="video/mp4,video/webm" class="d-none">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="document.getElementById('tv-file-input').click()">
|
||||
<i class="fas fa-plus me-1"></i>Agregar video o imagen
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="guardarOrdenTv()">
|
||||
<i class="fas fa-save me-1"></i>Guardar orden / duración
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="tv-file-input" accept="video/mp4,video/webm,image/jpeg,image/png,image/webp" class="d-none" multiple>
|
||||
|
||||
<!-- Barra de progreso -->
|
||||
<div id="tv-progress-wrap" class="mt-3" style="display:none">
|
||||
@@ -1584,28 +1606,38 @@ async function cambiarSesion(accion, sesionId) {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// TAB 5: PANTALLA TV
|
||||
// TAB 5: PANTALLA TV — playlist de videos e imágenes
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
(function () {
|
||||
const inp = document.getElementById('tv-file-input');
|
||||
const drop = document.getElementById('tv-drop');
|
||||
const inp = document.getElementById('tv-file-input');
|
||||
if (!inp) return;
|
||||
inp.addEventListener('change', () => {
|
||||
Array.from(inp.files).forEach(subirTvMedia);
|
||||
inp.value = '';
|
||||
});
|
||||
|
||||
// Drag-and-drop visual
|
||||
drop.addEventListener('dragover', e => { e.preventDefault(); drop.style.borderColor = '#3b82f6'; });
|
||||
drop.addEventListener('dragleave', () => { drop.style.borderColor = '#cbd5e1'; });
|
||||
drop.addEventListener('drop', e => {
|
||||
e.preventDefault(); drop.style.borderColor = '#cbd5e1';
|
||||
const f = e.dataTransfer.files[0];
|
||||
if (f) subirTvVideo(f);
|
||||
// Reordenar arrastrando (drag & drop nativo, sin librerías)
|
||||
const list = document.getElementById('tv-media-list');
|
||||
let dragEl = null;
|
||||
list.addEventListener('dragstart', e => {
|
||||
dragEl = e.target.closest('.tv-media-item');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
list.querySelectorAll('.tv-media-item').forEach(it => it.setAttribute('draggable', 'true'));
|
||||
list.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
const target = e.target.closest('.tv-media-item');
|
||||
if (!target || target === dragEl) return;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const after = (e.clientY - rect.top) > rect.height / 2;
|
||||
list.insertBefore(dragEl, after ? target.nextSibling : target);
|
||||
});
|
||||
inp.addEventListener('change', () => { if (inp.files[0]) subirTvVideo(inp.files[0]); });
|
||||
})();
|
||||
|
||||
function subirTvVideo(file) {
|
||||
const allowedMime = ['video/mp4', 'video/webm', 'video/ogg'];
|
||||
if (!allowedMime.includes(file.type)) { toast('Solo se permiten MP4 o WebM', 'error'); return; }
|
||||
if (file.size > 500 * 1024 * 1024) { toast('El video supera el límite de 500 MB', 'error'); return; }
|
||||
function subirTvMedia(file) {
|
||||
const allowedMime = ['video/mp4', 'video/webm', 'video/ogg', 'image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!allowedMime.includes(file.type)) { toast(file.name + ': formato no admitido', 'error'); return; }
|
||||
if (file.size > 500 * 1024 * 1024) { toast(file.name + ': supera el límite de 500 MB', 'error'); return; }
|
||||
|
||||
const wrap = document.getElementById('tv-progress-wrap');
|
||||
const bar = document.getElementById('tv-progress-bar');
|
||||
@@ -1614,10 +1646,10 @@ function subirTvVideo(file) {
|
||||
wrap.style.display = '';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('video', file);
|
||||
fd.append('media', file);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', API + 'save_tv_video.php');
|
||||
xhr.open('POST', API + 'save_tv_media.php');
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (!e.lengthComputable) return;
|
||||
@@ -1631,11 +1663,8 @@ function subirTvVideo(file) {
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
if (!json.ok) { toast(json.error || 'Error al subir', 'error'); return; }
|
||||
toast('Video guardado correctamente');
|
||||
// Mostrar preview y ocultar zona de subida
|
||||
document.getElementById('tv-preview').src = json.url;
|
||||
document.getElementById('tv-preview-wrap').style.display = '';
|
||||
document.getElementById('tv-upload-wrap').style.display = 'none';
|
||||
toast(file.name + ' agregado a la playlist');
|
||||
agregarItemTvMedia(json.item);
|
||||
} catch(e) { toast('Respuesta inesperada del servidor', 'error'); }
|
||||
});
|
||||
|
||||
@@ -1646,19 +1675,66 @@ function subirTvVideo(file) {
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
async function borrarTvVideo() {
|
||||
if (!confirm('¿Quitar el video de la pantalla TV?')) return;
|
||||
function agregarItemTvMedia(item) {
|
||||
document.getElementById('tv-media-empty')?.remove();
|
||||
const list = document.getElementById('tv-media-list');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tv-media-item d-flex align-items-center gap-3 p-2';
|
||||
div.dataset.id = item.id;
|
||||
div.setAttribute('draggable', 'true');
|
||||
div.style.cssText = 'border:1px solid #e2e8f0;border-radius:10px';
|
||||
const preview = item.tipo === 'video'
|
||||
? `<video muted style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}"></video>`
|
||||
: `<img style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}" alt="">`;
|
||||
const durInput = item.tipo === 'imagen'
|
||||
? `<div class="d-flex align-items-center gap-1">
|
||||
<input type="number" min="1" class="form-control form-control-sm tv-media-dur" style="width:70px" value="${item.duracion_segundos}">
|
||||
<span class="small text-muted">seg</span>
|
||||
</div>` : '';
|
||||
div.innerHTML = `
|
||||
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
|
||||
${preview}
|
||||
<div class="flex-grow-1">
|
||||
<div class="small fw-semibold"><i class="fas fa-${item.tipo==='video'?'film':'image'} me-1"></i>${item.tipo==='video'?'Video':'Imagen'}</div>
|
||||
<div class="text-muted small text-truncate" style="max-width:300px">${item.url.split('/').pop()}</div>
|
||||
</div>
|
||||
${durInput}
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(${item.id})"><i class="fas fa-trash"></i></button>`;
|
||||
list.appendChild(div);
|
||||
}
|
||||
|
||||
async function borrarTvMedia(id) {
|
||||
if (!confirm('¿Quitar este elemento de la playlist?')) return;
|
||||
try {
|
||||
const res = await fetch(API + 'save_tv_video.php', {
|
||||
const res = await fetch(API + 'delete_tv_media.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accion: 'borrar' })
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
||||
toast('Video eliminado');
|
||||
document.getElementById('tv-preview-wrap').style.display = 'none';
|
||||
document.getElementById('tv-upload-wrap').style.display = '';
|
||||
document.getElementById('tv-preview').src = '';
|
||||
document.querySelector(`.tv-media-item[data-id="${id}"]`)?.remove();
|
||||
const list = document.getElementById('tv-media-list');
|
||||
if (!list.querySelector('.tv-media-item')) {
|
||||
list.innerHTML = '<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>';
|
||||
}
|
||||
toast('Elemento eliminado');
|
||||
} catch(e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
async function guardarOrdenTv() {
|
||||
const items = Array.from(document.querySelectorAll('.tv-media-item')).map(el => ({
|
||||
id: parseInt(el.dataset.id),
|
||||
duracion_segundos: parseInt(el.querySelector('.tv-media-dur')?.value) || 8,
|
||||
}));
|
||||
if (!items.length) { toast('No hay elementos para guardar', 'error'); return; }
|
||||
try {
|
||||
const res = await fetch(API + 'reorder_tv_media.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
||||
toast('Orden y duración guardados');
|
||||
} catch(e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,21 @@ $_dispCfg = [];
|
||||
try {
|
||||
$__pdo = Database::getInstance()->getConnection();
|
||||
$__rows = $__pdo->query(
|
||||
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color','turnero_tv_video')"
|
||||
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
|
||||
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$_dispCfg = $__rows ?: [];
|
||||
} catch (\Throwable $_) {}
|
||||
$_labNombre = htmlspecialchars($_dispCfg['empresa_nombre'] ?? 'Sistema de Turnos');
|
||||
$_labLogo = $_dispCfg['doc_logo_base64'] ?? '';
|
||||
$_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '') ? $_dispCfg['doc_color'] : '#1565c0';
|
||||
$_tvVideo = $_dispCfg['turnero_tv_video'] ?? '';
|
||||
if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; }
|
||||
$_hasVideo = (bool)$_tvVideo;
|
||||
|
||||
$_tvPlaylist = [];
|
||||
try {
|
||||
$_tvPlaylist = $__pdo->query(
|
||||
"SELECT tipo, url, duracion_segundos FROM turnero_tv_media WHERE activo = 1 ORDER BY orden ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $_) {}
|
||||
$_hasVideo = (bool)$_tvPlaylist;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
@@ -428,11 +433,7 @@ $_hasVideo = (bool)$_tvVideo;
|
||||
<div class="pac-nombre" id="pac-nombre"></div>
|
||||
</div>
|
||||
<div class="reel-wrap <?= $_hasVideo ? 'has-video' : '' ?>" id="reel-wrap">
|
||||
<div class="reel-inner">
|
||||
<video autoplay muted loop playsinline>
|
||||
<source src="<?= htmlspecialchars($_tvVideo) ?>">
|
||||
</video>
|
||||
</div>
|
||||
<div class="reel-inner" id="tv-playlist"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -456,6 +457,38 @@ $_hasVideo = (bool)$_tvVideo;
|
||||
<script>
|
||||
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||
|
||||
/* ── Playlist TV (videos e imágenes en bucle) ── */
|
||||
const TV_PLAYLIST = <?= json_encode($_tvPlaylist, JSON_UNESCAPED_UNICODE) ?>;
|
||||
(function () {
|
||||
const wrap = document.getElementById('tv-playlist');
|
||||
if (!wrap || !TV_PLAYLIST.length) return;
|
||||
let idx = 0, imgTimer = null;
|
||||
|
||||
function mostrar(i) {
|
||||
clearTimeout(imgTimer);
|
||||
wrap.innerHTML = '';
|
||||
const item = TV_PLAYLIST[i];
|
||||
if (item.tipo === 'video') {
|
||||
const v = document.createElement('video');
|
||||
v.src = item.url; v.autoplay = true; v.muted = true; v.playsInline = true;
|
||||
v.addEventListener('ended', siguiente);
|
||||
v.addEventListener('error', siguiente);
|
||||
wrap.appendChild(v);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = item.url;
|
||||
img.style.cssText = 'width:100%;height:100%;object-fit:cover';
|
||||
wrap.appendChild(img);
|
||||
imgTimer = setTimeout(siguiente, Math.max(1, item.duracion_segundos || 8) * 1000);
|
||||
}
|
||||
}
|
||||
function siguiente() {
|
||||
idx = (idx + 1) % TV_PLAYLIST.length;
|
||||
mostrar(idx);
|
||||
}
|
||||
mostrar(idx);
|
||||
})();
|
||||
|
||||
function tick() {
|
||||
const d = new Date();
|
||||
document.getElementById('reloj').textContent =
|
||||
|
||||
Reference in New Issue
Block a user