Initial commit: Proyecto Portal ePromos completo con documentación unificada y seguridad base

This commit is contained in:
Isaac Aracena
2026-05-09 12:46:24 -04:00
commit c44f1a69f1
65 changed files with 7223 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
RewriteEngine On
# Quitar .php de las URLs
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ $1.php [L]
+186
View File
@@ -0,0 +1,186 @@
<?php
// admin/cartas.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/memoria';
$PUBLIC_DIR = '/ruleta/uploads/memoria'; // AJUSTA si tu base no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name) { $name=preg_replace('/[^A-Za-z0-9._-]/','_',$name); return substr($name,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f=$_FILES[$field]; if($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field.");
$mime=(new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed=['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp']; if(!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido.");
if($f['size']>5*1024*1024) throw new RuntimeException("Archivo muy grande (5MB).");
$ext=$allowed[$mime]; $base=sanitize_filename(pathinfo($f['name'],PATHINFO_FILENAME)); $fname=$base.'-'.uniqid().'.'.$ext;
$dest=rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$fname; if(!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR,'/').'/'.$fname;
}
function all_juegos(mysqli $conn){
$res=$conn->query("SELECT juego_id, nombre FROM juegos_config ORDER BY juego_id ASC");
return $res->fetch_all(MYSQLI_ASSOC);
}
function get_carta(mysqli $conn,$id){
$stmt=$conn->prepare("SELECT * FROM cartas WHERE id=?"); $stmt->bind_param("i",$id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg=$err=''; $action=$_GET['action']??''; $id=(int)($_GET['id']??0);
$juegos=all_juegos($conn);
$juego_id=(int)($_GET['juego_id'] ?? ($juegos[0]['juego_id'] ?? 1));
// CREATE/UPDATE
if ($_SERVER['REQUEST_METHOD']==='POST') {
$cid=(int)($_POST['id']??0);
$juego_id=(int)($_POST['juego_id']??0);
$nombre=trim($_POST['nombre']??'');
$activo=isset($_POST['activo'])?1:0;
$old_front=$_POST['old_imagen_frontal']??'';
$old_back =$_POST['old_imagen_trasera']??'';
if ($juego_id<=0 || $nombre==='') {
$err="Selecciona juego y nombre.";
} else {
try{
$new_front=upload_image('imagen_frontal',$UPLOAD_DIR,$PUBLIC_DIR);
$new_back =upload_image('imagen_trasera',$UPLOAD_DIR,$PUBLIC_DIR);
$front=$new_front?:($old_front?:null);
$back =$new_back ?:($old_back ?:null);
if ($cid>0) {
$stmt=$conn->prepare("UPDATE cartas SET juego_id=?, nombre=?, imagen_frontal=?, imagen_trasera=?, activo=? WHERE id=?");
$stmt->bind_param("isssii",$juego_id,$nombre,$front,$back,$activo,$cid);
$stmt->execute(); $msg="Carta actualizada.";
} else {
if(!$back){ throw new RuntimeException("La imagen trasera es obligatoria."); }
$stmt=$conn->prepare("INSERT INTO cartas (juego_id,nombre,imagen_frontal,imagen_trasera,activo) VALUES (?,?,?,?,?)");
$stmt->bind_param("isssi",$juego_id,$nombre,$front,$back,$activo);
$stmt->execute(); $msg="Carta creada.";
}
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
// DELETE
if ($action==='delete' && $id>0) {
$stmt=$conn->prepare("DELETE FROM cartas WHERE id=?"); $stmt->bind_param("i",$id); $stmt->execute(); $msg="Carta eliminada.";
}
// EDIT
$edit=null;
if ($action==='edit' && $id>0) { $edit=get_carta($conn,$id); if($edit) $juego_id=(int)$edit['juego_id']; }
// LIST
$stmt=$conn->prepare("SELECT c.*, gc.nombre AS juego FROM cartas c LEFT JOIN juegos_config gc ON gc.juego_id=c.juego_id WHERE c.juego_id=? ORDER BY c.id DESC");
$stmt->bind_param("i",$juego_id); $stmt->execute();
$items=$stmt->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Cartas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex align-items-center justify-content-between">
<h3 class="mb-3">Cartas</h3>
<form class="d-flex" method="get">
<select name="juego_id" class="form-select me-2" onchange="this.form.submit()">
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= $juego_id===$j['juego_id']?'selected':'' ?>>
<?= htmlspecialchars($j['nombre']) ?> (ID <?= (int)$j['juego_id'] ?>)
</option>
<?php endforeach; ?>
</select>
<noscript><button class="btn btn-primary">Cambiar</button></noscript>
</form>
</div>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar carta' : 'Nueva carta' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<input type="hidden" name="old_imagen_frontal" value="<?= htmlspecialchars($edit['imagen_frontal'] ?? '') ?>">
<input type="hidden" name="old_imagen_trasera" value="<?= htmlspecialchars($edit['imagen_trasera'] ?? '') ?>">
<div class="col-md-3">
<label class="form-label">Juego</label>
<select name="juego_id" class="form-select" required>
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= ($juego_id===$j['juego_id'])?'selected':'' ?>><?= htmlspecialchars($j['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Imagen trasera (obligatoria)</label>
<input type="file" name="imagen_trasera" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_trasera'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_trasera']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-3">
<label class="form-label">Imagen frontal (opcional)</label>
<input type="file" name="imagen_frontal" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_frontal'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_frontal']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-2">
<label class="form-label">Activo</label><br>
<input type="checkbox" name="activo" value="1" <?= !empty($edit['activo'])?'checked':'' ?>>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="cartas.php?juego_id=<?= (int)$juego_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>ID</th><th>Juego</th><th>Nombre</th><th>Trasera</th><th>Frontal</th><th>Activo</th><th>Acciones</th></tr></thead>
<tbody>
<?php foreach($items as $it): ?>
<tr>
<td><?= (int)$it['id'] ?></td>
<td><?= htmlspecialchars($it['juego'].' ('.$it['juego_id'].')') ?></td>
<td><?= htmlspecialchars($it['nombre']) ?></td>
<td><?= $it['imagen_trasera']?'<img class="thumb" src="'.htmlspecialchars($it['imagen_trasera']).'">':'—' ?></td>
<td><?= $it['imagen_frontal']?'<img class="thumb" src="'.htmlspecialchars($it['imagen_frontal']).'">':'—' ?></td>
<td><?= $it['activo']?'Sí':'No' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="cartas.php?action=edit&id=<?= (int)$it['id'] ?>&juego_id=<?= (int)$juego_id ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="cartas.php?action=delete&id=<?= (int)$it['id'] ?>&juego_id=<?= (int)$juego_id ?>" onclick="return confirm('¿Eliminar carta?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?><tr><td colspan="7" class="text-center text-muted">Sin cartas</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
session_destroy();
header('Location: index.php');
exit;
}
if (empty($_SESSION['admin_id'])) {
header('Location: index.php');
exit;
}
$total_ruletas = (int)$conn->query("SELECT COUNT(*) c FROM ruletas")->fetch_assoc()['c'];
$total_premios = (int)$conn->query("SELECT COUNT(*) c FROM premios")->fetch_assoc()['c'];
$total_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas")->fetch_assoc()['c'];
$ultimas = $conn->query("
SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address
FROM jugadas j
LEFT JOIN ruletas r ON r.id = j.ruleta_id
LEFT JOIN premios p ON p.id = j.premio_id
ORDER BY j.id DESC
LIMIT 10
");
// --- contadores para Memoria ---
$total_mem_juegos = (int)$conn->query("SELECT COUNT(*) c FROM juegos_config")->fetch_assoc()['c'];
$total_mem_cartas = (int)$conn->query("SELECT COUNT(*) c FROM cartas WHERE activo=1")->fetch_assoc()['c'];
$total_mem_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas_memoria")->fetch_assoc()['c'];
// últimas 10 jugadas de memoria
$ult_mem = $conn->query("
SELECT jm.id, jm.fecha_juego, jm.juego_id, jm.tiempo_ms, jm.aciertos, gc.nombre AS juego
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id = jm.juego_id
ORDER BY jm.id DESC
LIMIT 10
");
// --- contadores para Rasca y Gana ---
$total_rasca_juegos = (int)$conn->query("SELECT COUNT(*) c FROM rasca_config")->fetch_assoc()['c'];
$total_rasca_premios = (int)$conn->query("SELECT COUNT(*) c FROM rasca_premios WHERE activo=1")->fetch_assoc()['c'];
$total_rasca_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas_rasca")->fetch_assoc()['c'];
// últimas 10 jugadas de Rasca
$ult_rasca = $conn->query("
SELECT jr.id,
jr.fecha_juego,
rc.nombre AS rasca,
COALESCE(rp.nombre, '—') AS premio,
jr.ip_address AS ip
FROM jugadas_rasca jr
LEFT JOIN rasca_config rc ON rc.rasca_id = jr.rasca_id
LEFT JOIN rasca_premios rp ON rp.id = jr.premio_id
ORDER BY jr.id DESC
LIMIT 10
");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Dashboard | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Home</a>
<div class="d-flex">
<span class="navbar-text me-3">
<?= htmlspecialchars($_SESSION['admin_nombre'] ?? $_SESSION['admin_email']) ?>
</span>
<a href="?action=logout" class="btn btn-outline-light btn-sm">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php include __DIR__ . '/dashboard_landing_widgets_adv.php'; ?>
<h5 class="mt-4">Juegos Disponibles</h5>
<div class="row g-3">
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Ruletas</h6>
<div class="display-6"><?= $total_ruletas ?></div>
<a href="ruletas.php" class="btn btn-sm btn-primary mt-2">Gestionar ruletas</a>
</div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Premios</h6>
<div class="display-6"><?= $total_premios ?></div>
<a href="premios.php" class="btn btn-sm btn-primary mt-2">Gestionar premios</a>
</div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas</h6>
<div class="display-6"><?= $total_jugadas ?></div>
<a href="jugadas.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div></div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Juegos de Memoria</h6>
<div class="display-6"><?= $total_mem_juegos ?></div>
<a href="juegos_config.php" class="btn btn-sm btn-primary mt-2">Configurar juegos</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Cartas activas</h6>
<div class="display-6"><?= $total_mem_cartas ?></div>
<a href="cartas.php" class="btn btn-sm btn-primary mt-2">Gestionar cartas</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas Memoria</h6>
<div class="display-6"><?= $total_mem_jugadas ?></div>
<a href="jugadas_memoria.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Juegos de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_juegos ?></div>
<a href="rasca_config.php" class="btn btn-sm btn-primary mt-2">Configurar juegos</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Premios de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_premios ?></div>
<a href="rasca_premios.php" class="btn btn-sm btn-primary mt-2">Gestionar premios</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_jugadas ?></div>
<a href="rasca_jugadas.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div>
</div>
</div>
<h5 class="mt-4">Últimas 10 jugadas</h5>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>ID</th><th>Fecha</th><th>Ruleta</th><th>Premio</th><th>IP</th></tr></thead>
<tbody>
<?php while($row = $ultimas->fetch_assoc()): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['ruleta'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip_address'] ?? '—') ?></td>
</tr>
<?php endwhile; ?>
<?php if ($ultimas->num_rows === 0): ?>
<tr><td colspan="5" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<h5 class="mt-4">Últimas 10 jugadas — Memoria</h5>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr><th>ID</th><th>Fecha</th><th>Juego</th><th>Juego ID</th><th>Aciertos</th><th>Tiempo (s)</th></tr>
</thead>
<tbody>
<?php if ($ult_mem && $ult_mem->num_rows): ?>
<?php while($m = $ult_mem->fetch_assoc()): ?>
<tr>
<td><?= (int)$m['id'] ?></td>
<td><?= htmlspecialchars($m['fecha_juego']) ?></td>
<td><?= htmlspecialchars($m['juego'] ?? '—') ?></td>
<td><?= (int)$m['juego_id'] ?></td>
<td><?= (int)$m['aciertos'] ?></td>
<td><?= number_format(($m['tiempo_ms'] ?? 0)/1000, 2) ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<tr><td colspan="6" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="card shadow-sm mt-4">
<div class="card-body table-responsive">
<h5 class="mb-3">Últimas 10 jugadas (Rasca)</h5>
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th>
<th>Fecha</th>
<th>Rasca</th>
<th>Premio</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<?php while($row = $ult_rasca->fetch_assoc()): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['rasca'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip'] ?? '') ?></td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
+171
View File
@@ -0,0 +1,171 @@
<?php
// ruleta/admin/dashboard_landing_widget_data.php
header('Content-Type: application/json; charset=utf-8');
// No HTML en errores:
ini_set('display_errors', '0');
ini_set('log_errors', '1');
// Captura warnings/notices para adjuntarlos al JSON:
$php_warnings = [];
set_error_handler(function($errno, $errstr, $errfile, $errline) use (&$php_warnings) {
if (error_reporting() === 0) return true;
$php_warnings[] = "$errstr in $errfile:$errline";
return true;
});
// Limpia cualquier output previo
while (ob_get_level() > 0) { ob_end_clean(); }
ob_start();
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) {
http_response_code(401);
echo json_encode(['ok'=>false, 'error'=>'auth']);
exit;
}
// Helper seguro para prepares
function must_prepare(mysqli $conn, string $sql): mysqli_stmt {
$st = $conn->prepare($sql);
if (!$st) {
throw new RuntimeException('prepare_failed: '. $conn->error .' | SQL='. preg_replace('/\s+/', ' ', $sql));
}
return $st;
}
function qcount(mysqli $conn, string $sql): int {
$rs = $conn->query($sql);
if (!$rs) throw new RuntimeException('query_failed: '. $conn->error .' | SQL='. preg_replace('/\s+/', ' ', $sql));
$row = $rs->fetch_assoc();
return (int)($row['c'] ?? 0);
}
try {
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
$landing_id = (int)($_GET['landing_id'] ?? 0);
$want_meta = (int)($_GET['meta'] ?? 0);
if ($from === '' || $to === '') {
$to = date('Y-m-d');
$from = date('Y-m-d', strtotime($to.' -30 day'));
}
$from_dt = $from.' 00:00:00';
$to_dt = $to.' 23:59:59';
// --- KPIs
$kpis = [
'landings' => qcount($conn, "SELECT COUNT(*) c FROM landing_pages"),
'variants' => qcount($conn, "SELECT COUNT(*) c FROM landing_variants"),
'blocks' => qcount($conn, "SELECT COUNT(*) c FROM landing_blocks"),
'leads_window' => 0,
'range_label' => date('d M', strtotime($from)).' - '.date('d M', strtotime($to)),
'filtered' => $landing_id > 0
];
if ($landing_id > 0) {
$st = must_prepare($conn, "SELECT COUNT(*) c FROM landing_forms WHERE landing_id=? AND created_at BETWEEN ? AND ?");
$st->bind_param('iss', $landing_id, $from_dt, $to_dt);
} else {
$st = must_prepare($conn, "SELECT COUNT(*) c FROM landing_forms WHERE created_at BETWEEN ? AND ?");
$st->bind_param('ss', $from_dt, $to_dt);
}
$st->execute();
$r = $st->get_result()->fetch_assoc();
$kpis['leads_window'] = (int)($r['c'] ?? 0);
// --- Top + series
$top = [];
if ($landing_id > 0) {
$st = must_prepare($conn, "
SELECT lp.id AS landing_id, lp.titulo,
SUM(lvs.event_type='impression') AS imps,
SUM(lvs.event_type='conversion') AS conv,
ROUND(SUM(lvs.event_type='conversion')/NULLIF(SUM(lvs.event_type='impression'),0)*100,2) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id=lp.id
AND lvs.created_at BETWEEN ? AND ?
WHERE lp.id=?
GROUP BY lp.id, lp.titulo
");
$st->bind_param('ssi', $from_dt, $to_dt, $landing_id);
} else {
// --- Top global (landing_id = 0): usar subquery para poder ordenar por 'cr'
$st = must_prepare($conn, "
SELECT landing_id, titulo, imps, conv, cr
FROM (
SELECT
lp.id AS landing_id,
lp.titulo,
SUM(lvs.event_type='impression') AS imps,
SUM(lvs.event_type='conversion') AS conv,
ROUND(
SUM(lvs.event_type='conversion') / NULLIF(SUM(lvs.event_type='impression'), 0) * 100, 2
) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id = lp.id
AND lvs.created_at BETWEEN ? AND ?
GROUP BY lp.id, lp.titulo
) t
ORDER BY (cr IS NULL), cr DESC, imps DESC
LIMIT 10
");
$st->bind_param('ss', $from_dt, $to_dt);
}
$st->execute();
$rs = $st->get_result();
while($row = $rs->fetch_assoc()){
$row['imps'] = (int)($row['imps'] ?? 0);
$row['conv'] = (int)($row['conv'] ?? 0);
$row['cr'] = $row['imps'] ? (float)$row['cr'] : null;
// Serie diaria
$st2 = must_prepare($conn, "
SELECT DATE(created_at) d,
SUM(event_type='impression') imp,
SUM(event_type='conversion') conv
FROM landing_variant_stats
WHERE landing_id=? AND created_at BETWEEN ? AND ?
GROUP BY DATE(created_at)
ORDER BY d ASC
");
$lid = (int)$row['landing_id'];
$st2->bind_param('iss', $lid, $from_dt, $to_dt);
$st2->execute();
$ser = $st2->get_result()->fetch_all(MYSQLI_ASSOC);
$row['series'] = array_map(function($p){
return ['d'=>$p['d'], 'imp'=>(int)$p['imp'], 'conv'=>(int)$p['conv']];
}, $ser);
$top[] = $row;
}
// Lista para el select (si meta=1)
$landings_list = [];
if ($want_meta === 1) {
$q = $conn->query("SELECT id, titulo FROM landing_pages ORDER BY titulo ASC");
if (!$q) throw new RuntimeException('query_failed: '. $conn->error .' | SQL=SELECT id,titulo FROM landing_pages ORDER BY titulo ASC');
while($L = $q->fetch_assoc()){
$landings_list[] = ['id'=>(int)$L['id'], 'titulo'=>$L['titulo']];
}
}
$warnings = $php_warnings;
ob_end_clean();
echo json_encode([
'ok'=>true,
'kpis'=>$kpis,
'top'=>$top,
'landings_list'=>$landings_list,
'warnings'=>$warnings
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
ob_end_clean();
http_response_code(500);
echo json_encode(['ok'=>false, 'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
}
+172
View File
@@ -0,0 +1,172 @@
<?php
// ruleta/admin/dashboard_landing_widgets.php
if (!isset($conn)) { require_once __DIR__ . '/db.php'; }
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
// ---- Totales generales módulo landing ----
$tot_landings = (int)$conn->query("SELECT COUNT(*) c FROM landing_pages")->fetch_assoc()['c'];
$tot_variants = (int)$conn->query("SELECT COUNT(*) c FROM landing_variants")->fetch_assoc()['c'];
$tot_blocks = (int)$conn->query("SELECT COUNT(*) c FROM landing_blocks")->fetch_assoc()['c'];
$tot_leads_30d = (int)$conn->query("SELECT COUNT(*) c FROM landing_forms WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)")->fetch_assoc()['c'];
// ---- Top landings por CR últimos 30 días (impresiones/conversiones) ----
$top = [];
$st = $conn->prepare("
SELECT
lp.id AS landing_id, lp.titulo,
SUM(lvs.event_type='impression') AS impresiones,
SUM(lvs.event_type='conversion') AS conversiones,
ROUND(SUM(lvs.event_type='conversion')/NULLIF(SUM(lvs.event_type='impression'),0)*100,2) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id = lp.id
AND lvs.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY lp.id, lp.titulo
ORDER BY cr DESC IS NULL, cr DESC, impresiones DESC
LIMIT 10
");
$st->execute();
$top_rs = $st->get_result();
while($r = $top_rs->fetch_assoc()){ $top[] = $r; }
// ---- Listado rápido de landings con accesos (últimos 8) ----
$list = $conn->query("
SELECT id, titulo, slug, activo, DATE_FORMAT(fecha_creacion,'%Y-%m-%d %H:%i') AS fc
FROM landing_pages
ORDER BY id DESC
LIMIT 8
");
?>
<div class="container-fluid px-0">
<!-- Resumen de tarjetas -->
<div class="row g-3">
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Landings</h6><h3 class="mb-0"><?= number_format($tot_landings) ?></h3></div>
<div class="fs-3">🧭</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Variantes A/B</h6><h3 class="mb-0"><?= number_format($tot_variants) ?></h3></div>
<div class="fs-3">🧪</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Bloques</h6><h3 class="mb-0"><?= number_format($tot_blocks) ?></h3></div>
<div class="fs-3">🧱</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Leads (30d)</h6><h3 class="mb-0"><?= number_format($tot_leads_30d) ?></h3></div>
<div class="fs-3">📈</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla: Top 10 por CR (30d) -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Top CR por Landing (30 días)</h5>
<a href="landing.php" class="btn btn-sm btn-outline-primary">Ver todas</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Landing</th>
<th class="text-end">Impresiones</th>
<th class="text-end">Conversiones</th>
<th class="text-end">CR %</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php if($top): foreach($top as $r): ?>
<tr>
<td class="fw-semibold"><?= htmlspecialchars($r['titulo']) ?></td>
<td class="text-end"><?= (int)$r['impresiones'] ?></td>
<td class="text-end"><?= (int)$r['conversiones'] ?></td>
<td class="text-end"><?= $r['cr'] !== null ? number_format((float)$r['cr'],2) : '—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="landing_stats.php?landing_id=<?= (int)$r['landing_id'] ?>">Stats</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_variants.php?landing_id=<?= (int)$r['landing_id'] ?>">Variantes</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_builder.php?landing_id=<?= (int)$r['landing_id'] ?>">Builder</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_crm.php?landing_id=<?= (int)$r['landing_id'] ?>">CRM</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_forms.php?landing_id=<?= (int)$r['landing_id'] ?>">Leads</a>
</td>
</tr>
<?php endforeach; else: ?>
<tr><td colspan="5" class="text-center text-muted">Sin datos</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Últimas landings creadas (accesos rápidos) -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Accesos rápidos</h5>
<a href="landing.php" class="btn btn-sm btn-primary">Nueva landing</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Landing</th>
<th>Slug</th>
<th>Creada</th>
<th>Estado</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php if($list && $list->num_rows): while($r=$list->fetch_assoc()): ?>
<tr>
<td class="fw-semibold"><?= htmlspecialchars($r['titulo']) ?></td>
<td><code><?= htmlspecialchars($r['slug']) ?></code></td>
<td><?= htmlspecialchars($r['fc']) ?></td>
<td><?= $r['activo'] ? '<span class="badge text-bg-success">Activo</span>' : '<span class="badge text-bg-secondary">Inactivo</span>' ?></td>
<td class="text-nowrap d-flex gap-1">
<a class="btn btn-sm btn-outline-primary" href="landing_variants.php?landing_id=<?= (int)$r['id'] ?>">Variantes</a>
<a class="btn btn-sm btn-outline-primary" href="landing_schedule.php?landing_id=<?= (int)$r['id'] ?>">Schedule</a>
<a class="btn btn-sm btn-outline-primary" href="landing_page_blocks.php?landing_id=<?= (int)$r['id'] ?>">Bloques</a>
<a class="btn btn-sm btn-outline-primary" href="landing_builder.php?landing_id=<?= (int)$r['id'] ?>">Builder</a>
<a class="btn btn-sm btn-outline-primary" href="landing_stats.php?landing_id=<?= (int)$r['id'] ?>">Stats</a>
<a class="btn btn-sm btn-outline-primary" href="landing_crm.php?landing_id=<?= (int)$r['id'] ?>">CRM</a>
<a class="btn btn-sm btn-outline-primary" href="landing_forms.php?landing_id=<?= (int)$r['id'] ?>">Leads</a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" href="../landing.php?slug=<?= urlencode($r['slug']) ?>">Ver</a>
</td>
</tr>
<?php endwhile; else: ?>
<tr><td colspan="5" class="text-center text-muted">Aún no hay landings</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
+211
View File
@@ -0,0 +1,211 @@
<?php
// ruleta/admin/dashboard_landing_widgets_adv.php
if (!isset($conn)) { require_once __DIR__ . '/db.php'; }
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
?>
<div class="container-fluid px-0" id="landing-widget">
<div class="d-flex flex-wrap align-items-end gap-2 mb-2">
<h4 class="mb-0">Landings</h4>
<div class="ms-auto d-flex flex-wrap gap-2">
<div>
<label class="form-label mb-0 small">Landing</label>
<select id="lw-landing" class="form-select form-select-sm" style="min-width:220px">
<option value="0">— Todas las landings —</option>
</select>
</div>
<div>
<label class="form-label mb-0 small">Desde</label>
<input type="date" id="lw-from" class="form-control form-control-sm" />
</div>
<div>
<label class="form-label mb-0 small">Hasta</label>
<input type="date" id="lw-to" class="form-control form-control-sm" />
</div>
<button id="lw-refresh" class="btn btn-sm btn-primary">Actualizar</button>
</div>
</div>
<!-- KPIs -->
<div class="row g-3" id="lw-kpis">
<!-- Se llena por JS -->
</div>
<!-- Top por CR -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Top CR por Landing</h5>
<a href="landing.php" class="btn btn-sm btn-outline-primary">Ver todas</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="lw-top-table">
<thead>
<tr>
<th>Landing</th>
<th class="text-end">Impresiones</th>
<th class="text-end">Conversiones</th>
<th class="text-end">CR %</th>
<th style="width:180px">Tendencia</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<!-- Se llena por JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const $ = (s) => document.querySelector(s);
const API = 'dashboard_landing_widget_data.php';
function fmt(n){ return new Intl.NumberFormat().format(n||0); }
function esc(s){ const d=document.createElement('div'); d.textContent=(s??''); return d.innerHTML; }
function defaultDates(){
const to = new Date();
const from = new Date(); from.setDate(to.getDate()-30);
const f = (d)=>d.toISOString().slice(0,10);
$('#lw-from').value = f(from);
$('#lw-to').value = f(to);
}
async function fetchData(includeList=false){
const params = new URLSearchParams({
from: $('#lw-from').value || '',
to: $('#lw-to').value || '',
landing_id: $('#lw-landing').value || '0',
meta: includeList ? '1' : '0'
});
const res = await fetch(API + '?' + params.toString(), {credentials:'same-origin'});
const txt = await res.text();
try { return JSON.parse(txt); }
catch(e){ console.error('Respuesta no JSON:', txt); throw e; }
}
function renderLandingOptions(list){
const sel = $('#lw-landing');
const cur = sel.value || '0';
sel.innerHTML = '<option value="0">— Todas las landings —</option>';
(list||[]).forEach(r=>{
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = r.titulo + ' (#'+ r.id +')';
sel.appendChild(opt);
});
// mantener selección si existía
if ([...sel.options].some(o=>o.value===cur)) sel.value = cur;
}
function renderKPIs(k){
$('#lw-kpis').innerHTML = `
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Landings</h6><h3 class="mb-0">${fmt(k.landings)}</h3></div><div class="fs-3">🧭</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Variantes</h6><h3 class="mb-0">${fmt(k.variants)}</h3></div><div class="fs-3">🧪</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Bloques</h6><h3 class="mb-0">${fmt(k.blocks)}</h3></div><div class="fs-3">🧱</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Leads ${k.filtered ? '(landing seleccionada)' : '(rango)'}<br><small class="text-muted">${k.range_label}</small></h6>
<h3 class="mb-0">${fmt(k.leads_window)}</h3></div><div class="fs-3">📈</div></div></div></div>
`;
}
function makeSparkline(canvas, series){
const labels = series.map(p=>p.d.slice(5)); // MM-DD
const imps = series.map(p=>p.imp);
const conv = series.map(p=>p.conv);
new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{ label:'Imps', data: imps, tension: .3, pointRadius: 0 },
{ label:'Conv', data: conv, tension: .3, pointRadius: 0, yAxisID:'y1' }
]
},
options:{
responsive:true, maintainAspectRatio:false,
scales: {
x: { display:false },
y: { display:false, beginAtZero:true },
y1:{ display:false, beginAtZero:true, position:'right' }
},
plugins: { legend:{display:false}, tooltip:{enabled:true} }
}
});
}
function renderTop(rows){
const tbody = $('#lw-top-table tbody');
tbody.innerHTML = '';
rows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="fw-semibold">${esc(r.titulo)}</td>
<td class="text-end">${fmt(r.imps)}</td>
<td class="text-end">${fmt(r.conv)}</td>
<td class="text-end">${r.cr!==null ? (r.cr.toFixed(2)) : '—'}</td>
<td><div style="height:38px"><canvas id="spk-${r.landing_id}"></canvas></div></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="landing_stats.php?landing_id=${r.landing_id}">Stats</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_variants.php?landing_id=${r.landing_id}">Variantes</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_builder.php?landing_id=${r.landing_id}">Builder</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_crm.php?landing_id=${r.landing_id}">CRM</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_forms.php?landing_id=${r.landing_id}">Leads</a>
</td>
`;
tbody.appendChild(tr);
// sparkline
const cv = document.getElementById(`spk-${r.landing_id}`);
if (cv && r.series && r.series.length) makeSparkline(cv, r.series);
});
if (!rows.length){
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted">Sin datos</td></tr>`;
}
}
async function boot(){
defaultDates();
// Primer fetch: pedir datos + lista de landings
try {
const data = await fetchData(true);
if (data.landings_list) renderLandingOptions(data.landings_list);
renderKPIs(data.kpis);
renderTop(data.top);
} catch(e) {
console.error(e); alert('No se pudo cargar el widget de landings.');
}
}
async function refresh(){
$('#lw-refresh').disabled = true;
try{
const data = await fetchData(false);
renderKPIs(data.kpis);
renderTop(data.top);
}catch(e){
console.error(e);
alert('No se pudo cargar el widget de landings.');
}finally{
$('#lw-refresh').disabled = false;
}
}
// eventos
$('#lw-refresh').addEventListener('click', refresh);
$('#lw-landing').addEventListener('change', refresh);
// iniciar
boot();
})();
</script>
+15
View File
@@ -0,0 +1,15 @@
<?php
// admin/db.php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$DB_HOST = 'localhost';
$DB_USER = 'root';
$DB_PASS = '';
$DB_NAME = 'ruleta_db';
try {
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
$conn->set_charset('utf8mb4');
} catch (Throwable $e) {
die('Fallo conectando a MySQL: ' . $e->getMessage());
}
+94
View File
@@ -0,0 +1,94 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (!empty($_SESSION['admin_id'])) {
header('Location: dashboard.php');
exit;
}
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $conn->prepare(
"SELECT id, email, nombre, password_hash
FROM admin_users
WHERE email = ?
LIMIT 1"
);
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows === 1) {
$stmt->bind_result($id, $email_db, $nombre, $password_hash);
$stmt->fetch();
$ok = false;
$pareceHash = (strlen((string)$password_hash) >= 20 && substr((string)$password_hash,0,1) === '$');
if ($pareceHash) {
$ok = password_verify($password, $password_hash);
} else {
$ok = hash_equals((string)$password_hash, (string)$password);
if ($ok) {
$nuevoHash = password_hash($password, PASSWORD_DEFAULT);
$up = $conn->prepare("UPDATE admin_users SET password_hash=? WHERE id=?");
$up->bind_param("si", $nuevoHash, $id);
$up->execute();
$up->close();
}
}
if ($ok) {
$_SESSION['admin_id'] = (int)$id;
$_SESSION['admin_email'] = $email_db;
$_SESSION['admin_nombre'] = $nombre;
header('Location: dashboard.php');
exit;
}
}
$stmt->close();
$err = 'Credenciales inválidas';
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Login | Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center" style="min-height:100vh;">
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-10 col-md-6 col-lg-4">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="h4 mb-3 text-center">Admin Promo Hub</h1>
<?php if ($err): ?>
<div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div>
<?php endif; ?>
<form method="post" autocomplete="off">
<div class="mb-3">
<label class="form-label">Correo</label>
<input type="email" name="email" class="form-control" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Contraseña</label>
<input type="password" name="password" class="form-control" required>
</div>
<button class="btn btn-primary w-100">Ingresar</button>
</form>
</div>
</div>
<p class="text-center mt-3 text-muted small">© <?= date('Y') ?> GLM</p>
</div>
</div>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<?php
// admin/juegos_config.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/memoria'; // físico
$PUBLIC_DIR = '/ruleta/uploads/memoria'; // público: AJUSTA si tu base no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name) {
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
return substr($name, 0, 180);
}
function upload_image($field, $UPLOAD_DIR, $PUBLIC_DIR) {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido para $field. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5 * 1024 * 1024) throw new RuntimeException("Archivo muy grande para $field (máx 5MB).");
$ext = $allowed[$mime];
$base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$fname = $base . '-' . uniqid() . '.' . $ext;
$dest = rtrim($UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR . $fname;
if (!move_uploaded_file($f['tmp_name'], $dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR, '/') . '/' . $fname;
}
function get_config(mysqli $conn, $juego_id) {
$stmt = $conn->prepare("SELECT * FROM juegos_config WHERE juego_id=?");
$stmt->bind_param("i", $juego_id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg = $err = '';
$action = $_GET['action'] ?? '';
$juego_id = (int)($_GET['juego_id'] ?? 0);
// CREATE/UPDATE
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$jid = (int)($_POST['juego_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$ruleta_id = (int)($_POST['ruleta_id'] ?? 0);
$olds = [
'banner_1' => $_POST['old_banner_1'] ?? '',
'banner_2' => $_POST['old_banner_2'] ?? '',
'banner_3' => $_POST['old_banner_3'] ?? '',
'banner_4' => $_POST['old_banner_4'] ?? '',
'background_image' => $_POST['old_background_image'] ?? '',
'fullscreen_icon' => $_POST['old_fullscreen_icon'] ?? '',
'reverso_default' => $_POST['old_reverso_default'] ?? ''
];
if ($jid <= 0 || $nombre === '') {
$err = "Debes indicar un juego_id (entero) y un nombre.";
} else {
try {
$new = [
'banner_1' => upload_image('banner_1', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_2' => upload_image('banner_2', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_3' => upload_image('banner_3', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_4' => upload_image('banner_4', $UPLOAD_DIR, $PUBLIC_DIR),
'background_image' => upload_image('background_image', $UPLOAD_DIR, $PUBLIC_DIR),
'fullscreen_icon' => upload_image('fullscreen_icon', $UPLOAD_DIR, $PUBLIC_DIR),
'reverso_default' => upload_image('reverso_default', $UPLOAD_DIR, $PUBLIC_DIR),
];
foreach ($new as $k=>$v) { $new[$k] = $v ?: ($olds[$k] ?: null); }
// upsert por juego_id
$stmt = $conn->prepare("INSERT INTO juegos_config
(juego_id, nombre, ruleta_id, banner_1,banner_2,banner_3,banner_4, background_image, fullscreen_icon, reverso_default)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), ruleta_id=VALUES(ruleta_id),
banner_1=VALUES(banner_1), banner_2=VALUES(banner_2), banner_3=VALUES(banner_3), banner_4=VALUES(banner_4),
background_image=VALUES(background_image), fullscreen_icon=VALUES(fullscreen_icon), reverso_default=VALUES(reverso_default)");
$stmt->bind_param("isisssssss", $jid, $nombre, $ruleta_id,
$new['banner_1'], $new['banner_2'], $new['banner_3'], $new['banner_4'],
$new['background_image'], $new['fullscreen_icon'], $new['reverso_default']);
$stmt->execute();
$msg = "Configuración guardada.";
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
// DELETE
if ($action === 'delete' && $juego_id > 0) {
$stmt = $conn->prepare("DELETE FROM juegos_config WHERE juego_id=?");
$stmt->bind_param("i", $juego_id); $stmt->execute();
$msg = "Configuración eliminada.";
}
// EDIT
$edit = null;
if ($action === 'edit' && $juego_id > 0) {
$edit = get_config($conn, $juego_id);
}
// LIST
$list = $conn->query("SELECT * FROM juegos_config ORDER BY juego_id ASC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Configuración de Juegos</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Config. Juegos de Memoria</h3>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar juego' : 'Nuevo/Actualizar juego (por juego_id)' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<div class="col-md-2">
<label class="form-label">Juego ID</label>
<input type="number" name="juego_id" class="form-control" required value="<?= htmlspecialchars($edit['juego_id'] ?? '') ?>">
</div>
<div class="col-md-5">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<?php
// hidden olds
$fields = ['banner_1','banner_2','banner_3','banner_4','background_image','fullscreen_icon','reverso_default'];
foreach ($fields as $f) {
echo '<input type="hidden" name="old_'.$f.'" value="'.htmlspecialchars($edit[$f] ?? '').'">';
}
?>
<div class="col-md-6">
<label class="form-label">Background</label>
<input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['background_image'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Ícono Fullscreen</label>
<input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['fullscreen_icon'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Reverso por defecto</label>
<input type="file" name="reverso_default" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['reverso_default'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['reverso_default']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-12">
<button class="btn btn-primary">Guardar</button>
<?php if ($edit): ?><a href="juegos_config.php" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr>
<th>Juego ID</th><th>Nombre</th><th>Ruleta</th>
<th>B1</th><th>B2</th><th>B3</th><th>B4</th>
<th>BG</th><th>FS</th><th>Reverso</th><th>Acciones</th>
</tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['juego_id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['ruleta_id'] ?? '') ?></td>
<td><?= $r['banner_1']?'<img class="thumb" src="'.htmlspecialchars($r['banner_1']).'">':'—' ?></td>
<td><?= $r['banner_2']?'<img class="thumb" src="'.htmlspecialchars($r['banner_2']).'">':'—' ?></td>
<td><?= $r['banner_3']?'<img class="thumb" src="'.htmlspecialchars($r['banner_3']).'">':'—' ?></td>
<td><?= $r['banner_4']?'<img class="thumb" src="'.htmlspecialchars($r['banner_4']).'">':'—' ?></td>
<td><?= $r['background_image']?'<img class="thumb" src="'.htmlspecialchars($r['background_image']).'">':'—' ?></td>
<td><?= $r['fullscreen_icon']?'<img class="thumb" src="'.htmlspecialchars($r['fullscreen_icon']).'">':'—' ?></td>
<td><?= $r['reverso_default']?'<img class="thumb" src="'.htmlspecialchars($r['reverso_default']).'">':'—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="juegos_config.php?action=edit&juego_id=<?= (int)$r['juego_id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="juegos_config.php?action=delete&juego_id=<?= (int)$r['juego_id'] ?>" onclick="return confirm('¿Eliminar config?')">Eliminar</a>
<a class="btn-outline-warning" href="/../ruleta/memoria/index.php?juego_id=<?= (int)$r['id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+223
View File
@@ -0,0 +1,223 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
/** Cargar ruletas (id, nombre) */
function all_ruletas(mysqli $conn): array {
$res = $conn->query("SELECT id, nombre FROM ruletas ORDER BY id DESC");
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
}
/** Helper para bind dinámico (mysqli requiere referencias) */
function stmt_bind_params(mysqli_stmt $stmt, string $types, array &$params): void {
if ($types === '' || !$params) return;
$bind = [$types];
foreach ($params as $k => $v) { $bind[] = &$params[$k]; }
call_user_func_array([$stmt, 'bind_param'], $bind);
}
$ruletas = all_ruletas($conn);
$ruleta_id = (int)($_GET['ruleta_id'] ?? 0);
$q = trim((string)($_GET['q'] ?? ''));
$desde = (string)($_GET['desde'] ?? '');
$hasta = (string)($_GET['hasta'] ?? '');
$where = "1=1";
$params = [];
$types = "";
// Filtros
if ($ruleta_id) {
$where .= " AND j.ruleta_id=?";
$types .= "i";
$params[] = $ruleta_id;
}
if ($q !== '') {
$where .= " AND (p.nombre LIKE CONCAT('%',?,'%') OR j.ip_address LIKE CONCAT('%',?,'%'))";
$types .= "ss";
$params[] = $q;
$params[] = $q;
}
if ($desde !== '') {
$where .= " AND j.fecha_juego >= ?";
$types .= "s";
$params[] = $desde . " 00:00:00";
}
if ($hasta !== '') {
$where .= " AND j.fecha_juego <= ?";
$types .= "s";
$params[] = $hasta . " 23:59:59";
}
// Export CSV
if (($_GET['export'] ?? '') === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=jugadas.csv');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','Fecha','Ruleta','Premio','IP','User Agent']);
$sql = "SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address, j.user_agent
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where
ORDER BY j.id DESC";
$stmt = $conn->prepare($sql);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
fputcsv($out, [
$row['id'],
$row['fecha_juego'],
$row['ruleta'],
$row['premio'],
$row['ip_address'],
$row['user_agent']
]);
}
fclose($out);
exit;
}
// Paginación
$page = max(1, (int)($_GET['page'] ?? 1));
$pp = 25;
$off = ($page - 1) * $pp;
// Total
$sqlCount = "SELECT COUNT(*) c
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where";
$stmt = $conn->prepare($sqlCount);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$total = (int)$stmt->get_result()->fetch_assoc()['c'];
// Página actual
$sql = "SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address, j.user_agent
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where
ORDER BY j.id DESC
LIMIT $pp OFFSET $off";
$stmt = $conn->prepare($sql);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$pages = (int)ceil($total / $pp);
// Query para export sin "page"
$exportQuery = $_GET;
unset($exportQuery['page']);
$exportQuery['export'] = 'csv';
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Jugadas | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="ruletas.php">Ruletas</a>
<a class="btn btn-outline-light btn-sm" href="premios.php">Premios</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Historial de Jugadas</h3>
<form class="row g-2 mb-3" method="get">
<div class="col-md-3">
<label class="form-label">Ruleta</label>
<select name="ruleta_id" class="form-select">
<option value="">Todas</option>
<?php foreach ($ruletas as $ru): ?>
<?php $rid = (int)$ru['id']; ?>
<option value="<?= $rid ?>" <?= $ruleta_id == $rid ? 'selected' : '' ?>>
<?= htmlspecialchars($ru['nombre']) ?> (ID <?= $rid ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Desde</label>
<input type="date" name="desde" class="form-control" value="<?= htmlspecialchars($desde) ?>">
</div>
<div class="col-md-2">
<label class="form-label">Hasta</label>
<input type="date" name="hasta" class="form-control" value="<?= htmlspecialchars($hasta) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Buscar (premio/IP)</label>
<input type="text" name="q" class="form-control" value="<?= htmlspecialchars($q) ?>" placeholder="Premio, 192.168...">
</div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button class="btn btn-primary w-100">Filtrar</button>
<a class="btn btn-outline-secondary" href="jugadas.php">Limpiar</a>
</div>
<div class="col-12">
<a class="btn btn-success btn-sm" href="jugadas.php?<?= http_build_query($exportQuery) ?>">Exportar CSV</a>
</div>
</form>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th><th>Fecha</th><th>Ruleta</th><th>Premio</th><th>IP</th><th>User Agent</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['ruleta'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:400px;" title="<?= htmlspecialchars($row['user_agent'] ?? '') ?>">
<?= htmlspecialchars($row['user_agent'] ?? '') ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?>
<tr><td colspan="6" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3">
<ul class="pagination pagination-sm">
<?php for ($i = 1; $i <= $pages; $i++): ?>
<li class="page-item <?= $i == $page ? 'active' : '' ?>">
<a class="page-link" href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</div>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
<?php
// admin/jugadas_memoria.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function all_juegos(mysqli $conn){
$res=$conn->query("SELECT juego_id, nombre FROM juegos_config ORDER BY juego_id ASC");
return $res->fetch_all(MYSQLI_ASSOC);
}
$juegos = all_juegos($conn);
$juego_id = (int)($_GET['juego_id'] ?? 0);
$q = trim($_GET['q'] ?? ''); // buscar por IP o UA
$desde = $_GET['desde'] ?? '';
$hasta = $_GET['hasta'] ?? '';
$where = "1=1"; $types=""; $params=[];
if ($juego_id) { $where.=" AND jm.juego_id=?"; $types.="i"; $params[]=$juego_id; }
if ($q!=='') { $where.=" AND (jm.ip_address LIKE CONCAT('%',?,'%') OR jm.user_agent LIKE CONCAT('%',?,'%'))"; $types.="ss"; $params[]=$q; $params[]=$q; }
if ($desde!==''){ $where.=" AND jm.fecha_juego >= ?"; $types.="s"; $params[]=$desde." 00:00:00"; }
if ($hasta!==''){ $where.=" AND jm.fecha_juego <= ?"; $types.="s"; $params[]=$hasta." 23:59:59"; }
// Export CSV
if (($_GET['export'] ?? '') === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=jugadas_memoria.csv');
$out=fopen('php://output','w');
fputcsv($out,['ID','Fecha','Juego','Juego ID','Aciertos','Tiempo (ms)','IP','User Agent']);
$sql="SELECT jm.id, jm.fecha_juego, gc.nombre AS juego, jm.juego_id, jm.aciertos, jm.tiempo_ms, jm.ip_address, jm.user_agent
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id
WHERE $where ORDER BY jm.id DESC";
$stmt=$conn->prepare($sql); if($types){ $stmt->bind_param($types, ...$params); }
$stmt->execute(); $res=$stmt->get_result();
while($r=$res->fetch_assoc()){ fputcsv($out, [$r['id'],$r['fecha_juego'],$r['juego'],$r['juego_id'],$r['aciertos'],$r['tiempo_ms'],$r['ip_address'],$r['user_agent']]); }
fclose($out); exit;
}
// Paginación
$page=max(1,(int)($_GET['page']??1)); $pp=25; $off=($page-1)*$pp;
$sqlCount="SELECT COUNT(*) c FROM jugadas_memoria jm LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id WHERE $where";
$stmt=$conn->prepare($sqlCount); if($types){ $stmt->bind_param($types, ...$params); } $stmt->execute();
$total=(int)$stmt->get_result()->fetch_assoc()['c'];
$sql="SELECT jm.id, jm.fecha_juego, gc.nombre AS juego, jm.juego_id, jm.aciertos, jm.tiempo_ms, jm.ip_address, jm.user_agent
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id
WHERE $where ORDER BY jm.id DESC LIMIT $pp OFFSET $off";
$stmt=$conn->prepare($sql); if($types){ $stmt->bind_param($types, ...$params); } $stmt->execute();
$rows=$stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$pages=(int)ceil($total/$pp);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Jugadas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Jugadas — Memoria</h3>
<form class="row g-2 mb-3" method="get">
<div class="col-md-3">
<label class="form-label">Juego</label>
<select name="juego_id" class="form-select">
<option value="">Todos</option>
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= $juego_id===$j['juego_id']?'selected':'' ?>><?= htmlspecialchars($j['nombre']) ?> (ID <?= (int)$j['juego_id'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><label class="form-label">Desde</label><input type="date" name="desde" class="form-control" value="<?= htmlspecialchars($desde) ?>"></div>
<div class="col-md-2"><label class="form-label">Hasta</label><input type="date" name="hasta" class="form-control" value="<?= htmlspecialchars($hasta) ?>"></div>
<div class="col-md-3"><label class="form-label">Buscar (IP/UA)</label><input type="text" name="q" class="form-control" value="<?= htmlspecialchars($q) ?>"></div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button class="btn btn-primary w-100">Filtrar</button>
<a class="btn btn-outline-secondary" href="jugadas_memoria.php">Limpiar</a>
</div>
<div class="col-12">
<a class="btn btn-success btn-sm" href="jugadas_memoria.php?<?= http_build_query(array_merge($_GET,['export'=>'csv','page'=>null])) ?>">Exportar CSV</a>
</div>
</form>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr>
<th>ID</th><th>Fecha</th><th>Juego</th><th>Juego ID</th><th>Aciertos</th><th>Tiempo (s)</th><th>IP</th><th>User Agent</th>
</tr></thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['fecha_juego']) ?></td>
<td><?= htmlspecialchars($r['juego'] ?? '—') ?></td>
<td><?= (int)$r['juego_id'] ?></td>
<td><?= (int)$r['aciertos'] ?></td>
<td><?= number_format(($r['tiempo_ms'] ?? 0)/1000, 2) ?></td>
<td><?= htmlspecialchars($r['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:400px;" title="<?= htmlspecialchars($r['user_agent'] ?? '') ?>"><?= htmlspecialchars($r['user_agent'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="8" class="text-center text-muted">Sin registros</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
<?php if ($pages>1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm">
<?php for($i=1;$i<=$pages;$i++): ?>
<li class="page-item <?= $i===$page?'active':'' ?>"><a class="page-link" href="?<?= http_build_query(array_merge($_GET,['page'=>$i])) ?>"><?= $i ?></a></li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
</body>
</html>
+178
View File
@@ -0,0 +1,178 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function slugify($s){ $s=trim(strtolower($s)); $s=preg_replace('/[^a-z0-9-]+/','-',$s); return trim($s,'-'); }
$msg=$err=''; $action=$_GET['action']??''; $id=(int)($_GET['id']??0);
if($_SERVER['REQUEST_METHOD']==='POST'){
$pid=(int)($_POST['id']??0);
$titulo=trim($_POST['titulo']??'');
$slug=trim($_POST['slug']??'');
$estado=$_POST['estado']??'borrador';
$hero_image=trim($_POST['hero_image']??'');
$contenido_html=$_POST['contenido_html']??'';
$css_inline=$_POST['css_inline']??'';
$js_inline=$_POST['js_inline']??'';
$meta_title=trim($_POST['meta_title']??'');
$meta_desc=trim($_POST['meta_desc']??'');
$canonical_url=trim($_POST['canonical_url']??'');
$game_url = trim($_POST['game_url'] ?? '');
// (Opcional) Normaliza y valida URL
if ($game_url !== '' && !filter_var($game_url, FILTER_VALIDATE_URL)) {
$err = 'La URL del juego no es válida.';
}
if(!$err){
if($titulo===''){ $err='El título es obligatorio.'; }
else{
if($slug==='') $slug=slugify($titulo);
try{
if($pid>0){
$st=$conn->prepare("UPDATE landing_pages
SET titulo=?, slug=?, estado=?, hero_image=?, contenido_html=?, css_inline=?, js_inline=?, meta_title=?, meta_desc=?, canonical_url=?, game_url=?
WHERE id=?");
$st->bind_param("sssssssssssi",
$titulo,$slug,$estado,$hero_image,$contenido_html,$css_inline,$js_inline,$meta_title,$meta_desc,$canonical_url,$game_url,$pid
);
$st->execute(); $msg='Landing actualizada.';
}else{
$st=$conn->prepare("INSERT INTO landing_pages
(titulo,slug,estado,hero_image,contenido_html,css_inline,js_inline,meta_title,meta_desc,canonical_url,game_url)
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
$st->bind_param("sssssssssss",
$titulo,$slug,$estado,$hero_image,$contenido_html,$css_inline,$js_inline,$meta_title,$meta_desc,$canonical_url,$game_url
);
$st->execute(); $id=$conn->insert_id; $msg='Landing creada.';
}
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
}
if($action==='delete' && $id>0){
$conn->query("DELETE FROM landing_pages WHERE id=".$id);
$msg='Landing eliminada.'; $id=0;
}
$edit=null; if($action==='edit' && $id>0){ $edit=$conn->query("SELECT * FROM landing_pages WHERE id=".$id)->fetch_assoc(); }
$list=$conn->query("SELECT * FROM landing_pages ORDER BY id DESC");
?>
<!doctype html><html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Pages | Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head><body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
</div>
</div></nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?=$msg?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?=$err?></div><?php endif;?>
<div class="card mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit?'Editar landing':'Nueva landing' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id']??0) ?>">
<div class="col-md-6">
<label class="form-label">Título</label>
<input name="titulo" class="form-control" required value="<?= htmlspecialchars($edit['titulo']??'') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Slug</label>
<input name="slug" class="form-control" value="<?= htmlspecialchars($edit['slug']??'') ?>" placeholder="mi-landing">
</div>
<div class="col-md-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select">
<option value="borrador" <?= (($edit['estado']??'')==='borrador'?'selected':'')?>>Borrador</option>
<option value="publicado" <?= (($edit['estado']??'')==='publicado'?'selected':'')?>>Publicado</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Hero image (URL pública)</label>
<input name="hero_image" class="form-control" value="<?= htmlspecialchars($edit['hero_image']??'') ?>" placeholder="/ruleta/uploads/landings/hero.jpg">
</div>
<div class="col-12">
<label class="form-label">Contenido HTML</label>
<textarea name="contenido_html" class="form-control" rows="8" placeholder="<section>..."></textarea>
<script>document.getElementsByName('contenido_html')[0].value = <?= json_encode($edit['contenido_html']??'') ?>;</script>
</div>
<div class="col-12">
<label class="form-label">CSS inline (opcional)</label>
<textarea name="css_inline" class="form-control" rows="4" placeholder="/* estilos */"></textarea>
<script>document.getElementsByName('css_inline')[0].value = <?= json_encode($edit['css_inline']??'') ?>;</script>
</div>
<div class="col-12">
<label class="form-label">JS inline (opcional)</label>
<textarea name="js_inline" class="form-control" rows="4" placeholder="// scripts"></textarea>
<script>document.getElementsByName('js_inline')[0].value = <?= json_encode($edit['js_inline']??'') ?>;</script>
</div>
<div class="col-md-4">
<label class="form-label">Meta title</label>
<input name="meta_title" class="form-control" value="<?= htmlspecialchars($edit['meta_title']??'') ?>">
</div>
<div class="col-md-5">
<label class="form-label">Meta description</label>
<input name="meta_desc" class="form-control" value="<?= htmlspecialchars($edit['meta_desc']??'') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Canonical URL</label>
<input name="canonical_url" class="form-control" value="<?= htmlspecialchars($edit['canonical_url']??'') ?>">
</div>
<!-- NUEVO CAMPO: URL del juego -->
<div class="col-md-6">
<label class="form-label">URL del juego</label>
<input name="game_url" class="form-control" placeholder="https://ejemplo.com/juego"
value="<?= htmlspecialchars($edit['game_url'] ?? '') ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing.php" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th><th>Título</th><th>Slug</th><th>Estado</th><th>Actualizado</th><th>Acciones</th>
</tr>
</thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['titulo']) ?></td>
<td><code><?= htmlspecialchars($r['slug']) ?></code></td>
<td><?= htmlspecialchars($r['estado']) ?></td>
<td><?= htmlspecialchars($r['updated_at']??'') ?></td>
<td class="text-nowrap d-flex gap-1">
<a class="btn btn-sm btn-outline-primary" href="landing.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar landing?')">Eliminar</a>
<a class="btn btn-sm btn-success" target="_blank" href="../landing.php?slug=<?= urlencode($r['slug']) ?>">Ver</a>
<?php if (!empty($r['game_url'])): ?>
<a class="btn btn-sm btn-outline-success" target="_blank" href="<?= htmlspecialchars($r['game_url']) ?>">Juego</a>
<?php endif; ?>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+122
View File
@@ -0,0 +1,122 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$bid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$tipo = $_POST['tipo'] ?? 'html';
$contenido = $_POST['contenido'] ?? '';
if ($nombre === '' || !in_array($tipo, ['html','css','js'], true)) {
$err = 'Nombre y tipo válidos son obligatorios.';
} else {
try {
if ($bid > 0) {
$st = $conn->prepare("UPDATE landing_blocks SET nombre=?, tipo=?, contenido=? WHERE id=?");
$st->bind_param("sssi", $nombre, $tipo, $contenido, $bid);
$st->execute(); $msg = 'Bloque actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_blocks (nombre, tipo, contenido) VALUES (?,?,?)");
$st->bind_param("sss", $nombre, $tipo, $contenido);
$st->execute(); $msg = 'Bloque creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$conn->query("DELETE FROM landing_blocks WHERE id=".$id);
$msg = 'Bloque eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$rs = $conn->query("SELECT * FROM landing_blocks WHERE id=".$id);
$edit = $rs ? $rs->fetch_assoc() : null;
}
$list = $conn->query("SELECT * FROM landing_blocks ORDER BY updated_at DESC, id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Bloques reutilizables</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.mono{font-family:ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar bloque' : 'Nuevo bloque' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-6">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Tipo</label>
<select name="tipo" class="form-select">
<?php $t=$edit['tipo']??'html'; ?>
<option value="html" <?= $t==='html'?'selected':'' ?>>HTML</option>
<option value="css" <?= $t==='css'?'selected':'' ?>>CSS</option>
<option value="js" <?= $t==='js'?'selected':'' ?>>JS</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Contenido</label>
<textarea name="contenido" rows="10" class="form-control mono"><?= htmlspecialchars($edit['contenido'] ?? '') ?></textarea>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_blocks.php" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Tipo</th><th>Actualizado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['tipo']) ?></span></td>
<td><?= htmlspecialchars($r['updated_at']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_blocks.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_blocks.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar bloque?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) die('landing_id requerido');
$landing = $conn->query("SELECT * FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) die('Landing no existe');
$blocks = $conn->query("SELECT id,nombre,tipo FROM landing_blocks ORDER BY nombre ASC")->fetch_all(MYSQLI_ASSOC);
// bloques ya asignados (por zona y posición)
$assigned = [];
$rs = $conn->query("SELECT lpb.id, lpb.block_id, lpb.zona, lpb.posicion, lb.nombre, lb.tipo
FROM landing_page_blocks lpb
JOIN landing_blocks lb ON lb.id=lpb.block_id
WHERE lpb.landing_id={$landing_id}
ORDER BY lpb.zona, lpb.posicion");
while($r=$rs->fetch_assoc()){ $assigned[$r['zona']][] = $r; }
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Builder (Drag & Drop)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<style>
.zone{min-height:80px;border:2px dashed #ddd;border-radius:.5rem;padding:10px}
.chip{cursor:grab;padding:8px 10px;border:1px solid #ddd;border-radius:.5rem;background:#fff;margin-bottom:6px}
.palette{max-height:50vh;overflow:auto;border:1px solid #eee;border-radius:.5rem}
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="#">Builder</a>
<a class="btn btn-outline-light btn-sm" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>">Lista</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3"><a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a></div>
<h4 class="mb-3">Builder: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<div class="row g-3">
<div class="col-md-4">
<div class="card palette">
<div class="card-body">
<h5>Bloques disponibles</h5>
<div id="palette" class="zone">
<?php foreach($blocks as $b): ?>
<div class="chip" data-block-id="<?= (int)$b['id'] ?>">
[<?= htmlspecialchars($b['tipo']) ?>] <?= htmlspecialchars($b['nombre']) ?>
</div>
<?php endforeach; ?>
</div>
<p class="text-muted small mt-2">Arrastra un bloque a una zona.</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Header</h6>
<div id="zone-header" class="zone"><?php
if(!empty($assigned['header'])) foreach($assigned['header'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Main</h6>
<div id="zone-main" class="zone"><?php
if(!empty($assigned['main'])) foreach($assigned['main'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Footer</h6>
<div id="zone-footer" class="zone"><?php
if(!empty($assigned['footer'])) foreach($assigned['footer'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<button id="save" class="btn btn-primary">Guardar distribución</button>
<div id="msg" class="mt-2"></div>
</div>
</div>
</div>
<script>
function mkSortable(el){ return new Sortable(el, { group:'zones', animation:150, sort:true, pull:true, put:true }); }
['palette','zone-header','zone-main','zone-footer'].forEach(id => mkSortable(document.getElementById(id)));
document.getElementById('save').addEventListener('click', function(){
const landing_id = <?= (int)$landing_id ?>;
const zones = ['header','main','footer'];
const payload = { landing_id, items: [] };
zones.forEach(z => {
const zoneEl = document.getElementById('zone-'+z);
Array.from(zoneEl.children).forEach((chip, idx) => {
payload.items.push({
assign_id: chip.dataset.assignId || null, // si ya existía
block_id: chip.dataset.blockId,
zona: z,
posicion: idx + 1
});
});
});
fetch('landing_builder_save.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
})
.then(r=>r.json())
.then(res=>{
document.getElementById('msg').innerHTML =
res.ok ? '<div class="alert alert-success py-2">Guardado.</div>' :
'<div class="alert alert-danger py-2">'+(res.error||'Error')+'</div>';
// actualizar assign_id devueltos para chips nuevos
if(res.map_ids){
const map = res.map_ids;
document.querySelectorAll('.chip').forEach(ch=>{
const bid = ch.dataset.blockId;
const key = ch.dataset.assignId ? null : (bid + '@' + ch.parentElement.id.replace('zone-',''));
if(key && map[key]) ch.dataset.assignId = map[key];
});
}
})
.catch(()=>{ document.getElementById('msg').innerHTML='<div class="alert alert-danger py-2">Error de red</div>'; });
});
</script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
<?php
session_start();
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { echo json_encode(['ok'=>false,'error'=>'auth']); exit; }
$raw = file_get_contents('php://input');
$in = json_decode($raw,true);
$landing_id = (int)($in['landing_id'] ?? 0);
$items = $in['items'] ?? [];
if ($landing_id<=0 || !is_array($items)) { echo json_encode(['ok'=>false,'error'=>'input']); exit; }
$conn->begin_transaction();
try{
$map_ids = []; // devolver ids para chips nuevos
foreach($items as $it){
$assign_id = isset($it['assign_id']) ? (int)$it['assign_id'] : 0;
$block_id = (int)$it['block_id'];
$zona = substr(trim($it['zona'] ?? 'main'), 0, 40);
$posicion = (int)($it['posicion'] ?? 1);
if($assign_id>0){
$st=$conn->prepare("UPDATE landing_page_blocks SET zona=?, posicion=? WHERE id=? AND landing_id=?");
$st->bind_param('siii', $zona, $posicion, $assign_id, $landing_id);
$st->execute();
}else{
// crear nueva asignación
$st=$conn->prepare("INSERT INTO landing_page_blocks (landing_id, block_id, zona, posicion) VALUES (?,?,?,?)");
$st->bind_param('iisi', $landing_id, $block_id, $zona, $posicion);
$st->execute();
// clave aproximada para mapear chip -> asignación creada
$map_ids[$block_id.'@'.$zona] = $conn->insert_id;
}
}
$conn->commit();
echo json_encode(['ok'=>true,'map_ids'=>$map_ids]);
}catch(Throwable $e){
$conn->rollback();
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
}
+152
View File
@@ -0,0 +1,152 @@
<?php
// ruleta/admin/landing_crm.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$hid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$endpoint = trim($_POST['endpoint'] ?? '');
$method = $_POST['method'] ?? 'POST';
$secret = trim($_POST['secret'] ?? '');
$active = isset($_POST['active']) ? 1 : 0;
if ($nombre === '' || $endpoint === '' || !in_array($method, ['POST','GET'], true)) {
$err = 'Nombre, endpoint y método (POST/GET) son obligatorios.';
} else {
try {
if ($hid > 0) {
$st = $conn->prepare("UPDATE landing_crm_hooks SET nombre=?, endpoint=?, method=?, secret=?, active=? WHERE id=? AND landing_id=?");
$st->bind_param("ssssiii", $nombre, $endpoint, $method, $secret, $active, $hid, $landing_id);
$st->execute(); $msg = 'Hook actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_crm_hooks (landing_id, nombre, endpoint, method, secret, active) VALUES (?,?,?,?,?,?)");
$st->bind_param("issssi", $landing_id, $nombre, $endpoint, $method, $secret, $active);
$st->execute(); $msg = 'Hook creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_crm_hooks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Hook eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_crm_hooks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_crm_hooks WHERE landing_id={$landing_id} ORDER BY id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing CRM Hooks</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>">CRM Hooks</a>
<a class="btn btn-outline-light btn-sm" href="landing_forms.php?landing_id=<?= (int)$landing_id ?>">Leads</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong> (#<?= (int)$landing_id ?>)</h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar hook' : 'Nuevo hook' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Endpoint (URL)</label>
<input name="endpoint" class="form-control" required placeholder="https://hooks.zapier.com/..." value="<?= htmlspecialchars($edit['endpoint'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Método</label>
<?php $m=$edit['method']??'POST'; ?>
<select name="method" class="form-select">
<option value="POST" <?= $m==='POST'?'selected':'' ?>>POST</option>
<option value="GET" <?= $m==='GET' ?'selected':'' ?>>GET</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label">Secret / Bearer (opcional)</label>
<input name="secret" class="form-control" placeholder="Se enviará como Authorization: Bearer ..." value="<?= htmlspecialchars($edit['secret'] ?? '') ?>">
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="active" name="active" <?= (isset($edit['active']) ? (intval($edit['active']) ? 'checked' : '') : 'checked') ?>>
<label class="form-check-label" for="active">Activo</label>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_crm.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Endpoint</th><th>Método</th><th>Activo</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><code><?= htmlspecialchars($r['endpoint']) ?></code></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['method']) ?></span></td>
<td><?= intval($r['active']) ? 'Sí' : 'No' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar hook?')">Eliminar</a>
</td>
</tr>
<?php endwhile; if(!$list || !$list->num_rows): ?>
<tr><td colspan="6" class="text-center text-muted">Sin hooks configurados</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<?php
// ruleta/admin/landing_forms.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
$where = "WHERE lf.landing_id={$landing_id}";
$params = []; $types = '';
if ($from !== '') { $where .= " AND lf.created_at >= ?"; $params[] = $from.' 00:00:00'; $types.='s'; }
if ($to !== '') { $where .= " AND lf.created_at <= ?"; $params[] = $to .' 23:59:59'; $types.='s'; }
$sql = "SELECT lf.id, lf.created_at, lf.nombre, lf.email, lf.telefono, lf.mensaje, lf.ip_address
FROM landing_forms lf
{$where}
ORDER BY lf.id DESC
LIMIT 500";
$st = $conn->prepare($sql);
if (!empty($params)) { $st->bind_param($types, ...$params); }
$st->execute();
$list = $st->get_result();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Leads</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.msg{max-width:420px;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>">CRM Hooks</a>
<a class="btn btn-light btn-sm" href="landing_forms.php?landing_id=<?= (int)$landing_id ?>">Leads</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Leads: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<form class="row g-2 mb-3" method="get">
<input type="hidden" name="landing_id" value="<?= (int)$landing_id ?>">
<div class="col-md-3">
<label class="form-label">Desde</label>
<input type="date" class="form-control" name="from" value="<?= htmlspecialchars($from) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Hasta</label>
<input type="date" class="form-control" name="to" value="<?= htmlspecialchars($to) ?>">
</div>
<div class="col-md-6 d-flex align-items-end gap-2">
<button class="btn btn-primary">Filtrar</button>
<a class="btn btn-success" target="_blank"
href="landing_forms_export.php?landing_id=<?= (int)$landing_id ?>&from=<?= urlencode($from) ?>&to=<?= urlencode($to) ?>">
Exportar CSV
</a>
</div>
</form>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Fecha</th><th>Nombre</th><th>Email</th><th>Teléfono</th><th>Mensaje</th><th>IP</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['created_at']) ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['email']) ?></td>
<td><?= htmlspecialchars($r['telefono']) ?></td>
<td class="msg"><?= htmlspecialchars($r['mensaje']) ?></td>
<td><?= htmlspecialchars($r['ip_address']) ?></td>
</tr>
<?php endwhile; if(!$list || !$list->num_rows): ?>
<tr><td colspan="7" class="text-center text-muted">Sin leads</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
<?php
// ruleta/admin/landing_forms_export.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { http_response_code(403); echo 'auth'; exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
if ($landing_id <= 0) { http_response_code(400); echo 'landing_id requerido'; exit; }
$where = "WHERE lf.landing_id={$landing_id}";
$params = []; $types = '';
if ($from !== '') { $where .= " AND lf.created_at >= ?"; $params[] = $from.' 00:00:00'; $types.='s'; }
if ($to !== '') { $where .= " AND lf.created_at <= ?"; $params[] = $to .' 23:59:59'; $types.='s'; }
$sql = "SELECT lf.id, lf.created_at, lp.titulo AS landing, lf.nombre, lf.email, lf.telefono, lf.mensaje, lf.ip_address
FROM landing_forms lf
JOIN landing_pages lp ON lp.id=lf.landing_id
{$where}
ORDER BY lf.id DESC";
$st = $conn->prepare($sql);
if (!empty($params)) { $st->bind_param($types, ...$params); }
$st->execute();
$res = $st->get_result();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="landing_'.$landing_id.'_leads.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','Fecha','Landing','Nombre','Email','Telefono','Mensaje','IP']);
while($row = $res->fetch_assoc()){
fputcsv($out, [
$row['id'],
$row['created_at'],
$row['landing'],
$row['nombre'],
$row['email'],
$row['telefono'],
preg_replace("/\r|\n/"," ", (string)$row['mensaje']),
$row['ip_address'],
]);
}
fclose($out);
+187
View File
@@ -0,0 +1,187 @@
<?php
// ruleta/admin/landing_page_blocks.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
// Cargar landing
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
// Cargar catálogo de bloques para el <select>
$blocks_rs = $conn->query("SELECT id, nombre, tipo FROM landing_blocks ORDER BY nombre ASC");
$blocks = [];
while($b = $blocks_rs->fetch_assoc()){ $blocks[] = $b; }
// Helpers
function valid_zone($z) {
$z = trim($z);
if ($z==='') $z = 'main';
// podrías limitar: ['header','main','footer','sidebar']
return substr($z, 0, 40);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$lpb_id = (int)($_POST['id'] ?? 0);
$block_id = (int)($_POST['block_id'] ?? 0);
$zona = valid_zone($_POST['zona'] ?? 'main');
$posicion = (int)($_POST['posicion'] ?? 1);
if ($block_id <= 0) {
$err = 'Debes seleccionar un bloque.';
} else {
try {
if ($lpb_id > 0) {
$st = $conn->prepare("UPDATE landing_page_blocks SET block_id=?, zona=?, posicion=? WHERE id=? AND landing_id=?");
$st->bind_param("isiii", $block_id, $zona, $posicion, $lpb_id, $landing_id);
$st->execute(); $msg = 'Asignación actualizada.';
} else {
$st = $conn->prepare("INSERT INTO landing_page_blocks (landing_id, block_id, zona, posicion) VALUES (?,?,?,?)");
$st->bind_param("iisi", $landing_id, $block_id, $zona, $posicion);
$st->execute(); $msg = 'Bloque asignado a la landing.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_page_blocks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Asignación eliminada.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_page_blocks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
// Listado agrupado por zona y ordenado por posición
$list = $conn->query("
SELECT lpb.id, lpb.block_id, lpb.zona, lpb.posicion,
lb.nombre, lb.tipo
FROM landing_page_blocks lpb
JOIN landing_blocks lb ON lb.id = lpb.block_id
WHERE lpb.landing_id = {$landing_id}
ORDER BY lpb.zona ASC, lpb.posicion ASC, lpb.id ASC
");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Asignar Bloques</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-outline-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-outline-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-light btn-sm" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>">Asignar bloques</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong> <small class="text-muted">(#<?= (int)$landing_id ?>)</small></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar asignación' : 'Nueva asignación' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-6">
<label class="form-label">Bloque</label>
<select name="block_id" class="form-select" required>
<option value="">— Selecciona —</option>
<?php
$sel_block = (int)($edit['block_id'] ?? 0);
foreach ($blocks as $b):
?>
<option value="<?= (int)$b['id'] ?>" <?= $sel_block===(int)$b['id']?'selected':'' ?>>
[<?= htmlspecialchars(strtoupper($b['tipo'])) ?>] <?= htmlspecialchars($b['nombre']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Zona</label>
<input name="zona" class="form-control" placeholder="main / header / footer" value="<?= htmlspecialchars($edit['zona'] ?? 'main') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Posición</label>
<input name="posicion" type="number" class="form-control" value="<?= htmlspecialchars((string)($edit['posicion'] ?? '1')) ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Asignar' ?></button>
<?php if($edit):?><a href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<h5 class="mb-3">Bloques asignados</h5>
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th>
<th>Bloque</th>
<th>Tipo</th>
<th>Zona</th>
<th>Posición</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if ($list && $list->num_rows): while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['tipo']) ?></span></td>
<td><code><?= htmlspecialchars($r['zona']) ?></code></td>
<td><?= (int)$r['posicion'] ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar asignación?')">Eliminar</a>
</td>
</tr>
<?php endwhile; else: ?>
<tr><td colspan="6" class="text-center text-muted">Sin bloques asignados</td></tr>
<?php endif; ?>
</tbody>
</table>
<p class="text-muted small mb-0">* Sugerencia: usa zonas como <code>header</code>, <code>main</code>, <code>footer</code> o <code>sidebar</code>, y ordena con <strong>posiciones</strong> (1,2,3...).</p>
</div>
</div>
</div>
</body>
</html>
+153
View File
@@ -0,0 +1,153 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
function to_mysql_dt($s) {
// recibe "YYYY-MM-DDTHH:MM" y devuelve "YYYY-MM-DD HH:MM:00" o NULL
$s = trim((string)$s);
if ($s === '') return null;
$s = str_replace('T',' ',$s);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/',$s)) return $s.':00';
return null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sid = (int)($_POST['id'] ?? 0);
$start= to_mysql_dt($_POST['start_at'] ?? '');
$end = to_mysql_dt($_POST['end_at'] ?? '');
$estado = $_POST['estado'] ?? 'programada';
if (!in_array($estado, ['programada','activa','finalizada'], true)) {
$err = 'Estado inválido.';
} else {
try {
if ($sid > 0) {
$st = $conn->prepare("UPDATE landing_schedule SET start_at=?, end_at=?, estado=? WHERE id=? AND landing_id=?");
$st->bind_param("sssii", $start, $end, $estado, $sid, $landing_id);
$st->execute(); $msg = 'Rango actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_schedule (landing_id,start_at,end_at,estado) VALUES (?,?,?,?)");
$st->bind_param("isss", $landing_id, $start, $end, $estado);
$st->execute(); $msg = 'Rango creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_schedule WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Rango eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_schedule WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_schedule WHERE landing_id=".$landing_id." ORDER BY start_at ASC, id ASC");
// helpers para mostrar en datetime-local
function to_input_dt($s) {
if (!$s) return '';
$s = str_replace(' ', 'T', substr($s,0,16));
return $s;
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Schedule</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar rango' : 'Nuevo rango' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-4">
<label class="form-label">Inicio</label>
<input type="datetime-local" name="start_at" class="form-control" value="<?= htmlspecialchars(to_input_dt($edit['start_at'] ?? '')) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Fin</label>
<input type="datetime-local" name="end_at" class="form-control" value="<?= htmlspecialchars(to_input_dt($edit['end_at'] ?? '')) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Estado</label>
<?php $est=$edit['estado']??'programada'; ?>
<select name="estado" class="form-select">
<option value="programada" <?= $est==='programada'?'selected':'' ?>>Programada</option>
<option value="activa" <?= $est==='activa'?'selected':'' ?>>Activa</option>
<option value="finalizada" <?= $est==='finalizada'?'selected':'' ?>>Finalizada</option>
</select>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Inicio</th><th>Fin</th><th>Estado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['start_at'] ?? '—') ?></td>
<td><?= htmlspecialchars($r['end_at'] ?? '—') ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['estado']) ?></span></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar rango?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) die('landing_id requerido');
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) die('Landing no existe');
$rows = [];
$st = $conn->prepare("
SELECT lv.id AS variant_id, COALESCE(lv.nombre,'Base') AS nombre,
SUM(lvs.event_type='impression') AS impresiones,
SUM(lvs.event_type='conversion') AS conversiones
FROM landing_variant_stats lvs
LEFT JOIN landing_variants lv ON lv.id=lvs.variant_id
WHERE lvs.landing_id=?
GROUP BY lvs.variant_id, lv.nombre
ORDER BY impresiones DESC
");
$st->bind_param('i',$landing_id);
$st->execute();
$res = $st->get_result();
while($r=$res->fetch_assoc()){
$r['cr'] = $r['impresiones'] ? round(100*$r['conversiones']/$r['impresiones'],2) : 0.0;
$rows[] = $r;
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Stats</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_stats.php?landing_id=<?= (int)$landing_id ?>">Stats</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Stats: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<div class="card mb-4"><div class="card-body">
<canvas id="chart"></canvas>
</div></div>
<div class="card"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>Variante</th><th>Impresiones</th><th>Conversiones</th><th>CR %</th></tr></thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= htmlspecialchars($r['nombre'] ?? 'Base') ?></td>
<td><?= (int)$r['impresiones'] ?></td>
<td><?= (int)$r['conversiones'] ?></td>
<td><?= number_format($r['cr'],2) ?></td>
</tr>
<?php endforeach; ?>
<?php if(!$rows): ?><tr><td colspan="4" class="text-center text-muted">Sin datos</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
<script>
const data = <?= json_encode($rows, JSON_UNESCAPED_UNICODE) ?>;
const labels = data.map(r => r.nombre || 'Base');
const imps = data.map(r => +r.impresiones);
const convs = data.map(r => +r.conversiones);
const cr = data.map(r => +r.cr);
new Chart(document.getElementById('chart'), {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'Impresiones', data: imps },
{ label: 'Conversiones', data: convs },
{ label: 'CR %', data: cr, yAxisID: 'y1', type: 'line' }
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, title:{display:true,text:'Impresiones / Conversiones'} },
y1: { beginAtZero: true, position:'right', title:{display:true,text:'CR %'} }
}
}
});
</script>
</body>
</html>
+160
View File
@@ -0,0 +1,160 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$peso = (float)($_POST['peso'] ?? 1.0);
$estado= $_POST['estado'] ?? 'activo';
$hero_image = trim($_POST['hero_image'] ?? '');
$contenido_html = $_POST['contenido_html'] ?? '';
$css_inline = $_POST['css_inline'] ?? '';
$js_inline = $_POST['js_inline'] ?? '';
if ($nombre === '' || !in_array($estado, ['borrador','activo','pausado'], true)) {
$err = 'Nombre y estado válidos son obligatorios.';
} else {
try {
if ($vid > 0) {
$st = $conn->prepare("UPDATE landing_variants SET nombre=?, peso=?, estado=?, hero_image=?, contenido_html=?, css_inline=?, js_inline=? WHERE id=? AND landing_id=?");
$st->bind_param("sdssssiii", $nombre, $peso, $estado, $hero_image, $contenido_html, $css_inline, $js_inline, $vid, $landing_id);
$st->execute(); $msg = 'Variante actualizada.';
} else {
$st = $conn->prepare("INSERT INTO landing_variants (landing_id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline) VALUES (?,?,?,?,?,?,?,?)");
$st->bind_param("isdsssss", $landing_id, $nombre, $peso, $estado, $hero_image, $contenido_html, $css_inline, $js_inline);
$st->execute(); $msg = 'Variante creada.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_variants WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Variante eliminada.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_variants WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_variants WHERE landing_id=".$landing_id." ORDER BY id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Variantes</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.mono{font-family:ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-outline-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar variante' : 'Nueva variante' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-5">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Peso (tráfico)</label>
<input name="peso" type="number" step="0.001" min="0" class="form-control" value="<?= htmlspecialchars((string)($edit['peso'] ?? '1.000')) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Estado</label>
<?php $est = $edit['estado'] ?? 'activo'; ?>
<select name="estado" class="form-select">
<option value="borrador" <?= $est==='borrador'?'selected':'' ?>>Borrador</option>
<option value="activo" <?= $est==='activo'?'selected':'' ?>>Activo</option>
<option value="pausado" <?= $est==='pausado'?'selected':'' ?>>Pausado</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Hero image (URL)</label>
<input name="hero_image" class="form-control" placeholder="/ruleta/uploads/landings/hero-b.jpg" value="<?= htmlspecialchars($edit['hero_image'] ?? '') ?>">
</div>
<div class="col-12">
<label class="form-label">Contenido HTML (override)</label>
<textarea name="contenido_html" rows="6" class="form-control mono"><?= htmlspecialchars($edit['contenido_html'] ?? '') ?></textarea>
</div>
<div class="col-12">
<label class="form-label">CSS inline (override)</label>
<textarea name="css_inline" rows="4" class="form-control mono"><?= htmlspecialchars($edit['css_inline'] ?? '') ?></textarea>
</div>
<div class="col-12">
<label class="form-label">JS inline (override)</label>
<textarea name="js_inline" rows="4" class="form-control mono"><?= htmlspecialchars($edit['js_inline'] ?? '') ?></textarea>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_variants.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Peso</th><th>Estado</th><th>Actualizado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['peso']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['estado']) ?></span></td>
<td><?= htmlspecialchars($r['updated_at']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar variante?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+163
View File
@@ -0,0 +1,163 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function all_ruletas(mysqli $conn) {
$res = $conn->query("SELECT id, nombre FROM ruletas ORDER BY id DESC");
return $res->fetch_all(MYSQLI_ASSOC);
}
function find_premio(mysqli $conn, $id) {
$stmt = $conn->prepare("SELECT id, ruleta_id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo FROM premios WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute(); return $stmt->get_result()->fetch_assoc();
}
$ruletas = all_ruletas($conn);
$ruleta_id = (int)($_GET['ruleta_id'] ?? ($ruletas[0]['id'] ?? 0));
$msg = $err = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$ruleta_id = (int)($_POST['ruleta_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$cantidad = (int)($_POST['cantidad'] ?? 0);
$probabilidad = (float)($_POST['probabilidad'] ?? 0);
$inicio = (float)($_POST['inicio_angulo'] ?? 0);
$fin = (float)($_POST['fin_angulo'] ?? 0);
if (!$ruleta_id || $nombre === '') {
$err = "Completa ruleta y nombre.";
} else {
if ($id > 0) {
$stmt = $conn->prepare("UPDATE premios SET ruleta_id=?, nombre=?, cantidad=?, probabilidad=?, inicio_angulo=?, fin_angulo=? WHERE id=?");
$stmt->bind_param("isidddi", $ruleta_id, $nombre, $cantidad, $probabilidad, $inicio, $fin, $id);
$stmt->execute(); $msg = "Premio actualizado.";
} else {
$stmt = $conn->prepare("INSERT INTO premios (ruleta_id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo) VALUES (?,?,?,?,?,?)");
$stmt->bind_param("isiddd", $ruleta_id, $nombre, $cantidad, $probabilidad, $inicio, $fin);
$stmt->execute(); $msg = "Premio creado.";
}
}
}
if (($_GET['action'] ?? '') === 'delete') {
$id = (int)($_GET['id'] ?? 0);
if ($id) {
$stmt = $conn->prepare("DELETE FROM premios WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute(); $msg = "Premio eliminado.";
}
}
$edit = null;
if (($_GET['action'] ?? '') === 'edit') {
$edit = find_premio($conn, (int)($_GET['id'] ?? 0));
if ($edit) $ruleta_id = (int)$edit['ruleta_id'];
}
$stmt = $conn->prepare("SELECT p.id, p.nombre, p.cantidad, p.probabilidad, p.inicio_angulo, p.fin_angulo, r.nombre AS ruleta
FROM premios p INNER JOIN ruletas r ON r.id=p.ruleta_id
WHERE p.ruleta_id=? ORDER BY p.id DESC");
$stmt->bind_param("i", $ruleta_id); $stmt->execute();
$items = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Premios | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="ruletas.php">Ruletas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas.php">Jugadas</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex align-items-center justify-content-between">
<h3 class="mb-3">Premios</h3>
<form class="d-flex" method="get">
<select name="ruleta_id" class="form-select me-2" onchange="this.form.submit()">
<?php foreach ($ruletas as $r): ?>
<option value="<?= (int)$r['id'] ?>" <?= $ruleta_id===$r['id']?'selected':'' ?>>
<?= htmlspecialchars($r['nombre']) ?> (ID <?= (int)$r['id'] ?>)
</option>
<?php endforeach; ?>
</select>
<noscript><button class="btn btn-primary">Cambiar</button></noscript>
</form>
</div>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar premio' : 'Nuevo premio' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-3">
<label class="form-label">Ruleta</label>
<select name="ruleta_id" class="form-select" required>
<?php foreach ($ruletas as $r): ?>
<option value="<?= (int)$r['id'] ?>" <?= ($ruleta_id===$r['id'])?'selected':'' ?>><?= htmlspecialchars($r['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Cantidad</label>
<input type="number" name="cantidad" class="form-control" min="0" value="<?= (int)($edit['cantidad'] ?? 0) ?>">
</div>
<div class="col-md-2">
<label class="form-label">Probabilidad (%)</label>
<input type="number" step="0.01" name="probabilidad" class="form-control" min="0" value="<?= htmlspecialchars($edit['probabilidad'] ?? '0') ?>">
</div>
<div class="col-md-1">
<label class="form-label">Inicio°</label>
<input type="number" step="0.01" name="inicio_angulo" class="form-control" value="<?= htmlspecialchars($edit['inicio_angulo'] ?? '0') ?>">
</div>
<div class="col-md-1">
<label class="form-label">Fin°</label>
<input type="number" step="0.01" name="fin_angulo" class="form-control" value="<?= htmlspecialchars($edit['fin_angulo'] ?? '0') ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="premios.php?ruleta_id=<?= (int)$ruleta_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>ID</th><th>Premio</th><th>Cantidad</th><th>Prob %</th><th>Inicio°</th><th>Fin°</th><th>Acciones</th></tr></thead>
<tbody>
<?php foreach ($items as $it): ?>
<tr>
<td><?= (int)$it['id'] ?></td>
<td><?= htmlspecialchars($it['nombre']) ?></td>
<td><?= (int)$it['cantidad'] ?></td>
<td><?= htmlspecialchars($it['probabilidad']) ?></td>
<td><?= htmlspecialchars($it['inicio_angulo']) ?></td>
<td><?= htmlspecialchars($it['fin_angulo']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="premios.php?action=edit&id=<?= (int)$it['id'] ?>&ruleta_id=<?= (int)$ruleta_id ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="premios.php?action=delete&id=<?= (int)$it['id'] ?>&ruleta_id=<?= (int)$ruleta_id ?>" onclick="return confirm('¿Eliminar premio?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?><tr><td colspan="7" class="text-center text-muted">Sin premios</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
require_once __DIR__.'/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/rasca';
$PUBLIC_DIR = '/ruleta/uploads/rasca';
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name){ $name=preg_replace('/[^A-Za-z0-9._-]/','_',$name); return substr($name,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f=$_FILES[$field]; if($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field.");
$mime=(new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed=['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if(!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido.");
if($f['size']>5*1024*1024) throw new RuntimeException("Máximo 5MB.");
$ext=$allowed[$mime]; $base=sanitize_filename(pathinfo($f['name'],PATHINFO_FILENAME));
$name=$base.'-'.uniqid().'.'.$ext; $dest=rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$name;
if(!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR,'/') . '/' . $name;
}
function upload_or_keep($name,$old,$UPLOAD_DIR,$PUBLIC_DIR){
try{ $v=upload_image($name,$UPLOAD_DIR,$PUBLIC_DIR); return $v?:($old?:null); }
catch(Throwable $e){ return $old?:null; }
}
$msg=$err=''; $action=$_GET['action']??''; $rasca_id=(int)($_GET['rasca_id']??0);
if($_SERVER['REQUEST_METHOD']==='POST'){
$rid=(int)($_POST['rasca_id']??0);
$nombre=trim($_POST['nombre']??'');
$olds=[
'banner_1'=>$_POST['old_banner_1']??'','banner_2'=>$_POST['old_banner_2']??'',
'banner_3'=>$_POST['old_banner_3']??'','banner_4'=>$_POST['old_banner_4']??'',
'background_image'=>$_POST['old_background_image']??'','fullscreen_icon'=>$_POST['old_fullscreen_icon']??'',
'fg_image'=>$_POST['old_fg_image']??'','bg_image'=>$_POST['old_bg_image']??'',
];
if($rid<=0 || $nombre===''){ $err='Indica rasca_id y nombre.'; }
else{
try{
$new=[
'banner_1'=>upload_or_keep('banner_1',$olds['banner_1'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_2'=>upload_or_keep('banner_2',$olds['banner_2'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_3'=>upload_or_keep('banner_3',$olds['banner_3'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_4'=>upload_or_keep('banner_4',$olds['banner_4'],$UPLOAD_DIR,$PUBLIC_DIR),
'background_image'=>upload_or_keep('background_image',$olds['background_image'],$UPLOAD_DIR,$PUBLIC_DIR),
'fullscreen_icon'=>upload_or_keep('fullscreen_icon',$olds['fullscreen_icon'],$UPLOAD_DIR,$PUBLIC_DIR),
'fg_image'=>upload_or_keep('fg_image',$olds['fg_image'],$UPLOAD_DIR,$PUBLIC_DIR),
'bg_image'=>upload_or_keep('bg_image',$olds['bg_image'],$UPLOAD_DIR,$PUBLIC_DIR),
];
$sql="INSERT INTO rasca_config (rasca_id,nombre,banner_1,banner_2,banner_3,banner_4,background_image,fullscreen_icon,fg_image,bg_image)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE nombre=VALUES(nombre),
banner_1=VALUES(banner_1), banner_2=VALUES(banner_2), banner_3=VALUES(banner_3), banner_4=VALUES(banner_4),
background_image=VALUES(background_image), fullscreen_icon=VALUES(fullscreen_icon),
fg_image=VALUES(fg_image), bg_image=VALUES(bg_image)";
$st=$conn->prepare($sql);
$st->bind_param("isssssssss",$rid,$nombre,$new['banner_1'],$new['banner_2'],$new['banner_3'],$new['banner_4'],$new['background_image'],$new['fullscreen_icon'],$new['fg_image'],$new['bg_image']);
$st->execute(); $msg='Configuración guardada.'; $rasca_id=$rid;
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
if($action==='delete' && $rasca_id>0){ $st=$conn->prepare("DELETE FROM rasca_config WHERE rasca_id=?"); $st->bind_param("i",$rasca_id); $st->execute(); $msg='Configuración eliminada.'; $rasca_id=0; }
$edit=null; if($action==='edit' && $rasca_id>0){ $st=$conn->prepare("SELECT * FROM rasca_config WHERE rasca_id=?"); $st->bind_param("i",$rasca_id); $st->execute(); $edit=$st->get_result()->fetch_assoc(); }
$list=$conn->query("SELECT * FROM rasca_config ORDER BY rasca_id ASC");
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Configuración</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_jugadas.php">Jugadas Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div></nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?=htmlspecialchars($msg)?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?=htmlspecialchars($err)?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit?'Editar':'Nuevo/Actualizar (por rasca_id)' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<?php foreach(['banner_1','banner_2','banner_3','banner_4','background_image','fullscreen_icon','fg_image','bg_image'] as $h): ?>
<input type="hidden" name="old_<?= $h ?>" value="<?= htmlspecialchars($edit[$h]??'') ?>">
<?php endforeach; ?>
<div class="col-md-2"><label class="form-label">Rasca ID</label><input type="number" name="rasca_id" class="form-control" required value="<?= htmlspecialchars($edit['rasca_id']??'') ?>"></div>
<div class="col-md-5"><label class="form-label">Nombre</label><input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre']??'') ?>"></div>
<div class="col-md-6"><label class="form-label">Banner 1</label><input type="file" name="banner_1" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_1'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_1']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 2</label><input type="file" name="banner_2" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_2'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_2']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 3</label><input type="file" name="banner_3" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_3'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_3']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 4</label><input type="file" name="banner_4" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_4'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_4']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Background</label><input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['background_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Ícono Fullscreen</label><input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['fullscreen_icon'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Imagen FG (raspable)</label><input type="file" name="fg_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['fg_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fg_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Imagen BG (base/premio)</label><input type="file" name="bg_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['bg_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['bg_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-12"><button class="btn btn-primary">Guardar</button><?php if($edit):?><a href="rasca_config.php" class="btn btn-secondary">Cancelar</a><?php endif;?></div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>Rasca ID</th><th>Nombre</th><th>B1</th><th>B2</th><th>B3</th><th>B4</th><th>BG</th><th>FS</th><th>FG</th><th>Base</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['rasca_id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= $r['banner_1']?'<img class="thumb" src="'.htmlspecialchars($r['banner_1']).'">':'—' ?></td>
<td><?= $r['banner_2']?'<img class="thumb" src="'.htmlspecialchars($r['banner_2']).'">':'—' ?></td>
<td><?= $r['banner_3']?'<img class="thumb" src="'.htmlspecialchars($r['banner_3']).'">':'—' ?></td>
<td><?= $r['banner_4']?'<img class="thumb" src="'.htmlspecialchars($r['banner_4']).'">':'—' ?></td>
<td><?= $r['background_image']?'<img class="thumb" src="'.htmlspecialchars($r['background_image']).'">':'—' ?></td>
<td><?= $r['fullscreen_icon']?'<img class="thumb" src="'.htmlspecialchars($r['fullscreen_icon']).'">':'—' ?></td>
<td><?= $r['fg_image']?'<img class="thumb" src="'.htmlspecialchars($r['fg_image']).'">':'—' ?></td>
<td><?= $r['bg_image']?'<img class="thumb" src="'.htmlspecialchars($r['bg_image']).'">':'—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="rasca_config.php?action=edit&rasca_id=<?= (int)$r['rasca_id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="rasca_config.php?action=delete&rasca_id=<?= (int)$r['rasca_id'] ?>" onclick="return confirm('¿Eliminar config?')">Eliminar</a>
<a class="btn-outline-warning" href="/../ruleta/rasca_index.html?rasca_id=<?= (int)$r['rasca_id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+87
View File
@@ -0,0 +1,87 @@
<?php
session_start();
require_once __DIR__.'/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function h($v){ return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }
$rows = [];
$error = null;
try {
$sql = "
SELECT j.id, j.fecha_juego, j.rasca_id, j.premio_id, j.porcentaje,
j.ip_address, j.user_agent,
p.nombre AS premio_nombre
FROM jugadas_rasca j
LEFT JOIN rasca_premios p
ON p.id = j.premio_id AND p.rasca_id = j.rasca_id
ORDER BY j.id DESC
LIMIT 200
";
$q = $conn->query($sql);
if ($q) {
$rows = $q->fetch_all(MYSQLI_ASSOC);
} else {
$error = 'No se pudo ejecutar la consulta de jugadas.';
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Jugadas</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div></nav>
<div class="container py-4">
<h4 class="mb-3">Últimas jugadas (Rasca)</h4>
<?php if ($error): ?>
<div class="alert alert-danger"><?= h($error) ?></div>
<?php endif; ?>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th>
<th>Fecha</th>
<th>Rasca</th>
<th>Premio</th>
<th>%</th>
<th>IP</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['fecha_juego']) ?></td>
<td><?= (int)$r['rasca_id'] ?></td>
<td><?= h($r['premio_nombre'] ?: '#'.(int)$r['premio_id']) ?></td>
<td><?= h($r['porcentaje']) ?></td>
<td><?= h($r['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:360px"><?= h($r['user_agent'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php if(!$rows && !$error): ?>
<tr><td colspan="7" class="text-center text-muted">Sin jugadas</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+285
View File
@@ -0,0 +1,285 @@
<?php
/**
* /ruleta/admin/rasca_premios.php
* - Vista 1 (sin rasca_id): lista de rasca configurados para elegir
* - Vista 2 (con rasca_id): CRUD de premios de ese rasca
*/
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
//
// Config de uploads
//
$UPLOAD_DIR = __DIR__ . '/../uploads/rasca_premios';
$PUBLIC_DIR = '/ruleta/uploads/rasca_premios'; // ajusta si tu raíz no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
//
// Helpers
//
function sanitize_filename($n){ $n = preg_replace('/[^A-Za-z0-9._-]/','_', $n); return substr($n,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5*1024*1024) throw new RuntimeException("Archivo muy grande (máx 5MB).");
$ext = $allowed[$mime]; $base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$name = $base.'-'.uniqid().'.'.$ext; $dest = rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$name;
if (!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar el archivo.");
return rtrim($PUBLIC_DIR,'/').'/'.$name;
}
//
// Routing params
//
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
$rasca_id = isset($_GET['rasca_id']) ? (int)$_GET['rasca_id'] : null;
$msg = $err = '';
//
// Form handling (solo si hay rasca_id seleccionado)
//
if ($rasca_id && $_SERVER['REQUEST_METHOD']==='POST') {
$pid = (int)($_POST['id'] ?? 0);
$rasca_id = (int)($_POST['rasca_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$prob = (float)($_POST['probabilidad'] ?? 0);
$stock = ($_POST['stock'] !== '' ? (int)$_POST['stock'] : null);
$activo = isset($_POST['activo']) ? 1 : 0;
$old_img = $_POST['old_imagen_url'] ?? '';
if ($rasca_id<=0 || $nombre==='') {
$err = 'Indica rasca_id y nombre.';
} else {
try {
$img = upload_image('imagen',$UPLOAD_DIR,$PUBLIC_DIR) ?? ($old_img ?: null);
if ($pid > 0) {
$st = $conn->prepare("UPDATE rasca_premios SET rasca_id=?, nombre=?, probabilidad=?, stock=?, activo=?, imagen_url=? WHERE id=?");
// stock puede ser NULL: usa bind con "d" para probabilidad y "i" para stock; si es NULL, usa null y types "isdiisi" sigue válido porque mysqli convierte nulls
$st->bind_param("isdiisi", $rasca_id, $nombre, $prob, $stock, $activo, $img, $pid);
$st->execute();
$msg = 'Premio actualizado.';
} else {
$st = $conn->prepare("INSERT INTO rasca_premios (rasca_id, nombre, probabilidad, stock, activo, imagen_url) VALUES (?,?,?,?,?,?)");
$st->bind_param("isdiis", $rasca_id, $nombre, $prob, $stock, $activo, $img);
$st->execute();
$msg = 'Premio creado.';
}
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
//
// Delete premio
//
if ($rasca_id && $action==='delete' && $id>0) {
$st = $conn->prepare("DELETE FROM rasca_premios WHERE id=? AND rasca_id=?");
$st->bind_param("ii", $id, $rasca_id);
$st->execute();
$msg = 'Premio eliminado.';
}
//
// Datos para vistas
//
$rasca_list = []; // para Vista 1
$rasca_info = null; // info del rasca seleccionado
$edit = null; // en Vista 2, premio a editar
$premios = []; // lista de premios del rasca
if (!$rasca_id) {
// Vista 1: listar rasca_config + conteo de premios
$sql = "SELECT r.rasca_id, r.nombre,
COALESCE(p.cnt,0) AS total_premios
FROM rasca_config r
LEFT JOIN (
SELECT rasca_id, COUNT(*) cnt
FROM rasca_premios
GROUP BY rasca_id
) p ON p.rasca_id = r.rasca_id
ORDER BY r.rasca_id ASC";
$rs = $conn->query($sql);
if ($rs) $rasca_list = $rs->fetch_all(MYSQLI_ASSOC);
} else {
// Vista 2: CRUD de premios para rasca_id
$st = $conn->prepare("SELECT rasca_id, nombre FROM rasca_config WHERE rasca_id=?");
$st->bind_param("i", $rasca_id);
$st->execute();
$rasca_info = $st->get_result()->fetch_assoc();
if ($action==='edit' && $id>0) {
$st = $conn->prepare("SELECT * FROM rasca_premios WHERE id=? AND rasca_id=?");
$st->bind_param("ii", $id, $rasca_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$st = $conn->prepare("SELECT * FROM rasca_premios WHERE rasca_id=? ORDER BY id DESC");
$st->bind_param("i", $rasca_id);
$st->execute();
$premios = $st->get_result()->fetch_all(MYSQLI_ASSOC);
}
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Premios</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem}
.card-hover:hover{box-shadow:0 0.5rem 1rem rgba(0,0,0,.15);}
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_jugadas.php">Jugadas Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<?php if(!$rasca_id): ?>
<!-- VISTA 1: Selector de juegos Rasca -->
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">Selecciona un juego de Rasca</h4>
<a class="btn btn-primary" href="rasca_config.php">Crear/editar Rasca</a>
</div>
<?php if(!$rasca_list): ?>
<div class="alert alert-info">No hay juegos de rasca configurados todavía.</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($rasca_list as $rc): ?>
<div class="col-md-4">
<a href="rasca_premios.php?rasca_id=<?= (int)$rc['rasca_id'] ?>" class="text-decoration-none text-dark">
<div class="card card-hover h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Rasca #<?= (int)$rc['rasca_id'] ?></h5>
<span class="badge text-bg-secondary"><?= (int)$rc['total_premios'] ?> premios</span>
</div>
<p class="mt-2 mb-0 text-muted"><?= htmlspecialchars($rc['nombre'] ?? '') ?></p>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<button class="btn btn-sm btn-outline-primary">Administrar premios</button>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<!-- VISTA 2: CRUD premios de un Rasca -->
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<a href="rasca_premios.php" class="btn btn-link">&larr; Volver</a>
<h4 class="d-inline-block ms-2 mb-0">Premios Rasca #<?= (int)$rasca_id ?> <?= $rasca_info ? '— '.htmlspecialchars($rasca_info['nombre']) : '' ?></h4>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar premio' : 'Nuevo premio' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<input type="hidden" name="old_imagen_url" value="<?= htmlspecialchars($edit['imagen_url'] ?? '') ?>">
<div class="col-md-2">
<label class="form-label">Rasca ID</label>
<input type="number" name="rasca_id" class="form-control" required value="<?= htmlspecialchars($rasca_id) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Probabilidad (peso)</label>
<input type="number" step="0.001" name="probabilidad" class="form-control" required value="<?= htmlspecialchars($edit['probabilidad'] ?? '0') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Stock (vacío = ilimitado)</label>
<input type="number" name="stock" class="form-control" value="<?= htmlspecialchars(isset($edit['stock']) ? $edit['stock'] : '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Imagen del premio</label>
<input type="file" name="imagen" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if(!empty($edit['imagen_url'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_url']) ?>" class="thumb"></div>
<?php endif; ?>
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="activo" id="activo" <?= (isset($edit['activo']) ? ((int)$edit['activo']===1) : true) ? 'checked' : '' ?>>
<label class="form-check-label" for="activo">Activo</label>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if($edit): ?><a href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Prob</th>
<th>Stock</th>
<th>Activo</th>
<th>Imagen</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach($premios as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['probabilidad']) ?></td>
<td><?= is_null($r['stock']) ? '∞' : (int)$r['stock'] ?></td>
<td><?= ((int)$r['activo']===1 ? 'Sí' : 'No') ?></td>
<td><?= $r['imagen_url'] ? '<img class="thumb" src="'.htmlspecialchars($r['imagen_url']).'">' : '—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar premio?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if(!$premios): ?>
<tr><td colspan="7" class="text-center text-muted">Sin premios</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>
+303
View File
@@ -0,0 +1,303 @@
<?php
// admin/ruletas.php (con banners, background e icono fullscreen)
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
// Dónde guardar físicamente y cómo se verán públicamente (URL)
$UPLOAD_DIR = __DIR__ . '/../uploads/ruletas'; // físico
$PUBLIC_DIR = '/ruleta/uploads/ruletas'; // público (ajusta si tu base no es /ruleta)
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
// ---------- helpers ----------
function sanitize_filename($name) {
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
return substr($name, 0, 180);
}
function upload_image($field, $UPLOAD_DIR, $PUBLIC_DIR) {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido para $field. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5 * 1024 * 1024) throw new RuntimeException("Archivo muy grande para $field (máx 5MB).");
$ext = $allowed[$mime];
$base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$fname = $base . '-' . uniqid() . '.' . $ext;
$dest = rtrim($UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR . $fname;
if (!move_uploaded_file($f['tmp_name'], $dest)) throw new RuntimeException("No se pudo guardar el archivo de $field.");
return rtrim($PUBLIC_DIR, '/') . '/' . $fname; // ruta pública para <img src="">
}
function get_ruleta(mysqli $conn, $id) {
$stmt = $conn->prepare("SELECT id, nombre, descripcion,
imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon
FROM ruletas WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
// ---------- create / update ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$rid = (int)($_POST['id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$descripcion = trim($_POST['descripcion'] ?? '');
// valores previos (para mantener si no se sube nuevo archivo)
$old = [
'imagen_ruleta' => trim($_POST['old_imagen_ruleta'] ?? ''),
'imagen_puntero' => trim($_POST['old_imagen_puntero'] ?? ''),
'banner_1' => trim($_POST['old_banner_1'] ?? ''),
'banner_2' => trim($_POST['old_banner_2'] ?? ''),
'banner_3' => trim($_POST['old_banner_3'] ?? ''),
'banner_4' => trim($_POST['old_banner_4'] ?? ''),
'background_image'=> trim($_POST['old_background_image'] ?? ''),
'fullscreen_icon' => trim($_POST['old_fullscreen_icon'] ?? ''),
];
if ($nombre === '') {
$err = "El nombre es obligatorio.";
} else {
try {
// nuevas subidas (si vienen)
$new = [
'imagen_ruleta' => upload_image('imagen_ruleta', $UPLOAD_DIR, $PUBLIC_DIR),
'imagen_puntero' => upload_image('imagen_puntero', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_1' => upload_image('banner_1', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_2' => upload_image('banner_2', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_3' => upload_image('banner_3', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_4' => upload_image('banner_4', $UPLOAD_DIR, $PUBLIC_DIR),
'background_image'=> upload_image('background_image',$UPLOAD_DIR, $PUBLIC_DIR),
'fullscreen_icon' => upload_image('fullscreen_icon', $UPLOAD_DIR, $PUBLIC_DIR),
];
// decidir final (nuevo o viejo o null)
$vals = [];
foreach ($new as $k => $v) {
$vals[$k] = $v ?: ($old[$k] ?: null);
}
if ($rid > 0) {
$stmt = $conn->prepare("UPDATE ruletas SET
nombre=?, descripcion=?,
imagen_ruleta=?, imagen_puntero=?,
banner_1=?, banner_2=?, banner_3=?, banner_4=?,
background_image=?, fullscreen_icon=?
WHERE id=?");
$stmt->bind_param(
"ssssssssssi",
$nombre, $descripcion,
$vals['imagen_ruleta'], $vals['imagen_puntero'],
$vals['banner_1'], $vals['banner_2'], $vals['banner_3'], $vals['banner_4'],
$vals['background_image'], $vals['fullscreen_icon'],
$rid
);
$stmt->execute();
$msg = "Ruleta actualizada.";
} else {
$stmt = $conn->prepare("INSERT INTO ruletas
(nombre, descripcion, imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon)
VALUES (?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param(
"ssssssssss",
$nombre, $descripcion,
$vals['imagen_ruleta'], $vals['imagen_puntero'],
$vals['banner_1'], $vals['banner_2'], $vals['banner_3'], $vals['banner_4'],
$vals['background_image'], $vals['fullscreen_icon']
);
$stmt->execute();
$msg = "Ruleta creada.";
}
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
// ---------- delete ----------
if ($action === 'delete' && $id > 0) {
$stmt = $conn->prepare("DELETE FROM ruletas WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute();
$msg = "Ruleta eliminada.";
}
// ---------- edit load + list ----------
$edit = null;
if ($action === 'edit' && $id > 0) { $edit = get_ruleta($conn, $id); }
$res = $conn->query("SELECT id, nombre, descripcion,
imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon, fecha_creacion
FROM ruletas ORDER BY id DESC");
$items = $res->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Ruletas | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="premios.php">Premios</a>
<a class="btn btn-outline-light btn-sm" href="jugadas.php">Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Ruletas</h3>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar ruleta' : 'Nueva ruleta' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<!-- old values para conservar si no se sube -->
<input type="hidden" name="old_imagen_ruleta" value="<?= htmlspecialchars($edit['imagen_ruleta'] ?? '') ?>">
<input type="hidden" name="old_imagen_puntero" value="<?= htmlspecialchars($edit['imagen_puntero'] ?? '') ?>">
<input type="hidden" name="old_banner_1" value="<?= htmlspecialchars($edit['banner_1'] ?? '') ?>">
<input type="hidden" name="old_banner_2" value="<?= htmlspecialchars($edit['banner_2'] ?? '') ?>">
<input type="hidden" name="old_banner_3" value="<?= htmlspecialchars($edit['banner_3'] ?? '') ?>">
<input type="hidden" name="old_banner_4" value="<?= htmlspecialchars($edit['banner_4'] ?? '') ?>">
<input type="hidden" name="old_background_image"value="<?= htmlspecialchars($edit['background_image'] ?? '') ?>">
<input type="hidden" name="old_fullscreen_icon" value="<?= htmlspecialchars($edit['fullscreen_icon'] ?? '') ?>">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-8">
<label class="form-label">Descripción</label>
<input name="descripcion" class="form-control" value="<?= htmlspecialchars($edit['descripcion'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Imagen de la ruleta</label>
<input type="file" name="imagen_ruleta" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_ruleta'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_ruleta']) ?>" class="thumb" alt="ruleta">
<small class="text-muted ms-2">Se mantiene si no subes otra.</small></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Imagen del puntero</label>
<input type="file" name="imagen_puntero" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_puntero'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_puntero']) ?>" class="thumb" alt="puntero"></div>
<?php endif; ?>
</div>
<div class="col-12"><hr></div>
<div class="col-md-6">
<label class="form-label">Banner 1</label>
<input type="file" name="banner_1" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_1'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_1']) ?>" class="thumb" alt="b1"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 2</label>
<input type="file" name="banner_2" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_2'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_2']) ?>" class="thumb" alt="b2"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 3</label>
<input type="file" name="banner_3" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_3'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_3']) ?>" class="thumb" alt="b3"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 4</label>
<input type="file" name="banner_4" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_4'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_4']) ?>" class="thumb" alt="b4"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Background (imagen)</label>
<input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['background_image'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb" alt="bg"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Ícono botón Fullscreen</label>
<input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['fullscreen_icon'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb" alt="fs"></div>
<?php endif; ?>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="ruletas.php" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th><th>Nombre</th>
<th>Ruleta</th><th>Puntero</th><th>BG</th>
<th>B1</th><th>B2</th><th>B3</th><th>B4</th>
<th>FS</th><th>Creado</th><th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?php if ($r['imagen_ruleta']): ?><img class="thumb" src="<?= htmlspecialchars($r['imagen_ruleta']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['imagen_puntero']): ?><img class="thumb" src="<?= htmlspecialchars($r['imagen_puntero']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['background_image']): ?><img class="thumb" src="<?= htmlspecialchars($r['background_image']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_1']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_1']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_2']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_2']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_3']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_3']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_4']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_4']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['fullscreen_icon']): ?><img class="thumb" src="<?= htmlspecialchars($r['fullscreen_icon']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?= htmlspecialchars($r['fecha_creacion'] ?? '') ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="ruletas.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="ruletas.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar ruleta?')">Eliminar</a>
<a class="btn btn-sm btn-outline-success" href="premios.php?ruleta_id=<?= (int)$r['id'] ?>">Premios</a>
<a class="btn-outline-warning" href="/../ruleta/index.html?ruleta_id=<?= (int)$r['id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?>
<tr><td colspan="12" class="text-center text-muted">Sin ruletas</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>