cambios de video
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
<?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]);
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
* TAB 2: Exámenes y Consentimientos
|
* TAB 2: Exámenes y Consentimientos
|
||||||
* TAB 3: Prioridades
|
* TAB 3: Prioridades
|
||||||
* TAB 4: Sesión y WhatsApp
|
* TAB 4: Sesión y WhatsApp
|
||||||
|
* TAB 5: Pantalla TV (video de fondo)
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
|
||||||
@@ -159,6 +160,11 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<i class="fas fa-calendar-day me-1"></i>Sesión y WhatsApp
|
<i class="fas fa-calendar-day me-1"></i>Sesión y WhatsApp
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $tab==='tv'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=tv">
|
||||||
|
<i class="fas fa-tv me-1"></i>Pantalla TV
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-card">
|
<div class="tab-card">
|
||||||
@@ -706,6 +712,55 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- 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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Barra de progreso -->
|
||||||
|
<div id="tv-progress-wrap" class="mt-3" style="display:none">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<span class="small fw-semibold" id="tv-progress-lbl">Subiendo…</span>
|
||||||
|
<span class="small text-muted" id="tv-progress-pct">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height:8px">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
||||||
|
id="tv-progress-bar" role="progressbar" style="width:0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
</div><!-- /tab-card -->
|
</div><!-- /tab-card -->
|
||||||
</div><!-- /content-wrap -->
|
</div><!-- /content-wrap -->
|
||||||
|
|
||||||
@@ -942,6 +997,85 @@ async function cambiarSesion(accion, sesionId) {
|
|||||||
} catch (e) { toast('Error de conexión', 'error'); }
|
} catch (e) { toast('Error de conexión', 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// TAB 5: PANTALLA TV
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
(function () {
|
||||||
|
const inp = document.getElementById('tv-file-input');
|
||||||
|
const drop = document.getElementById('tv-drop');
|
||||||
|
if (!inp) return;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
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; }
|
||||||
|
|
||||||
|
const wrap = document.getElementById('tv-progress-wrap');
|
||||||
|
const bar = document.getElementById('tv-progress-bar');
|
||||||
|
const pct = document.getElementById('tv-progress-pct');
|
||||||
|
const lbl = document.getElementById('tv-progress-lbl');
|
||||||
|
wrap.style.display = '';
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('video', file);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', API + 'save_tv_video.php');
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', e => {
|
||||||
|
if (!e.lengthComputable) return;
|
||||||
|
const p = Math.round(e.loaded / e.total * 100);
|
||||||
|
bar.style.width = p + '%';
|
||||||
|
pct.textContent = p + '%';
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
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';
|
||||||
|
} catch(e) { toast('Respuesta inesperada del servidor', 'error'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => { wrap.style.display = 'none'; toast('Error de conexión', 'error'); });
|
||||||
|
|
||||||
|
lbl.textContent = 'Subiendo ' + file.name + '…';
|
||||||
|
bar.style.width = '0%';
|
||||||
|
xhr.send(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function borrarTvVideo() {
|
||||||
|
if (!confirm('¿Quitar el video de la pantalla TV?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + 'save_tv_video.php', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accion: 'borrar' })
|
||||||
|
});
|
||||||
|
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 = '';
|
||||||
|
} catch(e) { toast('Error de conexión', 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
async function guardarWhatsApp() {
|
async function guardarWhatsApp() {
|
||||||
const template = document.getElementById('wa-template')?.value.trim();
|
const template = document.getElementById('wa-template')?.value.trim();
|
||||||
const lang = document.getElementById('wa-lang')?.value.trim();
|
const lang = document.getElementById('wa-lang')?.value.trim();
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ $_dispCfg = [];
|
|||||||
try {
|
try {
|
||||||
$__pdo = Database::getInstance()->getConnection();
|
$__pdo = Database::getInstance()->getConnection();
|
||||||
$__rows = $__pdo->query(
|
$__rows = $__pdo->query(
|
||||||
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
|
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color','turnero_tv_video')"
|
||||||
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
$_dispCfg = $__rows ?: [];
|
$_dispCfg = $__rows ?: [];
|
||||||
} catch (\Throwable $_) {}
|
} catch (\Throwable $_) {}
|
||||||
$_labNombre = htmlspecialchars($_dispCfg['empresa_nombre'] ?? 'Sistema de Turnos');
|
$_labNombre = htmlspecialchars($_dispCfg['empresa_nombre'] ?? 'Sistema de Turnos');
|
||||||
$_labLogo = $_dispCfg['doc_logo_base64'] ?? '';
|
$_labLogo = $_dispCfg['doc_logo_base64'] ?? '';
|
||||||
$_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '') ? $_dispCfg['doc_color'] : '#1565c0';
|
$_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '') ? $_dispCfg['doc_color'] : '#1565c0';
|
||||||
|
$_tvVideo = $_dispCfg['turnero_tv_video'] ?? '';
|
||||||
|
// Validar que sea una URL del mismo origen (solo rutas relativas o mismo dominio)
|
||||||
|
if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; }
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
@@ -303,6 +306,13 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
|
|||||||
@keyframes r-pop { 0%{transform:translate(-50%,-50%) scale(1);background:var(--pc,var(--brand));opacity:.38} 100%{transform:translate(-50%,-50%) scale(50);background:var(--pc,var(--brand));opacity:0} }
|
@keyframes r-pop { 0%{transform:translate(-50%,-50%) scale(1);background:var(--pc,var(--brand));opacity:.38} 100%{transform:translate(-50%,-50%) scale(50);background:var(--pc,var(--brand));opacity:0} }
|
||||||
/* Cola dark */
|
/* Cola dark */
|
||||||
.pg-cola { background: rgba(255,255,255,.022); border-top: 1px solid var(--border); }
|
.pg-cola { background: rgba(255,255,255,.022); border-top: 1px solid var(--border); }
|
||||||
|
/* Video de fondo TV */
|
||||||
|
.tv-bg-video {
|
||||||
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||||
|
object-fit: cover; opacity: .14; z-index: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.pg-body { position: relative; }
|
||||||
|
.pg-body > *:not(.tv-bg-video) { position: relative; z-index: 1; }
|
||||||
.cola-lbl { color: var(--brand); }
|
.cola-lbl { color: var(--brand); }
|
||||||
.cola-sep { width: 1px; height: 12px; background: var(--border); flex-shrink: 0; }
|
.cola-sep { width: 1px; height: 12px; background: var(--border); flex-shrink: 0; }
|
||||||
.cola-chip { background: rgba(255,255,255,.04); font-size: .68rem; }
|
.cola-chip { background: rgba(255,255,255,.04); font-size: .68rem; }
|
||||||
@@ -382,6 +392,11 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="pg-body">
|
<div class="pg-body">
|
||||||
|
<?php if ($_tvVideo): ?>
|
||||||
|
<video class="tv-bg-video" autoplay muted loop playsinline>
|
||||||
|
<source src="<?= htmlspecialchars($_tvVideo) ?>">
|
||||||
|
</video>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="area-col">
|
<div class="area-col">
|
||||||
<div class="col-hdr rec"><i class="fas fa-door-open"></i> Recepción</div>
|
<div class="col-hdr rec"><i class="fas fa-door-open"></i> Recepción</div>
|
||||||
<div class="slot-grid" id="rec-grid"></div>
|
<div class="slot-grid" id="rec-grid"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user