Initial commit: Proyecto Portal ePromos completo con documentación unificada y seguridad base
This commit is contained in:
@@ -0,0 +1,241 @@
|
|||||||
|
-- Tabla principal de landings
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_pages (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
titulo VARCHAR(200) NOT NULL,
|
||||||
|
slug VARCHAR(160) NOT NULL UNIQUE,
|
||||||
|
estado ENUM('borrador','publicado') NOT NULL DEFAULT 'borrador',
|
||||||
|
hero_image VARCHAR(255) NULL,
|
||||||
|
contenido_html MEDIUMTEXT NULL, -- HTML renderizable
|
||||||
|
css_inline MEDIUMTEXT NULL, -- CSS opcional por landing
|
||||||
|
js_inline MEDIUMTEXT NULL, -- JS opcional por landing
|
||||||
|
meta_title VARCHAR(200) NULL,
|
||||||
|
meta_desc VARCHAR(255) NULL,
|
||||||
|
canonical_url VARCHAR(255) NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Captura de leads del formulario de la landing
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_forms (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT UNSIGNED NOT NULL,
|
||||||
|
nombre VARCHAR(150) NULL,
|
||||||
|
email VARCHAR(180) NULL,
|
||||||
|
telefono VARCHAR(60) NULL,
|
||||||
|
mensaje TEXT NULL,
|
||||||
|
ip_address VARCHAR(64) NULL,
|
||||||
|
user_agent TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX (landing_id),
|
||||||
|
CONSTRAINT fk_lf_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
-- 1. Bloques reutilizables (biblioteca)
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_blocks (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nombre VARCHAR(160) NOT NULL,
|
||||||
|
tipo ENUM('html','css','js') NOT NULL DEFAULT 'html',
|
||||||
|
contenido MEDIUMTEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_nombre (nombre)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 2. Asignación de bloques a una landing (con orden)
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_page_blocks (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT UNSIGNED NOT NULL,
|
||||||
|
block_id INT UNSIGNED NOT NULL,
|
||||||
|
posicion INT NOT NULL DEFAULT 1, -- orden
|
||||||
|
zona VARCHAR(40) NOT NULL DEFAULT 'main', -- main/header/footer/etc
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_lp_block (landing_id, block_id, zona),
|
||||||
|
KEY idx_landing_pos (landing_id, posicion),
|
||||||
|
CONSTRAINT fk_lpb_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_lpb_blk FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 3. Variantes A/B de una landing
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_variants (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT UNSIGNED NOT NULL,
|
||||||
|
nombre VARCHAR(120) NOT NULL,
|
||||||
|
peso DECIMAL(6,3) NOT NULL DEFAULT 1.000, -- tráfico relativo
|
||||||
|
estado ENUM('borrador','activo','pausado') NOT NULL DEFAULT 'activo',
|
||||||
|
-- contenido específico de la variante (opcional); si NULL, toma el de landing_pages
|
||||||
|
hero_image VARCHAR(255) NULL,
|
||||||
|
contenido_html MEDIUMTEXT NULL,
|
||||||
|
css_inline MEDIUMTEXT NULL,
|
||||||
|
js_inline MEDIUMTEXT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_lv_landing (landing_id, estado),
|
||||||
|
CONSTRAINT fk_lv_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 4. Programación por fecha (ventanas de publicación)
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_schedule (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT UNSIGNED NOT NULL,
|
||||||
|
start_at DATETIME NULL,
|
||||||
|
end_at DATETIME NULL,
|
||||||
|
estado ENUM('programada','activa','finalizada') NOT NULL DEFAULT 'programada',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_sched (landing_id, start_at, end_at),
|
||||||
|
CONSTRAINT fk_ls_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 5. Tracking: impresiones y conversiones por variante
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_variant_stats (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT UNSIGNED NOT NULL,
|
||||||
|
variant_id INT UNSIGNED NULL, -- NULL = página base (sin variante)
|
||||||
|
event_type ENUM('impression','conversion') NOT NULL,
|
||||||
|
session_id VARCHAR(64) NULL,
|
||||||
|
ip_address VARCHAR(64) NULL,
|
||||||
|
user_agent TEXT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_lv_evt (landing_id, variant_id, event_type, created_at),
|
||||||
|
CONSTRAINT fk_lvs_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_lvs_lv FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura de Base de Datos
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Landings principales
|
||||||
|
CREATE TABLE landing_pages (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
titulo VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
descripcion TEXT,
|
||||||
|
activo TINYINT(1) DEFAULT 1,
|
||||||
|
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Bloques reutilizables
|
||||||
|
CREATE TABLE landing_blocks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nombre VARCHAR(255) NOT NULL,
|
||||||
|
tipo ENUM('texto','html','imagen','video') DEFAULT 'texto',
|
||||||
|
contenido TEXT,
|
||||||
|
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Relación landing <-> bloques
|
||||||
|
CREATE TABLE landing_page_blocks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
block_id INT NOT NULL,
|
||||||
|
zona VARCHAR(40) DEFAULT 'main',
|
||||||
|
posicion INT DEFAULT 1,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Variantes para A/B testing
|
||||||
|
CREATE TABLE landing_variants (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
nombre VARCHAR(255),
|
||||||
|
porcentaje_traffic DECIMAL(5,2) DEFAULT 50.00,
|
||||||
|
activo TINYINT(1) DEFAULT 1,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Programación de publicaciones
|
||||||
|
CREATE TABLE landing_schedule (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
variant_id INT DEFAULT NULL,
|
||||||
|
fecha_inicio DATETIME,
|
||||||
|
fecha_fin DATETIME,
|
||||||
|
activo TINYINT(1) DEFAULT 1,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_pages (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
titulo VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
descripcion TEXT,
|
||||||
|
activo TINYINT(1) DEFAULT 1,
|
||||||
|
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_variants (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
nombre VARCHAR(255),
|
||||||
|
porcentaje_traffic DECIMAL(5,2) DEFAULT 50.00,
|
||||||
|
activo TINYINT(1) DEFAULT 1,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_blocks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nombre VARCHAR(255) NOT NULL,
|
||||||
|
tipo ENUM('texto','html','imagen','video') DEFAULT 'texto',
|
||||||
|
contenido TEXT,
|
||||||
|
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_page_blocks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
block_id INT NOT NULL,
|
||||||
|
zona VARCHAR(40) DEFAULT 'main',
|
||||||
|
posicion INT DEFAULT 1,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_forms (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
nombre VARCHAR(200),
|
||||||
|
email VARCHAR(200),
|
||||||
|
telefono VARCHAR(50),
|
||||||
|
mensaje TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ip_address VARCHAR(100),
|
||||||
|
user_agent TEXT,
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_variant_stats (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
landing_id INT NOT NULL,
|
||||||
|
variant_id INT NULL,
|
||||||
|
event_type ENUM('impression','conversion') NOT NULL,
|
||||||
|
session_id VARCHAR(100) NULL,
|
||||||
|
ip_address VARCHAR(100) NULL,
|
||||||
|
user_agent TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX (landing_id),
|
||||||
|
INDEX (variant_id),
|
||||||
|
INDEX (event_type),
|
||||||
|
INDEX (created_at),
|
||||||
|
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Configuración de base de datos (Sensible)
|
||||||
|
admin/db.php
|
||||||
|
|
||||||
|
# Contenidos de uploads (Mantener carpetas pero ignorar archivos subidos)
|
||||||
|
uploads/ruletas/*
|
||||||
|
uploads/rasca/*
|
||||||
|
uploads/rasca_premios/*
|
||||||
|
uploads/memoria/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
!uploads/**/.gitkeep
|
||||||
|
|
||||||
|
# Archivos de sistema
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Comprimidos y backups
|
||||||
|
*.zip
|
||||||
|
*.rar
|
||||||
|
*.7z
|
||||||
|
*.sql.bak
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Quitar .php de las URLs
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME}\.php -f
|
||||||
|
RewriteRule ^(.+)$ $1.php [L]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
ruleta/
|
||||||
|
│
|
||||||
|
├── rasca_index.html
|
||||||
|
├── rasca_style.css
|
||||||
|
├── rasca_app.js
|
||||||
|
├── rasca_wScratchpad.min.js
|
||||||
|
│
|
||||||
|
├── api/
|
||||||
|
│ ├── rasca_config.php
|
||||||
|
│ └── rasca_premio.php
|
||||||
|
│
|
||||||
|
└── admin/
|
||||||
|
├── rasca_config.php
|
||||||
|
├── rasca_premios.php
|
||||||
|
├── rasca_jugadas.php
|
||||||
|
└── db.php (ya existente en tu admin)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 475 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 807 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 388 KiB |
@@ -0,0 +1,87 @@
|
|||||||
|
# Portal ePromos — Hub de Juegos y Landings
|
||||||
|
|
||||||
|
Este proyecto es una plataforma gamificada para la captación de leads y aumento de engagement mediante experiencias interactivas. Incluye juegos de **Ruleta**, **Rasca y Gana**, y **Memoria**, además de un potente constructor de **Landing Pages**.
|
||||||
|
|
||||||
|
## 🚀 Requisitos del Sistema
|
||||||
|
- **PHP**: 8.0 o superior (Extensiones: `mysqli`, `mbstring`, `fileinfo`).
|
||||||
|
- **Base de Datos**: MariaDB/MySQL 10.4+.
|
||||||
|
- **Servidor Web**: Apache (con `mod_rewrite` habilitado) o Nginx.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Instalación y Configuración
|
||||||
|
|
||||||
|
### 1. Preparar el Código
|
||||||
|
1. Clona el repositorio en tu servidor.
|
||||||
|
2. Asegúrate de que las carpetas en `uploads/` tengan permisos de escritura:
|
||||||
|
```bash
|
||||||
|
chmod -R 775 uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurar Base de Datos
|
||||||
|
1. Crea la base de datos:
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE ruleta_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
2. Importa el esquema base: `ruleta_db.sql`.
|
||||||
|
3. Importa el esquema de landings: `-- Tabla principal de landings.txt`.
|
||||||
|
|
||||||
|
### 3. Configurar Credenciales
|
||||||
|
1. Renombra `admin/db.php.example` a `admin/db.php`.
|
||||||
|
2. Edita los valores de host, usuario y contraseña:
|
||||||
|
```php
|
||||||
|
$DB_HOST = 'tu_host';
|
||||||
|
$DB_USER = 'tu_usuario';
|
||||||
|
$DB_PASS = 'tu_password';
|
||||||
|
$DB_NAME = 'ruleta_db';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Ajuste de Rutas (Importante)
|
||||||
|
El código asume por defecto que el proyecto vive en la subcarpeta `/ruleta/`. Si lo instalas en la raíz o en otra ruta, ajusta las siguientes constantes:
|
||||||
|
- **Admin**: En `admin/ruletas.php`, `admin/rasca_config.php`, etc., cambia `$PUBLIC_DIR`.
|
||||||
|
- **Landings**: En `landings_public.php`, cambia `PUBLIC_LANDING_ROUTE`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Módulos Incluidos
|
||||||
|
|
||||||
|
### Ruleta
|
||||||
|
- Configuración de premios por ángulos (0-360°).
|
||||||
|
- Control de stock y probabilidad ponderada.
|
||||||
|
- **Ruta**: `index.html?ruleta_id=1`
|
||||||
|
|
||||||
|
### Rasca y Gana
|
||||||
|
- Capas de raspado personalizables.
|
||||||
|
- Premios dinámicos basados en stock.
|
||||||
|
- **Ruta**: `rasca_index.html?rasca_id=1`
|
||||||
|
|
||||||
|
### Memoria
|
||||||
|
- Gestión de pares de cartas.
|
||||||
|
- Posibilidad de enlazar con un premio final de ruleta.
|
||||||
|
- **Ruta**: `/memoria/?juego_id=1`
|
||||||
|
|
||||||
|
### Constructor de Landings
|
||||||
|
- Creación de landings vía Slug.
|
||||||
|
- A/B Testing integrado.
|
||||||
|
- Captura de formularios y exportación CSV.
|
||||||
|
- **Admin**: `/admin/landing.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Acceso al Panel de Administración
|
||||||
|
- **URL**: `/admin/`
|
||||||
|
- **Usuario por defecto**: `admin@localhost`
|
||||||
|
- **Contraseña temporal**: Para entrar por primera vez si no conoces la clave, ejecuta:
|
||||||
|
```sql
|
||||||
|
UPDATE admin_users SET password_hash='admin123' WHERE email='admin@localhost';
|
||||||
|
```
|
||||||
|
*Nota: El sistema re-hasheará la contraseña automáticamente al primer login.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Backup y Mantenimiento
|
||||||
|
- **Archivos**: Respaldar periódicamente la carpeta `uploads/`.
|
||||||
|
- **Base de Datos**: `mysqldump -u [user] -p ruleta_db > backup.sql`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Desarrollado para GLM por Isaac Aracena.*
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Quitar .php de las URLs
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME}\.php -f
|
||||||
|
RewriteRule ^(.+)$ $1.php [L]
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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()]);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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">← 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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/../admin/db.php';
|
||||||
|
|
||||||
|
$rasca_id = (int)($_GET['rasca_id'] ?? 0);
|
||||||
|
if ($rasca_id <= 0) { echo json_encode(['error'=>'rasca_id inválido']); exit; }
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("SELECT rasca_id,nombre,banner_1,banner_2,banner_3,banner_4,background_image,fullscreen_icon,fg_image,bg_image FROM rasca_config WHERE rasca_id=?");
|
||||||
|
$stmt->bind_param("i", $rasca_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$cfg = $stmt->get_result()->fetch_assoc();
|
||||||
|
|
||||||
|
if(!$cfg){
|
||||||
|
// Fallbacks por defecto
|
||||||
|
$cfg = [
|
||||||
|
'rasca_id'=>$rasca_id,'nombre'=>'Rasca',
|
||||||
|
'banner_1'=>'/ruleta/img/BANNER-1.png','banner_2'=>'/ruleta/img/BANNER-2.png',
|
||||||
|
'banner_3'=>'/ruleta/img/BANNER-3.png','banner_4'=>'/ruleta/img/BANNER-4.png',
|
||||||
|
'background_image'=>null,'fullscreen_icon'=>'/ruleta/img/fullscreen-icon.png',
|
||||||
|
'fg_image'=>null,'bg_image'=>null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/../admin/db.php';
|
||||||
|
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// JSON o form
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
if (!is_array($data)) $data = $_POST;
|
||||||
|
|
||||||
|
$rasca_id = isset($data['rasca_id']) ? (int)$data['rasca_id'] : 0;
|
||||||
|
$porcentaje = isset($data['percent']) ? (float)$data['percent'] : null; // opcional
|
||||||
|
|
||||||
|
if ($rasca_id <= 0) { echo json_encode(['ok'=>false,'error'=>'rasca_id inválido']); exit; }
|
||||||
|
|
||||||
|
// Premios activos con stock (NULL = ilimitado)
|
||||||
|
$sql = "SELECT id, nombre, probabilidad, stock, activo, imagen_url
|
||||||
|
FROM rasca_premios
|
||||||
|
WHERE rasca_id=? AND activo=1
|
||||||
|
AND (stock IS NULL OR stock > 0)
|
||||||
|
AND probabilidad > 0";
|
||||||
|
$st = $conn->prepare($sql);
|
||||||
|
$st->bind_param('i', $rasca_id);
|
||||||
|
$st->execute();
|
||||||
|
$st->bind_result($id, $nombre, $prob, $stock, $activo, $img);
|
||||||
|
|
||||||
|
$premios = []; $total = 0.0;
|
||||||
|
while ($st->fetch()) {
|
||||||
|
$prob = (float)$prob;
|
||||||
|
$premios[] = [
|
||||||
|
'id' => (int)$id,
|
||||||
|
'nombre' => $nombre,
|
||||||
|
'prob' => $prob,
|
||||||
|
'stock' => is_null($stock) ? null : (int)$stock,
|
||||||
|
'imagen_url' => $img ?: null
|
||||||
|
];
|
||||||
|
$total += max(0.0, $prob);
|
||||||
|
}
|
||||||
|
$st->close();
|
||||||
|
|
||||||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
|
||||||
|
if (!$premios) {
|
||||||
|
// Sin premios disponibles: registra jugada sin premio
|
||||||
|
$conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
|
||||||
|
VALUES (?,?,?,?,NULL)")
|
||||||
|
->bind_param('issd', $rasca_id, $ip, $ua, $porcentaje)
|
||||||
|
->execute();
|
||||||
|
echo json_encode(['ok'=>true,'sin_premio'=>true,'mensaje'=>'No hay premios activos o con stock']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($total <= 0) { echo json_encode(['ok'=>false,'error'=>'Probabilidades inválidas']); exit; }
|
||||||
|
|
||||||
|
// Selección ponderada
|
||||||
|
$rnd = mt_rand() / mt_getrandmax() * $total;
|
||||||
|
$acc = 0.0; $pick = null;
|
||||||
|
foreach ($premios as $p) { $acc += max(0.0, $p['prob']); if ($rnd <= $acc) { $pick = $p; break; } }
|
||||||
|
if ($pick === null) $pick = $premios[0];
|
||||||
|
|
||||||
|
// Transacción: descontar stock (si aplica) y registrar jugada
|
||||||
|
$conn->begin_transaction();
|
||||||
|
|
||||||
|
if ($pick['stock'] !== null) {
|
||||||
|
$upd = $conn->prepare("UPDATE rasca_premios SET stock = stock - 1 WHERE id=? AND stock > 0");
|
||||||
|
$upd->bind_param('i', $pick['id']);
|
||||||
|
$upd->execute();
|
||||||
|
if ($upd->affected_rows === 0) {
|
||||||
|
$conn->rollback();
|
||||||
|
// stock se agotó en carrera; registra jugada sin premio
|
||||||
|
$conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
|
||||||
|
VALUES (?,?,?,?,NULL)")
|
||||||
|
->bind_param('issd', $rasca_id, $ip, $ua, $porcentaje)
|
||||||
|
->execute();
|
||||||
|
echo json_encode(['ok'=>true,'sin_premio'=>true,'mensaje'=>'Stock agotado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ins = $conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
|
||||||
|
VALUES (?,?,?,?,?)");
|
||||||
|
$ins->bind_param('issdi', $rasca_id, $ip, $ua, $porcentaje, $pick['id']);
|
||||||
|
$ins->execute();
|
||||||
|
|
||||||
|
$conn->commit();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'premio' => $pick['nombre'],
|
||||||
|
'premio_id' => $pick['id'],
|
||||||
|
'imagen_url' => $pick['imagen_url']
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
@mysqli_rollback($conn);
|
||||||
|
echo json_encode(['ok'=>false,'error'=>'No hay articulos en stock.']);
|
||||||
|
//echo json_encode(['ok'=>false,'error'=>'Sorteo falló: '.$e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/../admin/db.php';
|
||||||
|
|
||||||
|
$id = (int)($_GET['ruleta_id'] ?? 1);
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("SELECT
|
||||||
|
imagen_ruleta, imagen_puntero,
|
||||||
|
banner_1, banner_2, banner_3, banner_4,
|
||||||
|
background_image, fullscreen_icon
|
||||||
|
FROM ruletas WHERE id=? LIMIT 1");
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
|
||||||
|
if (!$row = $res->fetch_assoc()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Ruleta no encontrada']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function norm(?string $p): ?string {
|
||||||
|
if (!$p) return null;
|
||||||
|
$p = str_replace('\\', '/', $p);
|
||||||
|
if ($p[0] !== '/') $p = '/' . ltrim($p, '/');
|
||||||
|
return $p;
|
||||||
|
}
|
||||||
|
$docroot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'imagen_ruleta','imagen_puntero',
|
||||||
|
'banner_1','banner_2','banner_3','banner_4',
|
||||||
|
'background_image','fullscreen_icon'
|
||||||
|
];
|
||||||
|
$out = [];
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$v = norm($row[$f] ?? null);
|
||||||
|
$out[$f] = ($v && file_exists($docroot . $v)) ? $v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($out);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
// api/seleccionar_premio.php - DB-driven selection
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/../admin/db.php';
|
||||||
|
|
||||||
|
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
$ruleta_id = isset($payload['ruleta_id']) ? (int)$payload['ruleta_id'] : (int)($_GET['ruleta_id'] ?? 1);
|
||||||
|
$user_agent = $payload['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||||
|
$ip_address = $payload['ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||||
|
|
||||||
|
// Leer premios disponibles
|
||||||
|
$stmt = $conn->prepare("SELECT id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo FROM premios WHERE ruleta_id=? AND cantidad > 0");
|
||||||
|
$stmt->bind_param("i", $ruleta_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$premios = [];
|
||||||
|
$totalProb = 0.0;
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$row['probabilidad'] = (float)$row['probabilidad'];
|
||||||
|
$totalProb += $row['probabilidad'];
|
||||||
|
$premios[] = $row;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if (!$premios || $totalProb <= 0) {
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode(['error'=>'No hay premios configurados o probabilidades en cero']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorteo ponderado
|
||||||
|
$pick = mt_rand() / mt_getrandmax() * $totalProb;
|
||||||
|
$acc = 0.0;
|
||||||
|
$ganador = null;
|
||||||
|
foreach ($premios as $p) {
|
||||||
|
$acc += $p['probabilidad'];
|
||||||
|
if ($pick <= $acc) { $ganador = $p; break; }
|
||||||
|
}
|
||||||
|
if (!$ganador) $ganador = end($premios);
|
||||||
|
|
||||||
|
// Guardar jugada
|
||||||
|
$stmt = $conn->prepare("INSERT INTO jugadas (ruleta_id, premio_id, ip_address, user_agent) VALUES (?,?,?,?)");
|
||||||
|
$stmt->bind_param("iiss", $ruleta_id, $ganador['id'], $ip_address, $user_agent);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Descontar inventario
|
||||||
|
$stmt = $conn->prepare("UPDATE premios SET cantidad = cantidad - 1 WHERE id=? AND cantidad > 0");
|
||||||
|
$stmt->bind_param("i", $ganador['id']);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'nombre' => $ganador['nombre'],
|
||||||
|
'inicio' => (float)$ganador['inicio_angulo'],
|
||||||
|
'fin' => (float)$ganador['fin_angulo']
|
||||||
|
]);
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
$servername = "208.109.71.144";
|
||||||
|
$username = "ruletaadmin1990";
|
||||||
|
$password = "Nd;GfPL#}*5s";
|
||||||
|
$dbname = "ruleta_db";
|
||||||
|
|
||||||
|
// Crear la conexión
|
||||||
|
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||||
|
|
||||||
|
// Verificar la conexión
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
die(json_encode(["error" => "Conexión fallida: " . $conn->connect_error]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener la IP del cliente en el servidor
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||||
|
|
||||||
|
// Recibir el user_agent desde el frontend
|
||||||
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
|
$user_agent = $data['user_agent'] ?? null;
|
||||||
|
|
||||||
|
// Establecer las variables de sesión en MariaDB
|
||||||
|
$conn->query("SET @user_agent = '$user_agent'");
|
||||||
|
$conn->query("SET @ip_address = '$ip_address'");
|
||||||
|
|
||||||
|
|
||||||
|
// Consultar los premios con cantidad mayor que 0
|
||||||
|
$sql = "SELECT id, nombre, probabilidad, cantidad, inicio, final FROM premios3 WHERE cantidad > 0";
|
||||||
|
$result = $conn->query($sql);
|
||||||
|
|
||||||
|
$premios = array();
|
||||||
|
$totalProbabilidad = 0;
|
||||||
|
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
// Calcular el total de probabilidades
|
||||||
|
while($row = $result->fetch_assoc()) {
|
||||||
|
$premios[] = $row;
|
||||||
|
$totalProbabilidad += $row['probabilidad'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar un número aleatorio entre 0 y el total de probabilidades
|
||||||
|
$random = mt_rand(0, $totalProbabilidad * 1000) / 1000; // Se multiplica por 1000 para trabajar con 3 decimales
|
||||||
|
$suma = 0;
|
||||||
|
|
||||||
|
// Seleccionar el premio basado en la probabilidad acumulada
|
||||||
|
foreach ($premios as $premio) {
|
||||||
|
$suma += $premio['probabilidad'];
|
||||||
|
if ($random <= $suma) {
|
||||||
|
// copiar esta parte Establecer las variables de sesión en MariaDB
|
||||||
|
$conn->query("SET @user_agent = '$user_agent'");
|
||||||
|
$conn->query("SET @ip_address = '$ip_address'");
|
||||||
|
// Actualizar la cantidad del premio seleccionado
|
||||||
|
$stmt = $conn->prepare("UPDATE premios3 SET cantidad = cantidad - 1 WHERE id = ?");
|
||||||
|
if ($stmt) {
|
||||||
|
$stmt->bind_param("i", $premio['id']);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Devolver el premio seleccionado con sus rangos de ángulo
|
||||||
|
echo json_encode([
|
||||||
|
'id' => $premio['id'],
|
||||||
|
'nombre' => $premio['nombre'],
|
||||||
|
'inicio' => $premio['inicio'],
|
||||||
|
'final' => $premio['final']
|
||||||
|
]);
|
||||||
|
exit(); // Salir inmediatamente después de devolver el premio seleccionado
|
||||||
|
} else {
|
||||||
|
echo json_encode(["error" => "Error en la preparación de la actualización"]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(["error" => "No hay premios disponibles"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
ruleta/ <-- Carpeta raíz del proyecto
|
||||||
|
│
|
||||||
|
├── index.html <-- Juego (frontend)
|
||||||
|
├── script.js <-- Lógica del juego
|
||||||
|
├── style.css <-- Estilos del juego
|
||||||
|
│
|
||||||
|
├── img/ <-- Imágenes por defecto del juego
|
||||||
|
│ ├── ruleta.png
|
||||||
|
│ ├── puntero.png
|
||||||
|
│ ├── BANNER-1.png
|
||||||
|
│ ├── BANNER-2.png
|
||||||
|
│ ├── BANNER-3.png
|
||||||
|
│ └── BANNER-4.png
|
||||||
|
│
|
||||||
|
├── uploads/ <-- Carpeta pública para subir imágenes
|
||||||
|
│ └── ruletas/ <-- Imágenes de ruletas y punteros personalizadas
|
||||||
|
│ ├── ruleta-abc123.png
|
||||||
|
│ └── puntero-xyz456.png
|
||||||
|
│
|
||||||
|
├── api/ <-- Endpoints de datos para el juego
|
||||||
|
│ ├── ruleta_config.php <-- Devuelve imágenes y config de ruleta según ID
|
||||||
|
│ └── seleccionar_premio.php <-- Devuelve premio ganador según ruleta_id
|
||||||
|
│
|
||||||
|
└── admin/ <-- Panel de administración (PHP)
|
||||||
|
├── index.php <-- Login de admin
|
||||||
|
├── dashboard.php <-- Panel principal
|
||||||
|
├── ruletas.php <-- CRUD de ruletas
|
||||||
|
├── premios.php <-- CRUD de premios
|
||||||
|
├── jugadas.php <-- Historial de jugadas
|
||||||
|
├── db.php <-- Conexión a la BD
|
||||||
|
└── (otros scripts .php según necesidad)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>¡Gracias por participar!</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #00b09b, #96c93d);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card p-4 shadow-lg mx-auto" style="max-width: 500px;">
|
||||||
|
<h1 class="mb-3">🎉 ¡Gracias por participar!</h1>
|
||||||
|
<p class="lead">Tu registro ha sido enviado correctamente.</p>
|
||||||
|
<p>En breve podrás acceder al juego o recibirás más instrucciones.</p>
|
||||||
|
<!-- <a href="/" class="btn btn-light mt-3">Volver al inicio</a>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 486 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
+108
@@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>GLM - Ruleta</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<!-- Estilos del juego -->
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Banners (se rellenan dinámicamente) -->
|
||||||
|
<div class="banner banner-1"></div>
|
||||||
|
<div class="banner banner-2"></div>
|
||||||
|
<div class="banner banner-3"></div>
|
||||||
|
<div class="banner banner-4"></div>
|
||||||
|
|
||||||
|
<!-- Contenido principal -->
|
||||||
|
<div class="content">
|
||||||
|
<h1></h1>
|
||||||
|
<div class="game">
|
||||||
|
<!-- Se asignan src dinámicamente -->
|
||||||
|
<img id="ruleta" alt="Ruleta" />
|
||||||
|
<img id="puntero" alt="Puntero" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de pantalla completa (icono dinámico) -->
|
||||||
|
<div class="fullscreen-btn" id="fullscreen-btn"></div>
|
||||||
|
|
||||||
|
<!-- Mensaje para orientación vertical -->
|
||||||
|
<div class="landscape-message">
|
||||||
|
Por favor, gira tu dispositivo para ver el contenido en horizontal.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de confirmación -->
|
||||||
|
<div id="confirm-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<p>¿Deseas participar en la promoción?</p>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="confirm-yes">Sí</button>
|
||||||
|
<button id="confirm-no">No</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de agradecimiento -->
|
||||||
|
<div id="thank-you-modal" class="popup-modal" style="display: none;">
|
||||||
|
<div class="popup-content">
|
||||||
|
<h2 id="thank-you-message"></h2>
|
||||||
|
<p id="thank-you-details"></p>
|
||||||
|
<button id="redirect-button">Cerrar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confeti -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Cargar imágenes/estilos por ruleta desde la API -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const ruletaId = params.get("ruleta_id") || 1;
|
||||||
|
|
||||||
|
fetch(`api/ruleta_config.php?ruleta_id=${ruletaId}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
// Ruleta & Puntero
|
||||||
|
const imgRuleta = document.getElementById("ruleta");
|
||||||
|
const imgPuntero = document.getElementById("puntero");
|
||||||
|
imgRuleta.src = data.imagen_ruleta || "img/ruleta.png";
|
||||||
|
imgPuntero.src = data.imagen_puntero || "img/puntero.png";
|
||||||
|
|
||||||
|
// Banners
|
||||||
|
if (data.banner_1) document.querySelector(".banner-1").style.backgroundImage = `url("${data.banner_1}")`;
|
||||||
|
if (data.banner_2) document.querySelector(".banner-2").style.backgroundImage = `url("${data.banner_2}")`;
|
||||||
|
if (data.banner_3) document.querySelector(".banner-3").style.backgroundImage = `url("${data.banner_3}")`;
|
||||||
|
if (data.banner_4) document.querySelector(".banner-4").style.backgroundImage = `url("${data.banner_4}")`;
|
||||||
|
|
||||||
|
// Background de la página
|
||||||
|
if (data.background_image) {
|
||||||
|
document.body.style.backgroundImage = `url("${data.background_image}")`;
|
||||||
|
document.body.style.backgroundSize = "cover";
|
||||||
|
document.body.style.backgroundRepeat = "no-repeat";
|
||||||
|
document.body.style.backgroundPosition= "center center";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icono del botón Fullscreen
|
||||||
|
if (data.fullscreen_icon) {
|
||||||
|
const fs = document.getElementById("fullscreen-btn");
|
||||||
|
fs.style.backgroundImage = `url("${data.fullscreen_icon}")`;
|
||||||
|
fs.style.backgroundRepeat = "no-repeat";
|
||||||
|
fs.style.backgroundSize = "contain";
|
||||||
|
fs.style.backgroundPosition= "center";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error cargando configuración de ruleta:", err);
|
||||||
|
// Fallback mínimo si falla
|
||||||
|
document.getElementById("ruleta").src = "img/ruleta.png";
|
||||||
|
document.getElementById("puntero").src = "img/puntero.png";
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Lógica del juego (usa seleccionar_premio.php y respeta ruleta_id) -->
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/admin/db.php';
|
||||||
|
require_once __DIR__ . '/lib/landing_helpers.php';
|
||||||
|
|
||||||
|
$slug = trim($_GET['slug'] ?? '');
|
||||||
|
if ($slug==='') { http_response_code(404); echo "Landing no encontrada."; exit; }
|
||||||
|
|
||||||
|
$st = $conn->prepare("SELECT * FROM landing_pages WHERE slug=? AND estado IN ('publicado','borrador') LIMIT 1");
|
||||||
|
$st->bind_param('s',$slug);
|
||||||
|
$st->execute();
|
||||||
|
$landing = $st->get_result()->fetch_assoc();
|
||||||
|
if(!$landing){ http_response_code(404); echo "Landing no encontrada."; exit; }
|
||||||
|
|
||||||
|
$landing_id = (int)$landing['id'];
|
||||||
|
|
||||||
|
// validar schedule
|
||||||
|
if ($landing['estado']!=='publicado' || !lp_schedule_active($conn, $landing_id)) {
|
||||||
|
http_response_code(404); echo "Landing no publicada."; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// variante A/B
|
||||||
|
$variant = lp_pick_variant($conn, $landing_id); // null = base
|
||||||
|
// contenido final = variante override o base
|
||||||
|
$hero = $variant['hero_image'] ?? ($landing['hero_image'] ?? '');
|
||||||
|
$html = $variant['contenido_html'] ?? ($landing['contenido_html'] ?? '');
|
||||||
|
$css = ($landing['css_inline'] ?? '') . "\n" . ($variant['css_inline'] ?? '');
|
||||||
|
$js = ($landing['js_inline'] ?? '') . "\n" . ($variant['js_inline'] ?? '');
|
||||||
|
|
||||||
|
// bloques
|
||||||
|
$bloques = lp_load_blocks($conn, $landing_id);
|
||||||
|
|
||||||
|
// tracking impresión
|
||||||
|
$session_id = session_id() ?: bin2hex(random_bytes(8));
|
||||||
|
$vid = $variant['id'] ?? null;
|
||||||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
$ins = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent)
|
||||||
|
VALUES (?,?,?,?,?,?)");
|
||||||
|
$evt='impression';
|
||||||
|
$ins->bind_param('iissss',$landing_id,$vid,$evt,$session_id,$ip,$ua);
|
||||||
|
$ins->execute();
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title><?= htmlspecialchars($landing['meta_title'] ?: $landing['titulo']) ?></title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<?php if(!empty($landing['meta_desc'])): ?>
|
||||||
|
<meta name="description" content="<?= htmlspecialchars($landing['meta_desc']) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if(!empty($landing['canonical_url'])): ?>
|
||||||
|
<link rel="canonical" href="<?= htmlspecialchars($landing['canonical_url']) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<style>
|
||||||
|
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto;line-height:1.5;color:#222}
|
||||||
|
.hero{min-height:50vh;display:flex;align-items:center;justify-content:center;background-size:cover;background-position:center;text-align:center;padding:4rem 1rem}
|
||||||
|
.container{max-width:980px;margin:0 auto;padding:2rem 1rem}
|
||||||
|
.btn{display:inline-block;padding:.75rem 1.25rem;border-radius:.5rem;background:#0d6efd;color:#fff;text-decoration:none}
|
||||||
|
<?= $css ?>
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hero" style="background-image:url('<?= htmlspecialchars($hero) ?>');">
|
||||||
|
<div class="container">
|
||||||
|
<h1><?= htmlspecialchars($landing['titulo']) ?></h1>
|
||||||
|
<p><?= htmlspecialchars($landing['meta_desc'] ?? '') ?></p>
|
||||||
|
<a class="btn" href="#form">Quiero más info</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- Bloques zona=header -->
|
||||||
|
<?php if(!empty($bloques['header'])): foreach($bloques['header'] as $b): ?>
|
||||||
|
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
|
||||||
|
<!-- Contenido administrable (base/variante) -->
|
||||||
|
<?= $html ?>
|
||||||
|
|
||||||
|
<!-- Bloques zona=main -->
|
||||||
|
<?php if(!empty($bloques['main'])): foreach($bloques['main'] as $b): ?>
|
||||||
|
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
|
||||||
|
<hr id="form">
|
||||||
|
<h3>Contáctanos</h3>
|
||||||
|
<form method="post" action="landing_form.php" onsubmit="lpConv();">
|
||||||
|
<input type="hidden" name="landing_id" value="<?= (int)$landing_id ?>">
|
||||||
|
<input type="hidden" name="variant_id" value="<?= (int)($variant['id'] ?? 0) ?>">
|
||||||
|
<div><input name="nombre" placeholder="Nombre" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
|
||||||
|
<div><input name="email" placeholder="Email" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
|
||||||
|
<div><input name="telefono" placeholder="Teléfono" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
|
||||||
|
<div><textarea name="mensaje" placeholder="Mensaje" rows="4" style="width:100%;padding:.6rem;margin:.25rem 0"></textarea></div>
|
||||||
|
<button class="btn">Enviar</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Bloques zona=footer -->
|
||||||
|
<?php if(!empty($bloques['footer'])): foreach($bloques['footer'] as $b): ?>
|
||||||
|
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// registra conversión al enviar (fire-and-forget)
|
||||||
|
function lpConv(){
|
||||||
|
navigator.sendBeacon('landing_track.php', new URLSearchParams({
|
||||||
|
landing_id: '<?= (int)$landing_id ?>',
|
||||||
|
variant_id: '<?= (int)($variant['id'] ?? 0) ?>',
|
||||||
|
event_type: 'conversion',
|
||||||
|
session_id: '<?= htmlspecialchars($session_id) ?>'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
<?= $js ?>
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/admin/db.php';
|
||||||
|
|
||||||
|
function post_json($url, array $payload, $secret=null){
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_HTTPHEADER => array_filter([
|
||||||
|
'Content-Type: application/json',
|
||||||
|
$secret ? 'Authorization: Bearer '.$secret : null
|
||||||
|
]),
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
|
||||||
|
CURLOPT_TIMEOUT => 8
|
||||||
|
]);
|
||||||
|
$res = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return $err ?: $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safe_redirect(string $url, int $status = 303){
|
||||||
|
$url = str_replace(["\r","\n"], '', $url);
|
||||||
|
header("Location: ".$url, true, $status);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_abs_url(string $url): bool {
|
||||||
|
return (bool) filter_var($url, FILTER_VALIDATE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_rel_path(string $url): bool {
|
||||||
|
// /ruta o ruta (sin esquema)
|
||||||
|
return preg_match('#^/[^/]|^[^:]+$#', $url) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD']==='POST'){
|
||||||
|
$landing_id = (int)($_POST['landing_id'] ?? 0);
|
||||||
|
$variant_id = (int)($_POST['variant_id'] ?? 0);
|
||||||
|
$nombre = trim($_POST['nombre'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$telefono= trim($_POST['telefono'] ?? '');
|
||||||
|
$mensaje = trim($_POST['mensaje'] ?? '');
|
||||||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? null);
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
|
||||||
|
if($landing_id>0){
|
||||||
|
// 1) Guardar lead local
|
||||||
|
$st=$conn->prepare("INSERT INTO landing_forms (landing_id,nombre,email,telefono,mensaje,ip_address,user_agent) VALUES (?,?,?,?,?,?,?)");
|
||||||
|
$st->bind_param('issssss',$landing_id,$nombre,$email,$telefono,$mensaje,$ip,$ua);
|
||||||
|
$st->execute();
|
||||||
|
|
||||||
|
// 2) Track conversión para la variante elegida
|
||||||
|
$evt='conversion';
|
||||||
|
$sess = session_id() ?: null;
|
||||||
|
$vid = $variant_id > 0 ? $variant_id : null;
|
||||||
|
$st2 = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent) VALUES (?,?,?,?,?,?)");
|
||||||
|
$st2->bind_param('iissss',$landing_id,$vid,$evt,$sess,$ip,$ua);
|
||||||
|
$st2->execute();
|
||||||
|
|
||||||
|
// 3) Enviar a CRMs configurados
|
||||||
|
$hooks = $conn->query("SELECT endpoint, method, secret FROM landing_crm_hooks WHERE landing_id={$landing_id} AND active=1")->fetch_all(MYSQLI_ASSOC);
|
||||||
|
if($hooks){
|
||||||
|
$payload = [
|
||||||
|
'landing_id'=>$landing_id,
|
||||||
|
'variant_id'=>$vid,
|
||||||
|
'lead'=>[
|
||||||
|
'nombre'=>$nombre,'email'=>$email,'telefono'=>$telefono,'mensaje'=>$mensaje,
|
||||||
|
'ip'=>$ip,'user_agent'=>$ua,'created_at'=>date('c')
|
||||||
|
],
|
||||||
|
'utm'=>[
|
||||||
|
'source'=>$_POST['utm_source']??null,
|
||||||
|
'medium'=>$_POST['utm_medium']??null,
|
||||||
|
'campaign'=>$_POST['utm_campaign']??null
|
||||||
|
]
|
||||||
|
];
|
||||||
|
foreach($hooks as $h){
|
||||||
|
if (strtoupper($h['method'])==='POST') {
|
||||||
|
post_json($h['endpoint'], $payload, $h['secret'] ?? null);
|
||||||
|
} else {
|
||||||
|
@file_get_contents($h['endpoint']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Redirigir al juego (game_url) si existe; si no, fallback a gracias.html
|
||||||
|
try {
|
||||||
|
$st3 = $conn->prepare("SELECT game_url FROM landing_pages WHERE id=? LIMIT 1");
|
||||||
|
$st3->bind_param('i', $landing_id);
|
||||||
|
$st3->execute();
|
||||||
|
$res = $st3->get_result()->fetch_assoc();
|
||||||
|
$game_url = trim((string)($res['game_url'] ?? ''));
|
||||||
|
|
||||||
|
if ($game_url !== '') {
|
||||||
|
if (is_abs_url($game_url)) {
|
||||||
|
safe_redirect($game_url, 303);
|
||||||
|
} elseif (is_rel_path($game_url)) {
|
||||||
|
// Normaliza si deseas forzar slash inicial:
|
||||||
|
// $game_url = '/' . ltrim($game_url, '/');
|
||||||
|
safe_redirect($game_url, 303);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Si hay error, continua a fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
safe_redirect('gracias.html', 303);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Solicitud inválida.";
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/admin/db.php';
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$landing_id = (int)($_POST['landing_id'] ?? $_GET['landing_id'] ?? 0);
|
||||||
|
$variant_id = (int)($_POST['variant_id'] ?? $_GET['variant_id'] ?? 0);
|
||||||
|
$event_type = $_POST['event_type'] ?? $_GET['event_type'] ?? 'conversion';
|
||||||
|
$session_id = $_POST['session_id'] ?? $_GET['session_id'] ?? null;
|
||||||
|
|
||||||
|
if($landing_id<=0 || !in_array($event_type, ['impression','conversion'], true)) { http_response_code(400); echo "bad"; exit; }
|
||||||
|
|
||||||
|
$vid = $variant_id>0 ? $variant_id : null;
|
||||||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
|
||||||
|
$st = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent) VALUES (?,?,?,?,?,?)");
|
||||||
|
$st->bind_param('iissss', $landing_id, $vid, $event_type, $session_id, $ip, $ua);
|
||||||
|
$st->execute();
|
||||||
|
|
||||||
|
echo "ok";
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
// landings_public.php
|
||||||
|
// Lista pública de landings con estado 'publicado' + menú + filtros
|
||||||
|
|
||||||
|
require_once __DIR__ . '/admin/db.php'; // ajusta la ruta si tu db.php está en otra carpeta
|
||||||
|
|
||||||
|
// Ruta pública para ver un landing por slug
|
||||||
|
const PUBLIC_LANDING_ROUTE = '/ruleta/landing.php?slug='; // p.ej. /landing.php?slug=mi-landing
|
||||||
|
|
||||||
|
function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||||
|
function valid_date($s){ return (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $s ?? ''); }
|
||||||
|
|
||||||
|
// Parámetros de filtros
|
||||||
|
$term = trim($_GET['q'] ?? '');
|
||||||
|
$with_game = isset($_GET['con_juego']) ? 1 : 0;
|
||||||
|
$with_image = isset($_GET['con_imagen']) ? 1 : 0;
|
||||||
|
$desde = $_GET['desde'] ?? '';
|
||||||
|
$hasta = $_GET['hasta'] ?? '';
|
||||||
|
$sort = $_GET['orden'] ?? 'recientes'; // recientes | alfa
|
||||||
|
$pageSize = (int)($_GET['pp'] ?? 12);
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
|
||||||
|
if (!in_array($pageSize, [6,12,24,48,96], true)) $pageSize = 12;
|
||||||
|
$offset = ($page - 1) * $pageSize;
|
||||||
|
|
||||||
|
// Construcción del WHERE dinámico
|
||||||
|
$where = "estado='publicado'";
|
||||||
|
$params = []; $types = '';
|
||||||
|
|
||||||
|
if ($term !== '') {
|
||||||
|
$where .= " AND (titulo LIKE ? OR slug LIKE ? OR meta_title LIKE ? OR meta_desc LIKE ?)";
|
||||||
|
$like = '%'.$term.'%';
|
||||||
|
array_push($params, $like, $like, $like, $like); $types .= 'ssss';
|
||||||
|
}
|
||||||
|
if ($with_game) {
|
||||||
|
$where .= " AND game_url IS NOT NULL AND game_url <> ''";
|
||||||
|
}
|
||||||
|
if ($with_image) {
|
||||||
|
$where .= " AND hero_image IS NOT NULL AND hero_image <> ''";
|
||||||
|
}
|
||||||
|
if (valid_date($desde)) {
|
||||||
|
$where .= " AND (updated_at IS NULL OR updated_at >= ?)";
|
||||||
|
$params[] = $desde . ' 00:00:00';
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
if (valid_date($hasta)) {
|
||||||
|
$where .= " AND (updated_at IS NULL OR updated_at <= ?)";
|
||||||
|
$params[] = $hasta . ' 23:59:59';
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orden
|
||||||
|
switch ($sort) {
|
||||||
|
case 'alfa':
|
||||||
|
$orderSql = "titulo ASC, id DESC";
|
||||||
|
break;
|
||||||
|
case 'recientes':
|
||||||
|
default:
|
||||||
|
$orderSql = "COALESCE(updated_at, '1970-01-01 00:00:00') DESC, id DESC";
|
||||||
|
$sort = 'recientes';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Total para paginación ----
|
||||||
|
$sqlCount = "SELECT COUNT(*) AS total FROM landing_pages WHERE $where";
|
||||||
|
$stmt = $conn->prepare($sqlCount);
|
||||||
|
if ($types) { $stmt->bind_param($types, ...$params); }
|
||||||
|
$stmt->execute();
|
||||||
|
$total = (int)($stmt->get_result()->fetch_assoc()['total'] ?? 0);
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
$total_pages = max(1, (int)ceil($total / $pageSize));
|
||||||
|
if ($page > $total_pages) { $page = $total_pages; $offset = ($page - 1) * $pageSize; }
|
||||||
|
|
||||||
|
// ---- Consulta principal ----
|
||||||
|
$sql = "SELECT id, titulo, slug, hero_image, meta_desc, updated_at, game_url
|
||||||
|
FROM landing_pages
|
||||||
|
WHERE $where
|
||||||
|
ORDER BY $orderSql
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $conn->prepare($sql);
|
||||||
|
|
||||||
|
// Bind evitando “positional after unpacking”
|
||||||
|
$limitVal = $pageSize;
|
||||||
|
$offsetVal = $offset;
|
||||||
|
|
||||||
|
if ($types) {
|
||||||
|
$types2 = $types . 'ii';
|
||||||
|
$bindValues = array_merge($params, [$limitVal, $offsetVal]);
|
||||||
|
$stmt->bind_param($types2, ...$bindValues);
|
||||||
|
} else {
|
||||||
|
$stmt->bind_param('ii', $limitVal, $offsetVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Fallback de imagen
|
||||||
|
$fallbackImg = 'https://picsum.photos/seed/landings/800/450';
|
||||||
|
|
||||||
|
// Helper para mantener parámetros en paginación
|
||||||
|
$baseParams = $_GET; unset($baseParams['page']);
|
||||||
|
$base = $_SERVER['PHP_SELF'].'?'.http_build_query($baseParams);
|
||||||
|
$prev = max(1, $page-1); $next = min($total_pages, $page+1);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Landings activos</title>
|
||||||
|
<meta name="description" content="Explora los landings activos y participa en las campañas disponibles.">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.card-img-top{ aspect-ratio: 16/9; object-fit: cover; }
|
||||||
|
.line-clamp-2{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
||||||
|
.filter-toggle { cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- MENÚ -->
|
||||||
|
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="<?= e($_SERVER['PHP_SELF']) ?>">GLM · Landings</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="mainNav">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item"><a class="nav-link active" href="<?= e($_SERVER['PHP_SELF']) ?>">Landings</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/">Inicio</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/contacto.php">Contacto</a></li>
|
||||||
|
</ul>
|
||||||
|
<!-- BÚSQUEDA RÁPIDA EN MENÚ -->
|
||||||
|
<form class="d-flex gap-2" method="get" action="">
|
||||||
|
<input name="q" class="form-control form-control-sm" placeholder="Buscar..." value="<?= e($term) ?>">
|
||||||
|
<button class="btn btn-outline-light btn-sm">Buscar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container py-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
|
||||||
|
<h1 class="h4 mb-0">Landings activos</h1>
|
||||||
|
<span class="text-muted"><?= $total ?> resultado<?= $total===1?'':'s' ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PANEL DE FILTROS -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="">
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Buscar</label>
|
||||||
|
<input name="q" class="form-control" placeholder="Título, slug, meta..."
|
||||||
|
value="<?= e($term) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<div class="form-check mt-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="con_juego" name="con_juego" <?= $with_game?'checked':'' ?>>
|
||||||
|
<label class="form-check-label" for="con_juego">Con juego</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<div class="form-check mt-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="con_imagen" name="con_imagen" <?= $with_image?'checked':'' ?>>
|
||||||
|
<label class="form-check-label" for="con_imagen">Con imagen</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<label class="form-label">Desde</label>
|
||||||
|
<input type="date" name="desde" class="form-control" value="<?= e(valid_date($desde)?$desde:'') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<label class="form-label">Hasta</label>
|
||||||
|
<input type="date" name="hasta" class="form-control" value="<?= e(valid_date($hasta)?$hasta:'') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<label class="form-label">Orden</label>
|
||||||
|
<select name="orden" class="form-select">
|
||||||
|
<option value="recientes" <?= $sort==='recientes'?'selected':'' ?>>Más recientes</option>
|
||||||
|
<option value="alfa" <?= $sort==='alfa'?'selected':'' ?>>Alfabético (A-Z)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<label class="form-label">Por página</label>
|
||||||
|
<select name="pp" class="form-select">
|
||||||
|
<?php foreach ([6,12,24,48,96] as $pp): ?>
|
||||||
|
<option value="<?= $pp ?>" <?= $pageSize===$pp?'selected':'' ?>><?= $pp ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-auto ms-auto">
|
||||||
|
<button class="btn btn-primary">Aplicar</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="<?= e($_SERVER['PHP_SELF']) ?>">Limpiar</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LISTADO -->
|
||||||
|
<?php if (!$rows): ?>
|
||||||
|
<div class="alert alert-info">No hay landings publicados que coincidan con los filtros.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-4">
|
||||||
|
<?php foreach ($rows as $r): ?>
|
||||||
|
<div class="col-12 col-sm-6 col-lg-4">
|
||||||
|
<div class="card h-100 shadow-sm">
|
||||||
|
<img class="card-img-top" loading="lazy"
|
||||||
|
src="<?= e($r['hero_image'] ?: $fallbackImg) ?>"
|
||||||
|
alt="<?= e($r['titulo']) ?>">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h2 class="h6 card-title mb-2"><?= e($r['titulo']) ?></h2>
|
||||||
|
<?php if (!empty($r['meta_desc'])): ?>
|
||||||
|
<p class="card-text text-muted small line-clamp-2"><?= e($r['meta_desc']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="mt-auto d-flex flex-wrap gap-2">
|
||||||
|
<a class="btn btn-primary btn-sm"
|
||||||
|
href="<?= e(PUBLIC_LANDING_ROUTE . urlencode($r['slug'])) ?>">
|
||||||
|
Ver landing
|
||||||
|
</a>
|
||||||
|
<?php if (!empty($r['game_url'])): ?>
|
||||||
|
<a class="btn btn-outline-success btn-sm" target="_blank" rel="noopener"
|
||||||
|
href="<?= e($r['game_url']) ?>">
|
||||||
|
Jugar
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted">Actualizado: <?= e($r['updated_at'] ?: '—') ?></small>
|
||||||
|
<?php if (!empty($r['slug'])): ?>
|
||||||
|
<span class="badge text-bg-light"><?= e($r['slug']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginación -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<nav class="mt-4" aria-label="Paginación">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li class="page-item <?= $page<=1?'disabled':'' ?>">
|
||||||
|
<a class="page-link" href="<?= e($base.'&page='.$prev) ?>">Anterior</a>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
$start = max(1, $page-2); $end = min($total_pages, $page+2);
|
||||||
|
for ($i=$start; $i<=$end; $i++): ?>
|
||||||
|
<li class="page-item <?= $i===$page?'active':'' ?>">
|
||||||
|
<a class="page-link" href="<?= e($base.'&page='.$i) ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page>=$total_pages?'disabled':'' ?>">
|
||||||
|
<a class="page-link" href="<?= e($base.'&page='.$next) ?>">Siguiente</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-top py-4">
|
||||||
|
<div class="container text-center text-muted small">
|
||||||
|
© <?= date('Y') ?> — Landings activos
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
function lp_now() { return new DateTime('now'); }
|
||||||
|
|
||||||
|
function lp_schedule_active(mysqli $conn, int $landing_id): bool {
|
||||||
|
// activa si NO hay schedule o si hay una ventana vigente
|
||||||
|
$now = lp_now()->format('Y-m-d H:i:s');
|
||||||
|
$st = $conn->prepare("SELECT 1 FROM landing_schedule
|
||||||
|
WHERE landing_id=? AND (start_at IS NULL OR start_at<=?) AND (end_at IS NULL OR end_at>=?)
|
||||||
|
LIMIT 1");
|
||||||
|
$st->bind_param('iss', $landing_id, $now, $now);
|
||||||
|
$st->execute(); $st->store_result();
|
||||||
|
return $st->num_rows > 0 || $conn->query("SELECT COUNT(*) c FROM landing_schedule WHERE landing_id={$landing_id}")->fetch_assoc()['c'] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lp_pick_variant(mysqli $conn, int $landing_id): ?array {
|
||||||
|
// cookie stickiness
|
||||||
|
$cookieName = "lpv_".$landing_id;
|
||||||
|
if (!empty($_COOKIE[$cookieName])) {
|
||||||
|
$vid = (int)$_COOKIE[$cookieName];
|
||||||
|
$st = $conn->prepare("SELECT id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline
|
||||||
|
FROM landing_variants WHERE id=? AND landing_id=? AND estado='activo' LIMIT 1");
|
||||||
|
$st->bind_param('ii', $vid, $landing_id);
|
||||||
|
$st->execute();
|
||||||
|
$res = $st->get_result()->fetch_assoc();
|
||||||
|
if ($res) return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// carga variantes activas
|
||||||
|
$st = $conn->prepare("SELECT id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline
|
||||||
|
FROM landing_variants WHERE landing_id=? AND estado='activo'");
|
||||||
|
$st->bind_param('i', $landing_id);
|
||||||
|
$st->execute();
|
||||||
|
$r = $st->get_result();
|
||||||
|
$vars = []; $total = 0.0;
|
||||||
|
while ($row = $r->fetch_assoc()) {
|
||||||
|
$w = max(0.0, (float)$row['peso']);
|
||||||
|
$row['_w'] = $w;
|
||||||
|
$vars[] = $row;
|
||||||
|
$total += $w;
|
||||||
|
}
|
||||||
|
if (!$vars || $total <= 0) return null;
|
||||||
|
|
||||||
|
// ruleta ponderada
|
||||||
|
$rnd = mt_rand() / mt_getrandmax() * $total;
|
||||||
|
$acc = 0.0;
|
||||||
|
foreach ($vars as $v) {
|
||||||
|
$acc += $v['_w'];
|
||||||
|
if ($rnd <= $acc) {
|
||||||
|
// cookie 7 días
|
||||||
|
setcookie($cookieName, (string)$v['id'], time()+7*86400, "/");
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $vars[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function lp_load_blocks(mysqli $conn, int $landing_id): array {
|
||||||
|
// bloques por zonas (main/header/footer, etc.)
|
||||||
|
$sql = "SELECT lb.id, lb.nombre, lb.tipo, lb.contenido, lpb.zona, lpb.posicion
|
||||||
|
FROM landing_page_blocks lpb
|
||||||
|
JOIN landing_blocks lb ON lb.id=lpb.block_id
|
||||||
|
WHERE lpb.landing_id=?
|
||||||
|
ORDER BY lpb.zona, lpb.posicion ASC";
|
||||||
|
$st=$conn->prepare($sql);
|
||||||
|
$st->bind_param('i',$landing_id);
|
||||||
|
$st->execute();
|
||||||
|
$res=$st->get_result();
|
||||||
|
$out=[];
|
||||||
|
while($row=$res->fetch_assoc()){
|
||||||
|
$out[$row['zona']][]=$row; // agrupa por zona
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
// memoria/index.php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/../admin/db.php'; // ajusta la ruta si tu db.php está en otro lugar
|
||||||
|
|
||||||
|
$juego_id = (int)($_GET['juego_id'] ?? 1);
|
||||||
|
|
||||||
|
// 1) Cargar config del juego (banners, fondo, reverso, icono FS y ruleta_id a la que enlaza)
|
||||||
|
$stmt = $conn->prepare("SELECT
|
||||||
|
nombre, banner_1, banner_2, banner_3, banner_4,
|
||||||
|
background_image, fullscreen_icon, reverso_default, ruleta_id
|
||||||
|
FROM juegos_config
|
||||||
|
WHERE juego_id = ? LIMIT 1");
|
||||||
|
$stmt->bind_param("i", $juego_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$config = $stmt->get_result()->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if (!$config) {
|
||||||
|
die("Config del juego no encontrada (juego_id=$juego_id).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Cargar cartas activas (traseras a emparejar). Puedes manejar n pares desde la BD.
|
||||||
|
$cartas = [];
|
||||||
|
$res = $conn->prepare("SELECT id, nombre, imagen_trasera, COALESCE(imagen_frontal, '') AS imagen_frontal
|
||||||
|
FROM cartas
|
||||||
|
WHERE juego_id = ? AND activo = 1");
|
||||||
|
$res->bind_param("i", $juego_id);
|
||||||
|
$res->execute();
|
||||||
|
$q = $res->get_result();
|
||||||
|
while ($row = $q->fetch_assoc()) { $cartas[] = $row; }
|
||||||
|
$res->close();
|
||||||
|
|
||||||
|
if (!$cartas) {
|
||||||
|
die("No hay cartas activas para este juego (juego_id=$juego_id).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverso por defecto (lado frontal que se ve al inicio)
|
||||||
|
$reverso = $config['reverso_default'] ?: '/memoria/images/back.png';
|
||||||
|
|
||||||
|
// Generar parejas: duplico la lista de cartas. Si tienes más de 12, puedes recortar.
|
||||||
|
$base = $cartas;
|
||||||
|
shuffle($base);
|
||||||
|
$max_pairs = 6; // cambia si quieres más o menos pares
|
||||||
|
$base = array_slice($base, 0, $max_pairs);
|
||||||
|
|
||||||
|
// Duplicar para formar pares y asignar una clave de pareja consistente
|
||||||
|
$deck = [];
|
||||||
|
$pair_idx = 1;
|
||||||
|
foreach ($base as $c) {
|
||||||
|
$key = 'pair_' . $pair_idx++;
|
||||||
|
$deck[] = ['key'=>$key, 'back'=>$c['imagen_trasera'], 'front'=>$c['imagen_frontal'] ?: $reverso];
|
||||||
|
$deck[] = ['key'=>$key, 'back'=>$c['imagen_trasera'], 'front'=>$c['imagen_frontal'] ?: $reverso];
|
||||||
|
}
|
||||||
|
shuffle($deck);
|
||||||
|
|
||||||
|
// Pasar al front la config necesaria
|
||||||
|
$CONFIG_JS = [
|
||||||
|
'juego_id' => $juego_id,
|
||||||
|
'ruleta_id' => (int)$config['ruleta_id'],
|
||||||
|
'banners' => [
|
||||||
|
'banner_1' => $config['banner_1'] ?: null,
|
||||||
|
'banner_2' => $config['banner_2'] ?: null,
|
||||||
|
'banner_3' => $config['banner_3'] ?: null,
|
||||||
|
'banner_4' => $config['banner_4'] ?: null,
|
||||||
|
],
|
||||||
|
'background_image' => $config['background_image'] ?: null,
|
||||||
|
'fullscreen_icon' => $config['fullscreen_icon'] ?: null,
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Juego de Memoria - ePromo</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Banners dinámicos -->
|
||||||
|
<div class="banner banner-1"></div>
|
||||||
|
<div class="banner banner-2"></div>
|
||||||
|
<div class="banner banner-3"></div>
|
||||||
|
<div class="banner banner-4"></div>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<ul class="cards" id="cards">
|
||||||
|
<?php foreach ($deck as $card): ?>
|
||||||
|
<li class="card" data-key="<?= htmlspecialchars($card['key']) ?>">
|
||||||
|
<div class="view front-view">
|
||||||
|
<img src="<?= htmlspecialchars($card['front']) ?>" alt="front">
|
||||||
|
</div>
|
||||||
|
<div class="view back-view">
|
||||||
|
<img src="<?= htmlspecialchars($card['back']) ?>" alt="back">
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fullscreen -->
|
||||||
|
<button id="fullscreen-btn"><img id="fullscreen-icon" src="" alt="Pantalla Completa"></button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Inyectar config al front
|
||||||
|
window.MEMORIA_CONFIG = <?= json_encode($CONFIG_JS, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
// Aplicar banners/bg/icono
|
||||||
|
(function(){
|
||||||
|
const C = window.MEMORIA_CONFIG || {};
|
||||||
|
if (C.background_image) {
|
||||||
|
document.body.style.backgroundImage = `url("${C.background_image}")`;
|
||||||
|
document.body.style.backgroundSize = 'cover';
|
||||||
|
document.body.style.backgroundRepeat = 'no-repeat';
|
||||||
|
document.body.style.backgroundPosition = 'center center';
|
||||||
|
}
|
||||||
|
const b1 = document.querySelector('.banner-1'), b2 = document.querySelector('.banner-2'),
|
||||||
|
b3 = document.querySelector('.banner-3'), b4 = document.querySelector('.banner-4');
|
||||||
|
if (b1 && C.banners.banner_1) b1.style.backgroundImage = `url("${C.banners.banner_1}")`;
|
||||||
|
if (b2 && C.banners.banner_2) b2.style.backgroundImage = `url("${C.banners.banner_2}")`;
|
||||||
|
if (b3 && C.banners.banner_3) b3.style.backgroundImage = `url("${C.banners.banner_3}")`;
|
||||||
|
if (b4 && C.banners.banner_4) b4.style.backgroundImage = `url("${C.banners.banner_4}")`;
|
||||||
|
if (C.fullscreen_icon) document.getElementById('fullscreen-icon').src = C.fullscreen_icon;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
// memoria/registrar_jugada.php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/../admin/db.php';
|
||||||
|
|
||||||
|
// Leer payload
|
||||||
|
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
$juego_id = (int)($payload['juego_id'] ?? 1);
|
||||||
|
$elapsed = (int)($payload['elapsed_ms'] ?? 0);
|
||||||
|
$aciertos = (int)($payload['aciertos'] ?? 0);
|
||||||
|
|
||||||
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
|
||||||
|
// 1) Traer la ruleta asociada a este juego
|
||||||
|
$stmt = $conn->prepare("SELECT ruleta_id FROM juegos_config WHERE juego_id=? LIMIT 1");
|
||||||
|
$stmt->bind_param("i", $juego_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$ruleta_id = (int)($stmt->get_result()->fetch_assoc()['ruleta_id'] ?? 0);
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// 2) Registrar jugada propia del juego de memoria
|
||||||
|
$stmt = $conn->prepare("INSERT INTO jugadas_memoria (juego_id, ip_address, user_agent, tiempo_ms, aciertos) VALUES (?,?,?,?,?)");
|
||||||
|
$stmt->bind_param("issii", $juego_id, $ip_address, $user_agent, $elapsed, $aciertos);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// 3) Seleccionar premio usando la misma lógica que la ruleta
|
||||||
|
$premio_nombre = null;
|
||||||
|
if ($ruleta_id > 0) {
|
||||||
|
// Leer premios con inventario y probabilidad
|
||||||
|
$stmt = $conn->prepare("SELECT id, nombre, cantidad, probabilidad FROM premios WHERE ruleta_id=? AND cantidad > 0");
|
||||||
|
$stmt->bind_param("i", $ruleta_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$premios = [];
|
||||||
|
$totalProb = 0.0;
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$row['probabilidad'] = (float)$row['probabilidad'];
|
||||||
|
$premios[] = $row;
|
||||||
|
$totalProb += $row['probabilidad'];
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if ($premios && $totalProb > 0) {
|
||||||
|
// sorteo ponderado
|
||||||
|
$rnd = mt_rand() / mt_getrandmax() * $totalProb;
|
||||||
|
$acc = 0.0; $ganador = null;
|
||||||
|
foreach ($premios as $p) {
|
||||||
|
$acc += $p['probabilidad'];
|
||||||
|
if ($rnd <= $acc) { $ganador = $p; break; }
|
||||||
|
}
|
||||||
|
if (!$ganador) $ganador = end($premios);
|
||||||
|
|
||||||
|
// Guardar jugada general de la ruleta (para tus reportes unificados)
|
||||||
|
$stmt = $conn->prepare("INSERT INTO jugadas (ruleta_id, premio_id, ip_address, user_agent) VALUES (?,?,?,?)");
|
||||||
|
$stmt->bind_param("iiss", $ruleta_id, $ganador['id'], $ip_address, $user_agent);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Descontar inventario
|
||||||
|
$stmt = $conn->prepare("UPDATE premios SET cantidad = cantidad - 1 WHERE id=? AND cantidad > 0");
|
||||||
|
$stmt->bind_param("i", $ganador['id']);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
$premio_nombre = $ganador['nombre'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'premio' => $premio_nombre
|
||||||
|
]);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// memoria/script.js
|
||||||
|
(function () {
|
||||||
|
const cards = document.querySelectorAll(".card");
|
||||||
|
const totalPairs = cards.length / 2;
|
||||||
|
|
||||||
|
let matched = 0;
|
||||||
|
let cardOne = null, cardTwo = null;
|
||||||
|
let disableDeck = false;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
function flipCard({ target: clickedCard }) {
|
||||||
|
if (disableDeck || clickedCard === cardOne || clickedCard.classList.contains("flip")) return;
|
||||||
|
clickedCard.classList.add("flip");
|
||||||
|
if (!cardOne) { cardOne = clickedCard; return; }
|
||||||
|
cardTwo = clickedCard;
|
||||||
|
disableDeck = true;
|
||||||
|
|
||||||
|
const k1 = cardOne.getAttribute("data-key");
|
||||||
|
const k2 = cardTwo.getAttribute("data-key");
|
||||||
|
matchCards(k1, k2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchCards(k1, k2) {
|
||||||
|
if (k1 === k2) {
|
||||||
|
matched++;
|
||||||
|
cardOne.removeEventListener("click", flipCard);
|
||||||
|
cardTwo.removeEventListener("click", flipCard);
|
||||||
|
cardOne = cardTwo = null;
|
||||||
|
disableDeck = false;
|
||||||
|
|
||||||
|
if (matched === totalPairs) {
|
||||||
|
// Juego terminado
|
||||||
|
setTimeout(onGameFinished, 600);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
cardOne.classList.add("shake");
|
||||||
|
cardTwo.classList.add("shake");
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
cardOne.classList.remove("shake", "flip");
|
||||||
|
cardTwo.classList.remove("shake", "flip");
|
||||||
|
cardOne = cardTwo = null;
|
||||||
|
disableDeck = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameFinished() {
|
||||||
|
const elapsedMs = Date.now() - startTime;
|
||||||
|
const C = window.MEMORIA_CONFIG || {};
|
||||||
|
const juego_id = C.juego_id || 1;
|
||||||
|
|
||||||
|
// Registrar jugada y obtener premio (usando la ruleta vinculada en la config del juego)
|
||||||
|
fetch("registrar_jugada.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
juego_id,
|
||||||
|
elapsed_ms: elapsedMs,
|
||||||
|
aciertos: matched,
|
||||||
|
// ruleta_id es opcional aquí; el servidor puede leerlo de la config
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const nombrePremio = data && data.premio ? data.premio : "¡Completado!";
|
||||||
|
mostrarConfetiConTexto(nombrePremio);
|
||||||
|
// Si quieres, podrías reiniciar el mazo o redirigir:
|
||||||
|
// setTimeout(() => location.reload(), 2500);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
mostrarConfetiConTexto("¡Completado!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventos
|
||||||
|
cards.forEach(card => card.addEventListener("click", flipCard));
|
||||||
|
|
||||||
|
function toggleFullScreen() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(err => console.log(err));
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch(err => console.log(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("fullscreen-btn").addEventListener("click", toggleFullScreen);
|
||||||
|
|
||||||
|
// Confeti
|
||||||
|
function mostrarConfetiConTexto(texto = "") {
|
||||||
|
const duration = 4000, end = Date.now() + duration;
|
||||||
|
(function frame() {
|
||||||
|
confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 }, scalar: 1.1 });
|
||||||
|
confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 }, scalar: 1.1 });
|
||||||
|
if (Date.now() < end) requestAnimationFrame(frame);
|
||||||
|
})();
|
||||||
|
if (texto) console.log("🎉 Premio:", texto);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/* Import Google Font - Poppins */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
|
||||||
|
*{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #E9AD14 /*url(https://portal.gomezleemarketing.com/wp-content/uploads/2024/02/BANNER-SUPERIOR-GRANDE.png) repeat center center*/;
|
||||||
|
}
|
||||||
|
.wrapper{
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #727272;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.cards, .card, .view{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.cards{
|
||||||
|
height: 800px;
|
||||||
|
width: 800px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.cards .card{
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
perspective: 1000px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
height: calc(100% / 4 - 10px);
|
||||||
|
width: calc(100% / 3 - 10px);
|
||||||
|
}
|
||||||
|
.card.shake{
|
||||||
|
animation: shake 0.35s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100%{
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
20%{
|
||||||
|
transform: translateX(-13px);
|
||||||
|
}
|
||||||
|
40%{
|
||||||
|
transform: translateX(13px);
|
||||||
|
}
|
||||||
|
60%{
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
80%{
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card .view{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
pointer-events: none;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.25s linear;
|
||||||
|
}
|
||||||
|
.card .front-view img{
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.card .back-view img{
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
.card .back-view{
|
||||||
|
transform: rotateY(-180deg);
|
||||||
|
}
|
||||||
|
.card.flip .back-view{
|
||||||
|
transform: rotateY(0);
|
||||||
|
}
|
||||||
|
.card.flip .front-view{
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.cards{
|
||||||
|
height: 400px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.card .front-view img{
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
.card .back-view img{
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 530px) {
|
||||||
|
.cards{
|
||||||
|
height: 400px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.card .front-view img{
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
.card .back-view img{
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen-btn img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Rasca y Gana (premios separados)
|
||||||
|
// Requiere: window.RASCA_BOOT, jQuery 2.x y rasca_wScratchpad.min.js
|
||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
const BOOT = window.RASCA_BOOT || { rascaId: "1", api:{config:"/api/rasca_config.php?rasca_id=1", premio:"/api/rasca_premio.php"} };
|
||||||
|
let CFG = null;
|
||||||
|
let confirmado = false, enviado = false, scratchInit = false;
|
||||||
|
|
||||||
|
function setBanner(id, url){
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if(!el) return;
|
||||||
|
if(url){ el.style.backgroundImage = "url('"+url+"')"; el.style.display='block'; }
|
||||||
|
else el.style.display='none';
|
||||||
|
}
|
||||||
|
function aplicarVisual(cfg){
|
||||||
|
if(cfg.background_image){ document.body.style.backgroundImage = "url('"+cfg.background_image+"')"; }
|
||||||
|
setBanner("banner-1", cfg.banner_1 || "img/BANNER-1.png");
|
||||||
|
setBanner("banner-2", cfg.banner_2 || "img/BANNER-2.png");
|
||||||
|
setBanner("banner-3", cfg.banner_3 || "img/BANNER-3.png");
|
||||||
|
setBanner("banner-4", cfg.banner_4 || "img/BANNER-4.png");
|
||||||
|
if(cfg.fullscreen_icon && document.getElementById('fullscreen-icon')){
|
||||||
|
document.getElementById('fullscreen-icon').src = cfg.fullscreen_icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleFullScreen(){
|
||||||
|
if(!document.fullscreenElement){ document.documentElement.requestFullscreen().catch(()=>{}); }
|
||||||
|
else { document.exitFullscreen().catch(()=>{}); }
|
||||||
|
}
|
||||||
|
function initScratch(){
|
||||||
|
if(scratchInit) return; scratchInit = true;
|
||||||
|
const bg = (CFG && CFG.bg_image) || "img/ruleta.png"; // fallback
|
||||||
|
const fg = (CFG && CFG.fg_image) || "img/puntero.png"; // fallback (capa raspable)
|
||||||
|
|
||||||
|
$("#card").wScratchPad({
|
||||||
|
size: 80, bg: bg, fg: fg, cursor: "pointer",
|
||||||
|
scratchMove: function(e, percent){
|
||||||
|
if(!confirmado){
|
||||||
|
if(!window.confirm("¿Deseas participar en la promoción?")) return;
|
||||||
|
confirmado = true;
|
||||||
|
}
|
||||||
|
if(percent > 0 && !enviado){
|
||||||
|
enviado = true;
|
||||||
|
$("#card").wScratchPad("clear");
|
||||||
|
$.ajax({
|
||||||
|
url: BOOT.api.premio, method: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8", dataType: "json",
|
||||||
|
data: JSON.stringify({rasca_id: parseInt(BOOT.rascaId,10), percent: Math.round(percent)})
|
||||||
|
})
|
||||||
|
.done(function(res){
|
||||||
|
if(res && res.ok){
|
||||||
|
if(res.sin_premio){ $("#premio").text("Gracias por participar."); return; }
|
||||||
|
//$("#premio").text(res.premio ? ("¡Felicidades! " + res.premio) : "¡Felicidades!");
|
||||||
|
if(res.imagen_url){ $("#card").wScratchPad("bg", res.imagen_url); }
|
||||||
|
else if (CFG && CFG.bg_image){ $("#card").wScratchPad("bg", CFG.bg_image); }
|
||||||
|
}else{
|
||||||
|
$("#premio").text(res && res.error ? res.error : "No se pudo completar la jugada."); enviado = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function(){ $("#premio").text("Error de red. Intenta nuevamente."); enviado = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function cargarConfig(){
|
||||||
|
$.ajax({ url: BOOT.api.config, method: "GET", dataType: "json" })
|
||||||
|
.done(function(cfg){ CFG = cfg || {}; aplicarVisual(CFG); initScratch(); })
|
||||||
|
.fail(function(){ CFG = {}; initScratch(); });
|
||||||
|
}
|
||||||
|
$(function(){ $("#fullscreen-btn").on("click", toggleFullScreen); cargarConfig(); });
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Rasca y Gana</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="rasca_style.css">
|
||||||
|
|
||||||
|
<!-- jQuery 2.x (necesario para wScratchpad) -->
|
||||||
|
<script src="https://code.jquery.com/jquery-2.2.4.min.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<!-- Cambia el nombre del archivo que ya tienes a rasca_wScratchpad.min.js y colócalo junto a este HTML -->
|
||||||
|
<script src="rasca_wScratchpad.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Boot: define rutas API y rasca_id (por URL ?rasca_id=1) -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const q = new URLSearchParams(location.search);
|
||||||
|
const rascaId = q.get('rasca_id') || '1';
|
||||||
|
const API_BASE = '/ruleta/api'; // 🔧 AJUSTA si tu raíz no es /ruleta
|
||||||
|
|
||||||
|
window.RASCA_BOOT = {
|
||||||
|
rascaId,
|
||||||
|
api: {
|
||||||
|
config: API_BASE + '/rasca_config.php?rasca_id=' + encodeURIComponent(rascaId),
|
||||||
|
premio: API_BASE + '/rasca_premio.php'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="rasca_app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Banners (opcionales; se cargan desde la config) -->
|
||||||
|
<div id="banner-1" class="banner banner-1"></div>
|
||||||
|
<div id="banner-2" class="banner banner-2"></div>
|
||||||
|
<div id="banner-3" class="banner banner-3"></div>
|
||||||
|
<div id="banner-4" class="banner banner-4"></div>
|
||||||
|
|
||||||
|
<!-- Juego -->
|
||||||
|
<div id="container">
|
||||||
|
<div id="card"></div>
|
||||||
|
<div id="premio" class="premio"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pantalla completa -->
|
||||||
|
<button id="fullscreen-btn" aria-label="Pantalla completa">
|
||||||
|
<img id="fullscreen-icon" src="img/fullscreen-icon.png" alt="Pantalla Completa">
|
||||||
|
</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{
|
||||||
|
width:100%;height:100vh;background:#111;color:#fff;
|
||||||
|
display:flex;align-items:center;justify-content:center;overflow:hidden;
|
||||||
|
background-position:center;background-size:cover;background-repeat:no-repeat;
|
||||||
|
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,"Noto Sans",sans-serif
|
||||||
|
}
|
||||||
|
.banner{position:fixed;background-size:contain;background-repeat:no-repeat;z-index:10}
|
||||||
|
.banner-1{top:1rem;left:1rem;width:140px;height:140px}
|
||||||
|
.banner-2{top:1rem;right:1rem;width:260px;height:260px}
|
||||||
|
.banner-3{bottom:1rem;left:1rem;width:180px;height:90px}
|
||||||
|
.banner-4{bottom:1rem;right:1rem;width:320px;height:90px}
|
||||||
|
#container{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
||||||
|
#card{width:420px;height:520px;position:relative;user-select:none;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.35)}
|
||||||
|
#card:active{transform:scale(1.02)}
|
||||||
|
#premio{position:absolute;top:calc(50% + 290px);left:50%;transform:translate(-50%,-50%);
|
||||||
|
background:rgba(0,0,0,.55);padding:.6rem 1rem;border-radius:10px;font-weight:600;min-width:260px;text-align:center}
|
||||||
|
canvas,img{border-radius:12px}
|
||||||
|
#fullscreen-btn{position:fixed;top:10px;right:10px;background:none;border:none;cursor:pointer;z-index:20}
|
||||||
|
#fullscreen-btn img{width:42px;height:42px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.5))}
|
||||||
|
@media (max-width:768px){
|
||||||
|
#card{width:320px;height:420px}
|
||||||
|
.banner-2{width:180px;height:180px}
|
||||||
|
.banner-4{width:240px}
|
||||||
|
}
|
||||||
Vendored
+255
@@ -0,0 +1,255 @@
|
|||||||
|
/*! wScratchPad - v2.1.0 - 2014-04-14 */ !(function (a) {
|
||||||
|
"use strict";
|
||||||
|
function b(b, c) {
|
||||||
|
(this.$el = a(b)),
|
||||||
|
(this.options = c),
|
||||||
|
(this.init = !1),
|
||||||
|
(this.enabled = !0),
|
||||||
|
this._generate();
|
||||||
|
}
|
||||||
|
(b.prototype = {
|
||||||
|
_generate: function () {
|
||||||
|
return a.support.canvas
|
||||||
|
? ((this.canvas = document.createElement("canvas")),
|
||||||
|
(this.ctx = this.canvas.getContext("2d")),
|
||||||
|
"static" === this.$el.css("position") &&
|
||||||
|
this.$el.css("position", "relative"),
|
||||||
|
(this.$img = a('<img src=""/>')
|
||||||
|
.attr("crossOrigin", "")
|
||||||
|
.css({ position: "absolute", width: "100%", height: "100%" })),
|
||||||
|
(this.$scratchpad = a(this.canvas).css({
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
})),
|
||||||
|
this.$scratchpad.bindMobileEvents(),
|
||||||
|
this.$scratchpad
|
||||||
|
.mousedown(
|
||||||
|
a.proxy(function (b) {
|
||||||
|
return this.enabled
|
||||||
|
? ((this.canvasOffset = a(this.canvas).offset()),
|
||||||
|
(this.scratch = !0),
|
||||||
|
void this._scratchFunc(b, "Down"))
|
||||||
|
: !0;
|
||||||
|
}, this)
|
||||||
|
)
|
||||||
|
.mousemove(
|
||||||
|
a.proxy(function (a) {
|
||||||
|
this.scratch && this._scratchFunc(a, "Move");
|
||||||
|
}, this)
|
||||||
|
)
|
||||||
|
.mouseup(
|
||||||
|
a.proxy(function (a) {
|
||||||
|
this.scratch &&
|
||||||
|
((this.scratch = !1), this._scratchFunc(a, "Up"));
|
||||||
|
}, this)
|
||||||
|
),
|
||||||
|
this._setOptions(),
|
||||||
|
this.$el.append(this.$img).append(this.$scratchpad),
|
||||||
|
(this.init = !0),
|
||||||
|
void this.reset())
|
||||||
|
: (this.$el.append("Canvas is not supported in this browser."), !0);
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
var b = this,
|
||||||
|
c = Math.ceil(this.$el.innerWidth()),
|
||||||
|
d = Math.ceil(this.$el.innerHeight()),
|
||||||
|
e = window.devicePixelRatio || 1;
|
||||||
|
(this.pixels = c * d),
|
||||||
|
this.$scratchpad.attr("width", c).attr("height", d),
|
||||||
|
this.canvas.setAttribute("width", c * e),
|
||||||
|
this.canvas.setAttribute("height", d * e),
|
||||||
|
this.ctx.scale(e, e),
|
||||||
|
(this.pixels = c * e * d * e),
|
||||||
|
this.$img.hide(),
|
||||||
|
this.options.bg &&
|
||||||
|
("#" === this.options.bg.charAt(0)
|
||||||
|
? this.$el.css("backgroundColor", this.options.bg)
|
||||||
|
: (this.$el.css("backgroundColor", ""),
|
||||||
|
this.$img.attr("src", this.options.bg))),
|
||||||
|
this.options.fg &&
|
||||||
|
("#" === this.options.fg.charAt(0)
|
||||||
|
? ((this.ctx.fillStyle = this.options.fg),
|
||||||
|
this.ctx.beginPath(),
|
||||||
|
this.ctx.rect(0, 0, c, d),
|
||||||
|
this.ctx.fill(),
|
||||||
|
this.$img.show())
|
||||||
|
: a(new Image())
|
||||||
|
.attr("src", this.options.fg)
|
||||||
|
.load(function () {
|
||||||
|
b.ctx.drawImage(this, 0, 0, c, d), b.$img.show();
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
clear: function () {
|
||||||
|
this.ctx.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.ceil(this.$el.innerWidth()),
|
||||||
|
Math.ceil(this.$el.innerHeight())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enable: function (a) {
|
||||||
|
this.enabled = a === !0 ? !0 : !1;
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
this.$el.children().remove(), a.removeData(this.$el, "wScratchPad");
|
||||||
|
},
|
||||||
|
_setOptions: function () {
|
||||||
|
var a, b;
|
||||||
|
for (a in this.options)
|
||||||
|
(this.options[a] = this.$el.attr("data-" + a) || this.options[a]),
|
||||||
|
(b = "set" + a.charAt(0).toUpperCase() + a.substring(1)),
|
||||||
|
this[b] && this[b](this.options[a]);
|
||||||
|
},
|
||||||
|
setBg: function () {
|
||||||
|
this.init && this.reset();
|
||||||
|
},
|
||||||
|
setFg: function () {
|
||||||
|
this.setBg();
|
||||||
|
},
|
||||||
|
setCursor: function (a) {
|
||||||
|
this.$el.css("cursor", a);
|
||||||
|
},
|
||||||
|
_scratchFunc: function (a, b) {
|
||||||
|
(a.pageX = Math.floor(a.pageX - this.canvasOffset.left)),
|
||||||
|
(a.pageY = Math.floor(a.pageY - this.canvasOffset.top)),
|
||||||
|
this["_scratch" + b](a),
|
||||||
|
(this.options.realtime || "Up" === b) &&
|
||||||
|
this.options["scratch" + b] &&
|
||||||
|
this.options["scratch" + b].apply(this, [a, this._scratchPercent()]);
|
||||||
|
},
|
||||||
|
_scratchPercent: function () {
|
||||||
|
for (
|
||||||
|
var a = 0,
|
||||||
|
b = this.ctx.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.canvas.width,
|
||||||
|
this.canvas.height
|
||||||
|
),
|
||||||
|
c = 0,
|
||||||
|
d = b.data.length;
|
||||||
|
d > c;
|
||||||
|
c += 4
|
||||||
|
)
|
||||||
|
0 === b.data[c] &&
|
||||||
|
0 === b.data[c + 1] &&
|
||||||
|
0 === b.data[c + 2] &&
|
||||||
|
0 === b.data[c + 3] &&
|
||||||
|
a++;
|
||||||
|
return (a / this.pixels) * 100;
|
||||||
|
},
|
||||||
|
_scratchDown: function (a) {
|
||||||
|
(this.ctx.globalCompositeOperation = "destination-out"),
|
||||||
|
(this.ctx.lineJoin = "round"),
|
||||||
|
(this.ctx.lineCap = "round"),
|
||||||
|
(this.ctx.strokeStyle = this.options.color),
|
||||||
|
(this.ctx.lineWidth = this.options.size),
|
||||||
|
this.ctx.beginPath(),
|
||||||
|
this.ctx.arc(
|
||||||
|
a.pageX,
|
||||||
|
a.pageY,
|
||||||
|
this.options.size / 2,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
!0
|
||||||
|
),
|
||||||
|
this.ctx.closePath(),
|
||||||
|
this.ctx.fill(),
|
||||||
|
this.ctx.beginPath(),
|
||||||
|
this.ctx.moveTo(a.pageX, a.pageY);
|
||||||
|
},
|
||||||
|
_scratchMove: function (a) {
|
||||||
|
this.ctx.lineTo(a.pageX, a.pageY), this.ctx.stroke();
|
||||||
|
},
|
||||||
|
_scratchUp: function () {
|
||||||
|
this.ctx.closePath();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
(a.support.canvas = document.createElement("canvas").getContext),
|
||||||
|
(a.fn.wScratchPad = function (c, d) {
|
||||||
|
function e() {
|
||||||
|
var d = a.data(this, "wScratchPad");
|
||||||
|
return (
|
||||||
|
d ||
|
||||||
|
((d = new b(this, a.extend(!0, {}, c))),
|
||||||
|
a.data(this, "wScratchPad", d)),
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("string" == typeof c) {
|
||||||
|
var f,
|
||||||
|
g = [],
|
||||||
|
h =
|
||||||
|
(void 0 !== d ? "set" : "get") +
|
||||||
|
c.charAt(0).toUpperCase() +
|
||||||
|
c.substring(1),
|
||||||
|
i = function () {
|
||||||
|
f.options[c] && (f.options[c] = d), f[h] && f[h].apply(f, [d]);
|
||||||
|
},
|
||||||
|
j = function () {
|
||||||
|
return f[h]
|
||||||
|
? f[h].apply(f, [d])
|
||||||
|
: f.options[c]
|
||||||
|
? f.options[c]
|
||||||
|
: void 0;
|
||||||
|
},
|
||||||
|
k = function () {
|
||||||
|
(f = a.data(this, "wScratchPad")),
|
||||||
|
f &&
|
||||||
|
(f[c] ? f[c].apply(f, [d]) : void 0 !== d ? i() : g.push(j()));
|
||||||
|
};
|
||||||
|
return this.each(k), g.length ? (1 === g.length ? g[0] : g) : this;
|
||||||
|
}
|
||||||
|
return (c = a.extend({}, a.fn.wScratchPad.defaults, c)), this.each(e);
|
||||||
|
}),
|
||||||
|
(a.fn.wScratchPad.defaults = {
|
||||||
|
size: 5,
|
||||||
|
bg: "#cacaca",
|
||||||
|
fg: "#6699ff",
|
||||||
|
realtime: !0,
|
||||||
|
scratchDown: null,
|
||||||
|
scratchUp: null,
|
||||||
|
scratchMove: null,
|
||||||
|
cursor: "crosshair",
|
||||||
|
}),
|
||||||
|
(a.fn.bindMobileEvents = function () {
|
||||||
|
a(this).on("touchstart touchmove touchend touchcancel", function (a) {
|
||||||
|
var b = a.changedTouches || a.originalEvent.targetTouches,
|
||||||
|
c = b[0],
|
||||||
|
d = "";
|
||||||
|
switch (a.type) {
|
||||||
|
case "touchstart":
|
||||||
|
d = "mousedown";
|
||||||
|
break;
|
||||||
|
case "touchmove":
|
||||||
|
(d = "mousemove"), a.preventDefault();
|
||||||
|
break;
|
||||||
|
case "touchend":
|
||||||
|
d = "mouseup";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var e = document.createEvent("MouseEvent");
|
||||||
|
e.initMouseEvent(
|
||||||
|
d,
|
||||||
|
!0,
|
||||||
|
!0,
|
||||||
|
window,
|
||||||
|
1,
|
||||||
|
c.screenX,
|
||||||
|
c.screenY,
|
||||||
|
c.clientX,
|
||||||
|
c.clientY,
|
||||||
|
!1,
|
||||||
|
!1,
|
||||||
|
!1,
|
||||||
|
!1,
|
||||||
|
0,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
c.target.dispatchEvent(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
+477
@@ -0,0 +1,477 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.1
|
||||||
|
-- https://www.phpmyadmin.net/
|
||||||
|
--
|
||||||
|
-- Servidor: 127.0.0.1
|
||||||
|
-- Tiempo de generación: 09-08-2025 a las 21:23:36
|
||||||
|
-- Versión del servidor: 10.4.32-MariaDB
|
||||||
|
-- Versión de PHP: 8.0.30
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Base de datos: `ruleta_db`
|
||||||
|
--
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `admin_users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `admin_users` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`email` varchar(150) NOT NULL,
|
||||||
|
`password_hash` varchar(255) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `admin_users`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `admin_users` (`id`, `nombre`, `email`, `password_hash`, `created_at`) VALUES
|
||||||
|
(1, 'Administrador', 'admin@localhost', '$2y$10$14EWwemmWzjU4Vr9fZXvduxHgsfnHrpk5ziLFN3IGJ5Pw5QzSorm6', '2025-08-08 21:23:15');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `cartas`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `cartas` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`juego_id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`imagen_frontal` varchar(255) DEFAULT NULL,
|
||||||
|
`imagen_trasera` varchar(255) NOT NULL,
|
||||||
|
`activo` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `cartas`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `cartas` (`id`, `juego_id`, `nombre`, `imagen_frontal`, `imagen_trasera`, `activo`, `created_at`) VALUES
|
||||||
|
(7, 1, 'carta1', '/ruleta/uploads/memoria/back-6896886e0c5fa.png', '/ruleta/uploads/memoria/img-1-6896886e0c89e.png', 1, '2025-08-08 23:29:50'),
|
||||||
|
(8, 1, 'carta2', '/ruleta/uploads/memoria/back-689688b64d392.png', '/ruleta/uploads/memoria/img-2-689688b64d806.png', 1, '2025-08-08 23:31:02'),
|
||||||
|
(9, 2, 'primera', NULL, '/ruleta/uploads/memoria/img-1-689689e0611fb.png', 1, '2025-08-08 23:36:00'),
|
||||||
|
(10, 2, 'imagen2', NULL, '/ruleta/uploads/memoria/img-2-68968c7021030.png', 1, '2025-08-08 23:46:56'),
|
||||||
|
(11, 2, 'image3', NULL, '/ruleta/uploads/memoria/img-3-68968ca3dab11.png', 1, '2025-08-08 23:47:47'),
|
||||||
|
(12, 2, 'imagen4', NULL, '/ruleta/uploads/memoria/img-4-68968cb31a8ad.png', 1, '2025-08-08 23:48:03'),
|
||||||
|
(13, 2, 'imagen5', NULL, '/ruleta/uploads/memoria/img-5-68968cc1091b2.png', 1, '2025-08-08 23:48:17'),
|
||||||
|
(14, 2, 'imagen6', NULL, '/ruleta/uploads/memoria/img-6-68968ccee8e67.png', 1, '2025-08-08 23:48:30');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `juegos_config`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `juegos_config` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`juego_id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`banner_1` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_2` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_3` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_4` varchar(255) DEFAULT NULL,
|
||||||
|
`background_image` varchar(255) DEFAULT NULL,
|
||||||
|
`fullscreen_icon` varchar(255) DEFAULT NULL,
|
||||||
|
`reverso_default` varchar(255) DEFAULT NULL,
|
||||||
|
`ruleta_id` int(11) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `juegos_config`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `juegos_config` (`id`, `juego_id`, `nombre`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`, `reverso_default`, `ruleta_id`, `created_at`) VALUES
|
||||||
|
(2, 2, 'Test', NULL, NULL, NULL, NULL, '/ruleta/uploads/memoria/BANNER-SUPERIOR-GRANDE-68978a66db94e.png', '/ruleta/uploads/memoria/fullscreen-icon-68978a66dc1b1.png', '/ruleta/uploads/memoria/front-68978a66dcac1.jpg', 0, '2025-08-08 23:35:30');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `jugadas`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `jugadas` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`ruleta_id` int(11) NOT NULL,
|
||||||
|
`premio_id` int(11) DEFAULT NULL,
|
||||||
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
|
`user_agent` text DEFAULT NULL,
|
||||||
|
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `jugadas`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `jugadas` (`id`, `ruleta_id`, `premio_id`, `ip_address`, `user_agent`, `fecha_juego`) VALUES
|
||||||
|
(1, 2, 2, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 21:39:31'),
|
||||||
|
(6, 2, 5, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 21:57:34'),
|
||||||
|
(7, 2, 5, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:01:31'),
|
||||||
|
(8, 3, 6, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:08:40'),
|
||||||
|
(9, 3, 6, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:09:44'),
|
||||||
|
(10, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:27:31'),
|
||||||
|
(11, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:29:41'),
|
||||||
|
(12, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:30:04'),
|
||||||
|
(13, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:41:26'),
|
||||||
|
(14, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:38:35'),
|
||||||
|
(15, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:38:53'),
|
||||||
|
(16, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:39:32'),
|
||||||
|
(17, 4, 8, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:42:29'),
|
||||||
|
(18, 4, 8, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:42:39'),
|
||||||
|
(19, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:43:58'),
|
||||||
|
(20, 4, 7, '148.101.50.128', 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', '2025-08-09 17:59:24');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `jugadas_memoria`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `jugadas_memoria` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`juego_id` int(11) NOT NULL,
|
||||||
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
|
`user_agent` text DEFAULT NULL,
|
||||||
|
`tiempo_ms` int(11) DEFAULT 0,
|
||||||
|
`aciertos` int(11) DEFAULT 0,
|
||||||
|
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `jugadas_memoria`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `jugadas_memoria` (`id`, `juego_id`, `ip_address`, `user_agent`, `tiempo_ms`, `aciertos`, `fecha_juego`) VALUES
|
||||||
|
(1, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 12864, 1, '2025-08-08 23:36:28'),
|
||||||
|
(2, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 8170, 1, '2025-08-08 23:45:59'),
|
||||||
|
(3, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 14532, 2, '2025-08-08 23:47:15'),
|
||||||
|
(4, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 38886, 6, '2025-08-08 23:49:16'),
|
||||||
|
(5, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 1644391, 6, '2025-08-09 14:24:29'),
|
||||||
|
(6, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 35695, 6, '2025-08-09 14:25:13'),
|
||||||
|
(7, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 36400, 6, '2025-08-09 14:26:00'),
|
||||||
|
(8, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 231321, 6, '2025-08-09 17:54:32');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `jugadas_rasca`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `jugadas_rasca` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`rasca_id` int(11) NOT NULL,
|
||||||
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
|
`user_agent` text DEFAULT NULL,
|
||||||
|
`porcentaje` int(11) DEFAULT 0,
|
||||||
|
`premio_id` int(11) DEFAULT NULL,
|
||||||
|
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `jugadas_rasca`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `jugadas_rasca` (`id`, `rasca_id`, `ip_address`, `user_agent`, `porcentaje`, `premio_id`, `fecha_juego`) VALUES
|
||||||
|
(1, 10, '::1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', 50, 2, '2025-08-09 18:26:37'),
|
||||||
|
(2, 10, '::1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', 50, 2, '2025-08-09 18:26:50'),
|
||||||
|
(3, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:27:29'),
|
||||||
|
(4, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:27:44'),
|
||||||
|
(5, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:29:45'),
|
||||||
|
(6, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:30:24'),
|
||||||
|
(7, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:31:29'),
|
||||||
|
(8, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:32:14'),
|
||||||
|
(9, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:32:36'),
|
||||||
|
(10, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:35:47'),
|
||||||
|
(11, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:37:16'),
|
||||||
|
(12, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:37:45'),
|
||||||
|
(13, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 18:39:13'),
|
||||||
|
(14, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 18:39:25'),
|
||||||
|
(15, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 9, 2, '2025-08-09 18:39:50'),
|
||||||
|
(16, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 5, 2, '2025-08-09 18:40:43'),
|
||||||
|
(17, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 27, 2, '2025-08-09 18:43:22'),
|
||||||
|
(18, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 3, 2, '2025-08-09 19:22:04'),
|
||||||
|
(19, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 19:22:42');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `premios`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `premios` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`ruleta_id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`cantidad` int(11) DEFAULT 0,
|
||||||
|
`probabilidad` decimal(6,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`inicio_angulo` decimal(7,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`fin_angulo` decimal(7,2) NOT NULL DEFAULT 0.00
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `premios`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `premios` (`id`, `ruleta_id`, `nombre`, `cantidad`, `probabilidad`, `inicio_angulo`, `fin_angulo`) VALUES
|
||||||
|
(2, 2, 'Freidora de aire', 0, 1.00, 100.00, 150.00),
|
||||||
|
(5, 2, 'correa', 98, 1.00, 180.00, 190.00),
|
||||||
|
(6, 3, 'bono de compra', 8, 1.00, 180.00, 182.00),
|
||||||
|
(7, 4, 'plancha', 12, 0.75, 180.00, 190.00),
|
||||||
|
(8, 4, 'Abanico de Piso', 8, 0.25, 15.00, 30.00);
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `rasca_config`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `rasca_config` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`rasca_id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`ruleta_id` int(11) DEFAULT NULL,
|
||||||
|
`banner_1` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_2` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_3` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_4` varchar(255) DEFAULT NULL,
|
||||||
|
`background_image` varchar(255) DEFAULT NULL,
|
||||||
|
`fullscreen_icon` varchar(255) DEFAULT NULL,
|
||||||
|
`fg_image` varchar(255) DEFAULT NULL,
|
||||||
|
`bg_image` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `rasca_config`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `rasca_config` (`id`, `rasca_id`, `nombre`, `ruleta_id`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`, `fg_image`, `bg_image`, `created_at`) VALUES
|
||||||
|
(8, 10, 'Test2', NULL, NULL, NULL, NULL, NULL, '/ruleta/uploads/rasca/BANNER-SUPERIOR-GRANDE-689784d6b20a2.png', '/ruleta/uploads/rasca/fullscreen-icon-689784d6b2a9e.png', '/ruleta/uploads/rasca/front-689784d6b3331.jpg', '/ruleta/uploads/rasca/back-68979308bd719.jpg', '2025-08-09 17:26:46');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `rasca_premios`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `rasca_premios` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`rasca_id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(150) NOT NULL,
|
||||||
|
`probabilidad` decimal(6,3) NOT NULL DEFAULT 0.000,
|
||||||
|
`stock` int(11) DEFAULT NULL,
|
||||||
|
`activo` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
`imagen_url` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `rasca_premios`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `rasca_premios` (`id`, `rasca_id`, `nombre`, `probabilidad`, `stock`, `activo`, `imagen_url`, `created_at`) VALUES
|
||||||
|
(2, 10, 'laptop', 1.000, 97, 1, '/ruleta/uploads/rasca_premios/Gpay_Card_2-689784f9ba2c3.jpg', '2025-08-09 17:27:21');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Estructura de tabla para la tabla `ruletas`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `ruletas` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`nombre` varchar(100) NOT NULL,
|
||||||
|
`descripcion` text DEFAULT NULL,
|
||||||
|
`imagen_ruleta` varchar(255) DEFAULT NULL,
|
||||||
|
`imagen_puntero` varchar(255) DEFAULT NULL,
|
||||||
|
`fecha_creacion` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`banner_1` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_2` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_3` varchar(255) DEFAULT NULL,
|
||||||
|
`banner_4` varchar(255) DEFAULT NULL,
|
||||||
|
`background_image` varchar(255) DEFAULT NULL,
|
||||||
|
`fullscreen_icon` varchar(255) DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Volcado de datos para la tabla `ruletas`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `ruletas` (`id`, `nombre`, `descripcion`, `imagen_ruleta`, `imagen_puntero`, `fecha_creacion`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`) VALUES
|
||||||
|
(2, 'Test', '1', '/ruleta/uploads/ruletas/ruleta1-68966beb88079.png', '/ruleta/uploads/ruletas/puntero11-68966beb88ae7.png', '2025-08-08 21:28:11', '/ruleta/uploads/ruletas/BANNER-1-689671662b081.png', '/ruleta/uploads/ruletas/BANNER-2-689671662c041.png', NULL, NULL, '/ruleta/uploads/ruletas/background1-6896708085ee4.png', '/ruleta/uploads/ruletas/fullscreen-icon-689671662c540.png'),
|
||||||
|
(3, 'ruleta2', 'jlsgksjgkjfg', '/ruleta/uploads/ruletas/ruleta-68967520e286f.png', '/ruleta/uploads/ruletas/puntero-68967520e2a68.png', '2025-08-08 22:07:28', NULL, '/ruleta/uploads/ruletas/puntero2-68967520e2c3d.png', NULL, NULL, '/ruleta/uploads/ruletas/background-68967520e3c87.png', '/ruleta/uploads/ruletas/fullscreen-icon-68967520e3e76.png'),
|
||||||
|
(4, 'promotion 1', 'My first rule', '/ruleta/uploads/ruletas/ruleta-68967960bad31.png', '/ruleta/uploads/ruletas/puntero-68967960bafae.png', '2025-08-08 22:25:36', '/ruleta/uploads/ruletas/BANNER-1-68967960bb15c.png', '/ruleta/uploads/ruletas/BANNER-2-68967960bb380.png', NULL, '/ruleta/uploads/ruletas/BANNER-4-68967960bb67a.png', '/ruleta/uploads/ruletas/Strong-68967960bbc33.png', '/ruleta/uploads/ruletas/fullscreen-icon-68967960bbed3.png');
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Índices para tablas volcadas
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `admin_users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `admin_users`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `email` (`email`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `cartas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `cartas`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `juegos_config`
|
||||||
|
--
|
||||||
|
ALTER TABLE `juegos_config`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `juego_id` (`juego_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `jugadas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `ruleta_id` (`ruleta_id`),
|
||||||
|
ADD KEY `premio_id` (`premio_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `jugadas_memoria`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas_memoria`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `jugadas_rasca`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas_rasca`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `premios`
|
||||||
|
--
|
||||||
|
ALTER TABLE `premios`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `ruleta_id` (`ruleta_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `rasca_config`
|
||||||
|
--
|
||||||
|
ALTER TABLE `rasca_config`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `rasca_id` (`rasca_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `rasca_premios`
|
||||||
|
--
|
||||||
|
ALTER TABLE `rasca_premios`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `rasca_id` (`rasca_id`),
|
||||||
|
ADD KEY `activo` (`activo`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indices de la tabla `ruletas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ruletas`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de las tablas volcadas
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `admin_users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `admin_users`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `cartas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `cartas`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=15;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `juegos_config`
|
||||||
|
--
|
||||||
|
ALTER TABLE `juegos_config`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `jugadas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `jugadas_memoria`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas_memoria`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `jugadas_rasca`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas_rasca`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=20;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `premios`
|
||||||
|
--
|
||||||
|
ALTER TABLE `premios`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `rasca_config`
|
||||||
|
--
|
||||||
|
ALTER TABLE `rasca_config`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `rasca_premios`
|
||||||
|
--
|
||||||
|
ALTER TABLE `rasca_premios`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT de la tabla `ruletas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ruletas`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Restricciones para tablas volcadas
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Filtros para la tabla `jugadas`
|
||||||
|
--
|
||||||
|
ALTER TABLE `jugadas`
|
||||||
|
ADD CONSTRAINT `jugadas_ibfk_1` FOREIGN KEY (`ruleta_id`) REFERENCES `ruletas` (`id`),
|
||||||
|
ADD CONSTRAINT `jugadas_ibfk_2` FOREIGN KEY (`premio_id`) REFERENCES `premios` (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Filtros para la tabla `premios`
|
||||||
|
--
|
||||||
|
ALTER TABLE `premios`
|
||||||
|
ADD CONSTRAINT `premios_ibfk_1` FOREIGN KEY (`ruleta_id`) REFERENCES `ruletas` (`id`) ON DELETE CASCADE;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
(function () {
|
||||||
|
const ruleta = document.getElementById("ruleta");
|
||||||
|
const puntero = document.getElementById("puntero");
|
||||||
|
|
||||||
|
// Modal confirmación
|
||||||
|
const confirmModal = document.getElementById("confirm-modal");
|
||||||
|
const confirmYes = document.getElementById("confirm-yes");
|
||||||
|
const confirmNo = document.getElementById("confirm-no");
|
||||||
|
|
||||||
|
// Modal agradecimiento
|
||||||
|
const thankModal = document.getElementById("thank-you-modal");
|
||||||
|
const thankTitle = document.getElementById("thank-you-message");
|
||||||
|
const thankDetails = document.getElementById("thank-you-details");
|
||||||
|
const thankCloseBtn = document.getElementById("redirect-button");
|
||||||
|
|
||||||
|
// Fullscreen
|
||||||
|
const fullscreenBtn = document.getElementById("fullscreen-btn");
|
||||||
|
|
||||||
|
let gira = 0;
|
||||||
|
let articuloGanador = "";
|
||||||
|
let estaGirando = false;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const ruletaId = parseInt(params.get("ruleta_id") || "1", 10) || 1;
|
||||||
|
|
||||||
|
// Fallback local (solo si backend no manda ángulos)
|
||||||
|
const rangosFallback = [
|
||||||
|
{ articulo: "Freidora de aire", inicio: 330, final: 16 },
|
||||||
|
{ articulo: "Articulo plastico", inicio: 15, final: 16 },
|
||||||
|
{ articulo: "Sandwichera", inicio: 150, final: 196 },
|
||||||
|
{ articulo: "Bandana", inicio: 170, final: 16 },
|
||||||
|
{ articulo: "Cubeta", inicio: 20, final: 52 },
|
||||||
|
{ articulo: "Bandana1", inicio: 42, final: 88 },
|
||||||
|
{ articulo: "Beggin'", inicio: 130, final: 160 },
|
||||||
|
{ articulo: "Sobre Pro Plan", inicio: 220, final: 232 },
|
||||||
|
{ articulo: "Plancha", inicio: 150, final: 232 },
|
||||||
|
{ articulo: "Caldero", inicio: 30, final: 194 },
|
||||||
|
{ articulo: "Abanico de piso", inicio: 296, final: 328 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function normaliza(a){ return ((a % 360) + 360) % 360; }
|
||||||
|
function centro(inicio, fin){
|
||||||
|
const i = normaliza(inicio), f = normaliza(fin);
|
||||||
|
if (i === f) return i;
|
||||||
|
if (i < f) return i + (f - i)/2;
|
||||||
|
const span = 360 - i + f;
|
||||||
|
return normaliza(i + span/2);
|
||||||
|
}
|
||||||
|
function buscarRangoLocal(nombre){
|
||||||
|
return rangosFallback.find(r => r.articulo.toLowerCase() === String(nombre).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function mostrarConfirmacion(){ confirmModal.style.display = "block"; }
|
||||||
|
function cerrarConfirmacion(){ confirmModal.style.display = "none"; }
|
||||||
|
|
||||||
|
function mostrarAgradecimiento(nombre){
|
||||||
|
thankTitle.textContent = "🎊🎉¡Felicidades!🎉🎊";
|
||||||
|
thankDetails.textContent = `🎁 Has ganado "${nombre}" 🎁`;
|
||||||
|
thankModal.style.display = "flex";
|
||||||
|
}
|
||||||
|
function ocultarAgradecimiento(){ thankModal.style.display = "none"; }
|
||||||
|
|
||||||
|
function girarA(angulo){
|
||||||
|
gira = 1440 + normaliza(angulo); // 4 vueltas + objetivo
|
||||||
|
estaGirando = true;
|
||||||
|
puntero.style.pointerEvents = "none";
|
||||||
|
ruleta.style.transition = "all 5s ease-out";
|
||||||
|
ruleta.style.transform = `rotate(${gira}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleta.addEventListener("transitionend", function () {
|
||||||
|
ruleta.style.transition = "none";
|
||||||
|
ruleta.style.transform = `rotate(${normaliza(gira)}deg)`;
|
||||||
|
estaGirando = false;
|
||||||
|
puntero.style.pointerEvents = "auto";
|
||||||
|
lanzarConfeti();
|
||||||
|
mostrarAgradecimiento(articuloGanador);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function obtenerIP() {
|
||||||
|
try {
|
||||||
|
const r = await fetch("https://api.ipify.org?format=json");
|
||||||
|
const d = await r.json();
|
||||||
|
return d.ip || "0.0.0.0";
|
||||||
|
} catch { return "0.0.0.0"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function obtenerArticuloGanadorYSpin() {
|
||||||
|
const userAgent = navigator.userAgent || "unknown";
|
||||||
|
const ip = await obtenerIP();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch("api/seleccionar_premio.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_agent: userAgent,
|
||||||
|
ip_address: ip,
|
||||||
|
ruleta_id: ruletaId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
// data esperado:
|
||||||
|
// { ok:true, nombre:"X", inicio:120, fin:160 } (inicio/fin opcionales)
|
||||||
|
articuloGanador = data.nombre || "Premio";
|
||||||
|
|
||||||
|
let angulo = null;
|
||||||
|
if (typeof data.inicio === "number" && typeof data.fin === "number") {
|
||||||
|
angulo = centro(data.inicio, data.fin);
|
||||||
|
} else {
|
||||||
|
const rango = buscarRangoLocal(articuloGanador);
|
||||||
|
angulo = rango ? centro(rango.inicio, rango.final) : Math.floor(Math.random() * 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
girarA(angulo);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error al obtener el artículo ganador:", err);
|
||||||
|
estaGirando = false;
|
||||||
|
puntero.style.pointerEvents = "auto";
|
||||||
|
alert("Hubo un problema al obtener el premio. Intenta de nuevo.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interacciones
|
||||||
|
puntero.addEventListener("mousedown", function () {
|
||||||
|
if (estaGirando) return;
|
||||||
|
mostrarConfirmacion();
|
||||||
|
});
|
||||||
|
confirmYes.addEventListener("click", function () {
|
||||||
|
cerrarConfirmacion();
|
||||||
|
if (estaGirando) return;
|
||||||
|
obtenerArticuloGanadorYSpin();
|
||||||
|
});
|
||||||
|
confirmNo.addEventListener("click", function () {
|
||||||
|
cerrarConfirmacion();
|
||||||
|
});
|
||||||
|
thankCloseBtn.addEventListener("click", ocultarAgradecimiento);
|
||||||
|
|
||||||
|
// Fullscreen
|
||||||
|
fullscreenBtn.addEventListener("click", () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(console.error);
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confeti
|
||||||
|
function lanzarConfeti() {
|
||||||
|
const duration = 4000;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
(function frame() {
|
||||||
|
confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 }, scalar: 1.2 });
|
||||||
|
confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 }, scalar: 1.2 });
|
||||||
|
if (Date.now() < end) requestAnimationFrame(frame);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/* Estilos generales existentes */
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000000;
|
||||||
|
background-size: cover;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 48px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow:
|
||||||
|
4px 4px 0 #e60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: -400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ruleta {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#puntero {
|
||||||
|
position: absolute;
|
||||||
|
top: 47%;
|
||||||
|
left: 50%;
|
||||||
|
width: 200px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para dispositivos con pantallas más pequeñas */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
#ruleta {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#puntero {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#ruleta, #puntero {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#ruleta, #puntero {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#puntero {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nueva clase para manejar orientación vertical */
|
||||||
|
.landscape-message {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: red;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid red;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.landscape-message {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ruleta, #puntero, .content, .game {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
right: 3%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: url('img/fullscreen-icon.png') no-repeat center center;
|
||||||
|
background-size: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.fullscreen-btn {
|
||||||
|
top: 3%;
|
||||||
|
right: 5%;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fullscreen-btn {
|
||||||
|
top: 3%;
|
||||||
|
right: 5%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.fullscreen-btn {
|
||||||
|
top: 2%;
|
||||||
|
right: 8%;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
z-index: 99999 !important; /* Mayor que cualquier otro elemento */
|
||||||
|
pointer-events: none; /* No bloquear interacciones */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos de los banners */
|
||||||
|
.banner {
|
||||||
|
position: fixed;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-1 {
|
||||||
|
top: 0%;
|
||||||
|
left: 5%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
background-image: url('img/BANNER-1.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-4 {
|
||||||
|
bottom: 0%;
|
||||||
|
right: 5%;
|
||||||
|
width: 400px;
|
||||||
|
height: 100px;
|
||||||
|
background-image: url('img/BANNER-4.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-2 {
|
||||||
|
top: 5%;
|
||||||
|
right: 5%;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
background-image: url('img/BANNER-2.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para el modal */
|
||||||
|
.modal {
|
||||||
|
display: none; /* Oculto por defecto */
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5); /* Fondo semitransparente */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-yes {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-no {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*PopUp Agradecimiento*/
|
||||||
|
#thank-you-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#thank-you-modal .popup-content {
|
||||||
|
background: #C10F1A;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#redirect-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#redirect-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user