diff --git a/conf/bd.sql b/conf/bd.sql new file mode 100644 index 0000000..fe44ff0 --- /dev/null +++ b/conf/bd.sql @@ -0,0 +1,89 @@ +-- Creación de la base de datos +CREATE DATABASE IF NOT EXISTS boletos_db; +USE boletos_db; + +-- Tabla de salas +CREATE TABLE IF NOT EXISTS salas ( + id INT AUTO_INCREMENT PRIMARY KEY, + nombre VARCHAR(100) NOT NULL, + filas INT NOT NULL, + asientos_por_fila INT NOT NULL +); + +-- Tabla de boletos +CREATE TABLE IF NOT EXISTS boletos ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_sala INT NOT NULL, + fila INT NOT NULL, + numero INT NOT NULL, + precio DECIMAL(10,2) NOT NULL, + estado ENUM('disponible', 'vendido') DEFAULT 'disponible', + FOREIGN KEY (id_sala) REFERENCES salas(id), + UNIQUE KEY unique_asiento (id_sala, fila, numero) +); + +-- Tabla de ventas +CREATE TABLE IF NOT EXISTS ventas ( + id VARCHAR(36) PRIMARY KEY, + fecha DATETIME NOT NULL, + nombre_cliente VARCHAR(100) NOT NULL, + total DECIMAL(10,2) NOT NULL +); + +select * from ventas; + +select * from venta_boletos; + +select * from boletos; + +-- Tabla relacional venta-boletos +CREATE TABLE IF NOT EXISTS venta_boletos ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_venta VARCHAR(36) NOT NULL, + id_boleto INT NOT NULL, + FOREIGN KEY (id_venta) REFERENCES ventas(id), + FOREIGN KEY (id_boleto) REFERENCES boletos(id), + UNIQUE KEY unique_venta_boleto (id_venta, id_boleto) +); + +-- Insertar una sala de ejemplo +INSERT INTO salas (id, nombre, filas, asientos_por_fila) +VALUES (1, 'Sala Principal', 10, 15); + +-- Procedimiento para inicializar boletos +DELIMITER // +CREATE PROCEDURE InicializaBoletos(IN sala_id INT, IN precio DECIMAL(10,2)) +BEGIN + DECLARE i INT DEFAULT 1; + DECLARE j INT DEFAULT 1; + DECLARE total_filas INT DEFAULT 0; + DECLARE total_asientos INT DEFAULT 0; + + -- Obtener dimensiones de la sala + SELECT filas, asientos_por_fila INTO total_filas, total_asientos + FROM salas WHERE id = sala_id; + + -- Depuración: Verificar valores obtenidos + SELECT CONCAT('Filas:', total_filas, ' Asientos:', total_asientos) AS Debug_Info; + + -- Si no se encuentran filas/asientos, salir del procedimiento + IF total_filas = 0 OR total_asientos = 0 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Sala no encontrada o sin dimensiones definidas'; + END IF; + + -- Eliminar boletos existentes de la sala + DELETE FROM boletos WHERE id_sala = sala_id; + + -- Crear nuevos boletos + SET i = 1; + WHILE i <= total_filas DO + SET j = 1; + WHILE j <= total_asientos DO + INSERT INTO boletos (id_sala, fila, numero, precio, estado) + VALUES (sala_id, i, j, precio, 'disponible'); + SET j = j + 1; + END WHILE; + SET i = i + 1; + END WHILE; +END // +DELIMITER ; \ No newline at end of file diff --git a/controlador/VendedorController.php b/controlador/VendedorController.php new file mode 100644 index 0000000..4d60fab --- /dev/null +++ b/controlador/VendedorController.php @@ -0,0 +1,48 @@ +<?php + +require_once '../modelo/BaseDatos.php'; +require_once '../modelo/Sala.php'; +require_once '../modelo/Boleto.php'; +require_once '../modelo/Venta.php'; + +class VendedorController { + private $baseDatos; + private $sala; + + public function __construct($baseDatos) { + $this->baseDatos = $baseDatos; + } + + public function cargarSala($idSala) { + $this->sala = $this->baseDatos->cargarSala($idSala); + return $this->sala; + } + + public function mostrarDisponibilidadAsientos() { + if ($this->sala) { + return $this->sala->disponibilidadAsientos(); + } + return null; + } + + public function seleccionarBoletos($idsBoletos) { + if ($this->sala) { + return $this->sala->obtenerBoletosPorId($idsBoletos); + } + return []; + } + + public function procesarVenta($boletos, $nombreCliente) { + $venta = new Venta($nombreCliente); + $venta->agregarBoletos($boletos); + + if (count($venta->getBoletos()) > 0) { + $resultado = $this->baseDatos->guardarVenta($venta); + if ($resultado) { + return $venta->generarComprobante(); + } + } + + return null; + } +} \ No newline at end of file diff --git a/controlador/asientos.php b/controlador/asientos.php new file mode 100644 index 0000000..fd8c6e4 --- /dev/null +++ b/controlador/asientos.php @@ -0,0 +1,38 @@ +<?php +// api/asientos.php - API para obtener el mapa de asientos +session_start(); + +require_once '../modelo/BaseDatos.php'; +require_once '../modelo/Sala.php'; +require_once '../modelo/Boleto.php'; +require_once '../modelo/Venta.php'; +require_once 'VendedorController.php'; + +header('Content-Type: application/json'); + +// Conexión a base de datos +$db = new BaseDatos('localhost:3306', 'root', '481037', 'boletos_db'); + +// Inicializar el controlador +$vendedorController = new VendedorController($db); + +// Cargar sala (solo hay una sala con id=1) +$sala = $vendedorController->cargarSala(1); + +// Si no hay sala, podríamos inicializarla para desarrollo +if (!$sala) { + $sala = new Sala(1, 'Sala Principal'); + $sala->inicializarBoletos(10, 15, 50.00); // 10 filas, 15 asientos por fila, $50 cada uno +} + +// Obtener mapa de asientos +$mapaAsientos = $vendedorController->mostrarDisponibilidadAsientos(); + +// Preparar respuesta +$response = [ + 'success' => true, + 'mapa' => $mapaAsientos, + 'precio' => 50.00 // Agregamos el precio de los boletos a la respuesta +]; + +echo json_encode($response); \ No newline at end of file diff --git a/controlador/comprobante.php b/controlador/comprobante.php new file mode 100644 index 0000000..7c544d9 --- /dev/null +++ b/controlador/comprobante.php @@ -0,0 +1,17 @@ +<?php +// comprobante.php - Lógica de servidor para el comprobante de venta +session_start(); + +// Verificar si hay un comprobante en la sesión +if (!isset($_SESSION['comprobante'])) { + header('Location: ../vista/index.html'); + exit; +} + +// Obtener el comprobante de la sesión +$comprobante = $_SESSION['comprobante']; + +// Devolver datos en formato JSON para ser consumidos por JavaScript +header('Content-Type: application/json'); +echo json_encode($comprobante); +?> \ No newline at end of file diff --git a/controlador/venta.php b/controlador/venta.php new file mode 100644 index 0000000..f976d92 --- /dev/null +++ b/controlador/venta.php @@ -0,0 +1,71 @@ +<?php +// api/venta.php - API para procesar ventas +session_start(); + +require_once '../modelo/BaseDatos.php'; +require_once '../modelo/Sala.php'; +require_once '../modelo/Boleto.php'; +require_once '../modelo/Venta.php'; +require_once 'VendedorController.php'; + +header('Content-Type: application/json'); + +// Verificar método de solicitud +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['success' => false, 'mensaje' => 'Método no permitido']); + exit; +} + +// Obtener datos JSON del cuerpo de la solicitud +$data = json_decode(file_get_contents('php://input'), true); + +if (!$data || !isset($data['asientos']) || !isset($data['nombre_cliente'])) { + echo json_encode(['success' => false, 'mensaje' => 'Datos incompletos']); + exit; +} + +// Extraer datos +$idsBoletos = $data['asientos']; +$nombreCliente = $data['nombre_cliente']; + +// Validar que haya asientos seleccionados +if (empty($idsBoletos)) { + echo json_encode(['success' => false, 'mensaje' => 'No se han seleccionado asientos']); + exit; +} + +// Conexión a base de datos +$db = new BaseDatos('localhost:3306', 'root', '481037', 'boletos_db'); + +// Inicializar el controlador +$vendedorController = new VendedorController($db); + +// Cargar la sala (agregando esta línea) +$vendedorController->cargarSala(1); // O el ID de sala correspondiente + +// Seleccionar boletos +$boletosSeleccionados = $vendedorController->seleccionarBoletos($idsBoletos); + +if (count($boletosSeleccionados) == 0) { + echo json_encode(['success' => false, 'mensaje' => 'No se han seleccionado asientos disponibles']); + exit; +} + +// Procesar venta +$comprobante = $vendedorController->procesarVenta($boletosSeleccionados, $nombreCliente); + +if (!$comprobante) { + echo json_encode(['success' => false, 'mensaje' => 'Error al procesar la venta. Intente nuevamente']); + exit; +} + +// Guardar comprobante en sesión +$_SESSION['comprobante'] = $comprobante; + +// Respuesta exitosa +echo json_encode([ + 'success' => true, + 'mensaje' => 'Venta procesada con éxito', + 'redirect' => '../vista/comprobante.html', + 'comprobante' => $comprobante +]); \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..968cc46 --- /dev/null +++ b/index.php @@ -0,0 +1,5 @@ +<?php +// Redirigir a la página principal del sistema +header("Location: vista/index.html"); +exit(); +?> diff --git a/modelo/BaseDatos.php b/modelo/BaseDatos.php new file mode 100644 index 0000000..c27b5a1 --- /dev/null +++ b/modelo/BaseDatos.php @@ -0,0 +1,94 @@ +<?php +require_once 'Sala.php'; +require_once 'Boleto.php'; +require_once 'Venta.php'; + +class BaseDatos { + private $conexion; + + public function __construct($host, $usuario, $password, $nombreBD) { + $this->conexion = new mysqli($host, $usuario, $password, $nombreBD); + + if ($this->conexion->connect_error) { + die("Error de conexión: " . $this->conexion->connect_error); + } + } + + public function cargarSala($idSala) { + $sala = null; + $query = "SELECT id, nombre FROM salas WHERE id = ?"; + $stmt = $this->conexion->prepare($query); + $stmt->bind_param("i", $idSala); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($fila = $result->fetch_assoc()) { + $sala = new Sala($fila['id'], $fila['nombre']); + + // Cargar boletos de la sala + $queryBoletos = "SELECT id, fila, numero, precio, estado FROM boletos WHERE id_sala = ?"; + $stmtBoletos = $this->conexion->prepare($queryBoletos); + $stmtBoletos->bind_param("i", $idSala); + $stmtBoletos->execute(); + $resultBoletos = $stmtBoletos->get_result(); + + $boletos = []; + while ($filaBoleto = $resultBoletos->fetch_assoc()) { + $boleto = new Boleto($filaBoleto['id'], $filaBoleto['fila'], $filaBoleto['numero'], $filaBoleto['precio']); + if ($filaBoleto['estado'] === 'vendido') { + $boleto->marcarComoVendido(); + } + $boletos[] = $boleto; + } + + // Asignar boletos a la sala + $sala->setBoletos($boletos); + } + + return $sala; + } + + public function guardarVenta($venta) { + // Iniciar transacción + $this->conexion->begin_transaction(); + + try { + // Insertar venta + $query = "INSERT INTO ventas (id, fecha, nombre_cliente, total) VALUES (?, ?, ?, ?)"; + $stmt = $this->conexion->prepare($query); + $id = $venta->getId(); + $fecha = $venta->getFecha(); + $nombreCliente = $venta->getNombreCliente(); + $total = $venta->getTotal(); + $stmt->bind_param("sssd", $id, $fecha, $nombreCliente, $total); + $stmt->execute(); + + // Actualizar estado de boletos + foreach ($venta->getBoletos() as $boleto) { + $queryBoleto = "UPDATE boletos SET estado = 'vendido' WHERE id = ?"; + $stmtBoleto = $this->conexion->prepare($queryBoleto); + $idBoleto = $boleto->getId(); + $stmtBoleto->bind_param("i", $idBoleto); + $stmtBoleto->execute(); + + // Insertar relación venta-boleto + $queryRelacion = "INSERT INTO venta_boletos (id_venta, id_boleto) VALUES (?, ?)"; + $stmtRelacion = $this->conexion->prepare($queryRelacion); + $stmtRelacion->bind_param("si", $id, $idBoleto); + $stmtRelacion->execute(); + } + + // Confirmar transacción + $this->conexion->commit(); + return true; + } catch (Exception $e) { + // Revertir transacción en caso de error + $this->conexion->rollback(); + return false; + } + } + + public function cerrarConexion() { + $this->conexion->close(); + } +} \ No newline at end of file diff --git a/modelo/Boleto.php b/modelo/Boleto.php new file mode 100644 index 0000000..bec7a82 --- /dev/null +++ b/modelo/Boleto.php @@ -0,0 +1,46 @@ +<?php +class Boleto { + private $id; + private $fila; + private $numero; + private $precio; + private $estado; // 'disponible' o 'vendido' + + public function __construct($id, $fila, $numero, $precio) { + $this->id = $id; + $this->fila = $fila; + $this->numero = $numero; + $this->precio = $precio; + $this->estado = 'disponible'; + } + + // Getters y setters + public function getId() { + return $this->id; + } + + public function getFila() { + return $this->fila; + } + + public function getNumero() { + return $this->numero; + } + + public function getPrecio() { + return $this->precio; + } + + public function getEstado() { + return $this->estado; + } + + public function marcarComoVendido() { + $this->estado = 'vendido'; + return true; + } + + public function estaDisponible() { + return $this->estado === 'disponible'; + } +} \ No newline at end of file diff --git a/modelo/Sala.php b/modelo/Sala.php new file mode 100644 index 0000000..b1ea1af --- /dev/null +++ b/modelo/Sala.php @@ -0,0 +1,62 @@ +<?php +require_once 'Boleto.php'; + +class Sala { + private $id; + private $nombre; + private $boletos = []; + + public function __construct($id, $nombre) { + $this->id = $id; + $this->nombre = $nombre; + } + + public function getId() { + return $this->id; + } + + public function getNombre() { + return $this->nombre; + } + + public function inicializarBoletos($filas, $asientosPorFila, $precio) { + $this->boletos = []; + $contador = 1; + + for ($i = 1; $i <= $filas; $i++) { + for ($j = 1; $j <= $asientosPorFila; $j++) { + $this->boletos[] = new Boleto($contador, $i, $j, $precio); + $contador++; + } + } + } + + public function obtenerBoletos() { + return $this->boletos; + } + + public function obtenerBoletosPorEstado($estado) { + return array_filter($this->boletos, function($boleto) use ($estado) { + return $boleto->getEstado() === $estado; + }); + } + + public function obtenerBoletosPorId($ids) { + return array_filter($this->boletos, function($boleto) use ($ids) { + return in_array($boleto->getId(), $ids); + }); + } + + public function disponibilidadAsientos() { + $mapa = []; + foreach ($this->boletos as $boleto) { + $mapa[$boleto->getFila()][$boleto->getNumero()] = $boleto->getEstado(); + } + return $mapa; + } + + // Setter para asignar boletos desde la base de datos + public function setBoletos($boletos) { + $this->boletos = $boletos; + } +} \ No newline at end of file diff --git a/modelo/Venta.php b/modelo/Venta.php new file mode 100644 index 0000000..1df14ae --- /dev/null +++ b/modelo/Venta.php @@ -0,0 +1,69 @@ +<?php +require_once 'Boleto.php'; + +class Venta { + private $id; + private $fecha; + private $boletos = []; + private $total; + private $nombreCliente; + + public function __construct($nombreCliente) { + $this->id = bin2hex(random_bytes(8)); + date_default_timezone_set('America/Mexico_City'); + $this->fecha = date('Y-m-d H:i:s'); + $this->nombreCliente = $nombreCliente; + $this->total = 0; + } + + public function getId() { + return $this->id; + } + + public function getFecha() { + return $this->fecha; + } + + public function getNombreCliente() { + return $this->nombreCliente; + } + + public function agregarBoletos($boletos) { + foreach ($boletos as $boleto) { + if ($boleto->estaDisponible()) { + $this->boletos[] = $boleto; + $this->total += $boleto->getPrecio(); + $boleto->marcarComoVendido(); + } + } + } + + public function generarComprobante() { + $comprobante = [ + 'id_venta' => $this->id, + 'fecha' => $this->fecha, + 'cliente' => $this->nombreCliente, + 'boletos' => [], + 'total' => $this->total + ]; + + foreach ($this->boletos as $boleto) { + $comprobante['boletos'][] = [ + 'id' => $boleto->getId(), + 'fila' => $boleto->getFila(), + 'numero' => $boleto->getNumero(), + 'precio' => $boleto->getPrecio() + ]; + } + + return $comprobante; + } + + public function getTotal() { + return $this->total; + } + + public function getBoletos() { + return $this->boletos; + } +} \ No newline at end of file diff --git a/vista/comprobante.html b/vista/comprobante.html new file mode 100644 index 0000000..69b427d --- /dev/null +++ b/vista/comprobante.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html lang="es"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Comprobante de Venta</title> + <link rel="stylesheet" href="css/comprobante.css"> +</head> +<body> + <div class="container"> + <h1>Comprobante de Venta</h1> + + <div class="comprobante" id="comprobante-imprimible"> + <div class="header"> + <h2>Boletos para Concierto - Sala Principal</h2> + </div> + + <div class="info-venta"> + <div> + <div class="info-item"> + <span class="info-label">Nº de Venta:</span> + <span id="id-venta"></span> + </div> + <div class="info-item"> + <span class="info-label">Fecha:</span> + <span id="fecha-venta"></span> + </div> + </div> + <div> + <div class="info-item"> + <span class="info-label">Cliente:</span> + <span id="cliente-venta"></span> + </div> + </div> + </div> + + <h3>Detalle de Boletos</h3> + <table> + <thead> + <tr> + <th>#</th> + <th>Ubicación</th> + <th>Precio</th> + </tr> + </thead> + <tbody id="detalle-boletos"> + <!-- El contenido de la tabla se generará dinámicamente con JavaScript --> + </tbody> + </table> + + <div class="total"> + Total: $<span id="total-venta"></span> + </div> + + <div style="margin-top: 40px; font-size: 14px; text-align: center;"> + <p>¡Gracias por su compra!</p> + <p>Este comprobante es su entrada oficial para el evento.</p> + <p>Por favor, preséntelo en la entrada del concierto.</p> + </div> + </div> + + <div class="acciones"> + <a href="index.html" class="btn">Volver a Ventas</a> + <button class="btn btn-print" onclick="imprimirComprobante()">Imprimir Comprobante</button> + </div> + </div> + + <script src="js/comprobante.js"></script> +</body> +</html> \ No newline at end of file diff --git a/vista/css/comprobante.css b/vista/css/comprobante.css new file mode 100644 index 0000000..bd3d2c1 --- /dev/null +++ b/vista/css/comprobante.css @@ -0,0 +1,81 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} +.container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} +h1, h2 { + color: #333; +} +.comprobante { + border: 1px solid #ddd; + padding: 20px; + border-radius: 4px; + margin: 20px 0; +} +.header { + border-bottom: 2px solid #333; + padding-bottom: 10px; + margin-bottom: 20px; +} +.info-venta { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} +.info-item { + margin-bottom: 10px; +} +.info-label { + font-weight: bold; +} +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} +th, td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} +th { + background-color: #f8f9fa; +} +.total { + font-size: 18px; + font-weight: bold; + text-align: right; + margin-top: 20px; + padding-top: 10px; + border-top: 1px solid #ddd; +} +.acciones { + display: flex; + justify-content: space-between; + margin-top: 20px; +} +.btn { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + text-decoration: none; + cursor: pointer; + display: inline-block; +} +.btn-print { + background-color: #6c757d; +} +.btn:hover { + opacity: 0.9; +} \ No newline at end of file diff --git a/vista/css/index.css b/vista/css/index.css new file mode 100644 index 0000000..2c00741 --- /dev/null +++ b/vista/css/index.css @@ -0,0 +1,131 @@ + + +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} +.container { + max-width: 1200px; + margin: 0 auto; + background-color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} +h1, h2 { + color: #333; +} +.mensaje { + background-color: #f8d7da; + color: #721c24; + padding: 10px; + margin-bottom: 20px; + border-radius: 4px; + display: none; +} +.sala { + margin: 20px 0; + text-align: center; +} +.escenario { + background-color: #ddd; + padding: 10px; + margin-bottom: 30px; + border-radius: 4px; + text-align: center; + font-weight: bold; +} +.filas { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} +.fila { + display: flex; + gap: 10px; + align-items: center; +} +.numero-fila { + width: 30px; + text-align: center; + font-weight: bold; +} +.asientos { + display: flex; + gap: 5px; +} +.asiento { + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + cursor: pointer; + user-select: none; + font-size: 12px; +} +.disponible { + background-color: #28a745; + color: white; +} +.vendido { + background-color: #dc3545; + color: white; + cursor: not-allowed; +} +.seleccionado { + background-color: #007bff; + color: white; +} +.form-group { + margin-bottom: 15px; +} +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} +input[type="text"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; +} +.btn { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; +} +.btn:hover { + background-color: #0069d9; +} +.resumen { + margin-top: 20px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 4px; +} +.leyenda { + display: flex; + gap: 15px; + margin-top: 20px; + justify-content: center; +} +.leyenda-item { + display: flex; + align-items: center; + gap: 5px; +} +.leyenda-color { + width: 20px; + height: 20px; + border-radius: 3px; +} \ No newline at end of file diff --git a/vista/index.html b/vista/index.html new file mode 100644 index 0000000..3e903b5 --- /dev/null +++ b/vista/index.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html lang="es"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Sistema de Venta de Boletos</title> + <link rel="stylesheet" href="css/index.css"> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> +</head> +<body> + <div class="container"> + <h1>Sistema de Venta de Boletos para Concierto</h1> + + <div id="mensajeAlerta" class="alert alert-warning d-none" role="alert"></div> + + <div class="sala"> + <h2>Selección de Asientos - Sala Principal</h2> + <div class="escenario">ESCENARIO</div> + + <div class="filas" id="mapaAsientos"> + <!-- El mapa de asientos se cargará dinámicamente con JavaScript --> + </div> + + <div class="leyenda"> + <div class="leyenda-item"> + <div class="leyenda-color" style="background-color: #28a745;"></div> + <span>Disponible</span> + </div> + <div class="leyenda-item"> + <div class="leyenda-color" style="background-color: #007bff;"></div> + <span>Seleccionado</span> + </div> + <div class="leyenda-item"> + <div class="leyenda-color" style="background-color: #dc3545;"></div> + <span>Vendido</span> + </div> + </div> + </div> + + <form id="formularioVenta"> + <div class="form-group"> + <label for="nombre_cliente">Nombre del Cliente:</label> + <input type="text" id="nombre_cliente" name="nombre_cliente" class="form-control" required> + </div> + + <div class="resumen" id="resumen"> + <h3>Resumen de Selección</h3> + <p>Asientos seleccionados: <span id="asientosSeleccionados">Ninguno</span></p> + <p>Total: $<span id="totalVenta">0.00</span></p> + <button type="submit" id="btnVender" class="btn btn-primary">Confirmar Venta</button> + </div> + + <!-- Campo oculto para almacenar IDs de asientos seleccionados --> + <div id="asientosSeleccionadosInput"></div> + </form> + </div> + + <script src="js/index.js"></script> +</body> +</html> \ No newline at end of file diff --git a/vista/js/comprobante.js b/vista/js/comprobante.js new file mode 100644 index 0000000..c538e80 --- /dev/null +++ b/vista/js/comprobante.js @@ -0,0 +1,80 @@ +// comprobante.js - Maneja la comunicación entre el PHP y el HTML + +// Cuando el documento esté listo, cargar los datos del comprobante +document.addEventListener('DOMContentLoaded', cargarComprobante); + +// Función para cargar los datos del comprobante desde el servidor +function cargarComprobante() { + fetch('../controlador/comprobante.php') + .then(response => { + if (!response.ok) { + throw new Error('Error al obtener los datos del comprobante'); + } + return response.json(); + }) + .then(comprobante => { + // Llenar los datos básicos del comprobante + document.getElementById('id-venta').textContent = comprobante.id_venta; + document.getElementById('fecha-venta').textContent = comprobante.fecha; + document.getElementById('cliente-venta').textContent = comprobante.cliente; + document.getElementById('total-venta').textContent = formatearNumero(comprobante.total); + + // Generar las filas de la tabla de boletos + const tablaBoletos = document.getElementById('detalle-boletos'); + let contenidoTabla = ''; + + comprobante.boletos.forEach((boleto, index) => { + contenidoTabla += ` + <tr> + <td>${index + 1}</td> + <td>Fila ${boleto.fila}, Asiento ${boleto.numero}</td> + <td>$${formatearNumero(boleto.precio)}</td> + </tr> + `; + }); + + tablaBoletos.innerHTML = contenidoTabla; + }) + .catch(error => { + console.error('Error:', error); + alert('No se pudo cargar el comprobante. Por favor, inténtelo de nuevo.'); + window.location.href = 'index.html'; + }); +} + +// Función para imprimir el comprobante +function imprimirComprobante() { + const contenido = document.getElementById('comprobante-imprimible').innerHTML; + const ventanaImpresion = window.open('', '_blank'); + + ventanaImpresion.document.write(` + <html> + <head> + <title>Comprobante de Venta</title> + <style> + body { font-family: Arial, sans-serif; } + .header { border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; } + .info-venta { display: flex; justify-content: space-between; margin-bottom: 20px; } + .info-item { margin-bottom: 10px; } + .info-label { font-weight: bold; } + table { width: 100%; border-collapse: collapse; margin-bottom: 20px; } + th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } + th { background-color: #f8f9fa; } + .total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; padding-top: 10px; border-top: 1px solid #ddd; } + </style> + </head> + <body> + ${contenido} + </body> + </html> + `); + + ventanaImpresion.document.close(); + ventanaImpresion.focus(); + ventanaImpresion.print(); +} + +// Función auxiliar para formatear números con dos decimales +function formatearNumero(numero) { + return Number(numero).toFixed(2); +} \ No newline at end of file diff --git a/vista/js/index.js b/vista/js/index.js new file mode 100644 index 0000000..ce74209 --- /dev/null +++ b/vista/js/index.js @@ -0,0 +1,206 @@ +// js/index.js - Script para manejar la interfaz de usuario y la comunicación con el backend + +document.addEventListener('DOMContentLoaded', function() { + let seleccionados = []; + const precioBoleto = 50.00; // Precio por boleto + + // Cargar el mapa de asientos al iniciar + cargarMapaAsientos(); + + // Agregar manejador para el formulario + const formulario = document.getElementById('formularioVenta'); + formulario.addEventListener('submit', validarYEnviarFormulario); + + // Función para cargar el mapa de asientos desde la API + async function cargarMapaAsientos() { + try { + const response = await fetch('../controlador/asientos.php'); + const data = await response.json(); + + if (!data.success) { + mostrarMensaje('Error al cargar el mapa de asientos', 'error'); + return; + } + + renderizarMapaAsientos(data.mapa); + } catch (error) { + console.error('Error al cargar el mapa de asientos:', error); + mostrarMensaje('Error de conexión con el servidor', 'error'); + } + } + + // Función para renderizar el mapa de asientos + function renderizarMapaAsientos(mapa) { + const contenedor = document.getElementById('mapaAsientos'); + contenedor.innerHTML = ''; + + Object.entries(mapa).forEach(([numeroFila, asientosEnFila]) => { + const filaElement = document.createElement('div'); + filaElement.className = 'fila'; + + // Crear elemento para el número de fila + const numeroFilaElement = document.createElement('div'); + numeroFilaElement.className = 'numero-fila'; + numeroFilaElement.textContent = `F${numeroFila}`; + filaElement.appendChild(numeroFilaElement); + + // Crear contenedor para los asientos + const asientosContainer = document.createElement('div'); + asientosContainer.className = 'asientos'; + + // Crear cada asiento + Object.entries(asientosEnFila).forEach(([numeroAsiento, estado]) => { + const idBoleto = ((parseInt(numeroFila) - 1) * Object.keys(asientosEnFila).length) + parseInt(numeroAsiento); + const asiento = document.createElement('div'); + + // Determinar la clase según el estado + asiento.className = `asiento ${estado === 'disponible' ? 'disponible' : 'vendido'}`; + asiento.dataset.id = idBoleto; + asiento.textContent = numeroAsiento; + + asientosContainer.appendChild(asiento); + }); + + filaElement.appendChild(asientosContainer); + contenedor.appendChild(filaElement); + }); + + // Una vez que se ha renderizado, añadir los eventos + configurarEventosAsientos(); + } + + // Configurar eventos para los asientos + function configurarEventosAsientos() { + const asientos = document.querySelectorAll('.asiento.disponible'); + + asientos.forEach(asiento => { + asiento.addEventListener('click', function() { + const asientoId = parseInt(this.getAttribute('data-id')); + + // Verificar si ya está seleccionado + const indice = seleccionados.findIndex(a => a.id === asientoId); + + if (indice === -1) { + // Agregar a seleccionados + seleccionados.push({ + id: asientoId, + elemento: this + }); + this.classList.remove('disponible'); + this.classList.add('seleccionado'); + } else { + // Quitar de seleccionados + seleccionados.splice(indice, 1); + this.classList.remove('seleccionado'); + this.classList.add('disponible'); + } + + actualizarResumen(); + }); + }); + } + + // Función para actualizar el resumen de venta + function actualizarResumen() { + const resumenAsientos = document.getElementById('asientosSeleccionados'); + const resumenTotal = document.getElementById('totalVenta'); + const asientosInput = document.getElementById('asientosSeleccionadosInput'); + + if (seleccionados.length === 0) { + resumenAsientos.textContent = 'Ninguno'; + resumenTotal.textContent = '0.00'; + + // Limpiar los inputs ocultos + if (asientosInput) { + asientosInput.innerHTML = ''; + } + } else { + // Mostrar los asientos seleccionados + const detalles = seleccionados.map(asiento => { + const fila = Math.floor((asiento.id - 1) / 15) + 1; + const numero = ((asiento.id - 1) % 15) + 1; + return `F${fila}-${numero}`; + }); + + resumenAsientos.textContent = detalles.join(', '); + resumenTotal.textContent = (seleccionados.length * precioBoleto).toFixed(2); + + // Actualizar campo oculto para el envío del formulario + if (asientosInput) { + asientosInput.innerHTML = ''; + seleccionados.forEach(asiento => { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'asientos[]'; + input.value = asiento.id; + asientosInput.appendChild(input); + }); + } + } + } + + // Función para validar y enviar el formulario + function validarYEnviarFormulario(e) { + e.preventDefault(); + + if (seleccionados.length === 0) { + mostrarMensaje('Por favor, seleccione al menos un asiento.'); + return false; + } + + const nombreCliente = document.getElementById('nombre_cliente').value.trim(); + if (nombreCliente === '') { + mostrarMensaje('Por favor, ingrese el nombre del cliente.'); + return false; + } + + // Preparar los datos para enviar + const asientosIds = seleccionados.map(asiento => asiento.id); + const datos = { + asientos: asientosIds, + nombre_cliente: nombreCliente + }; + + // Enviar los datos mediante fetch + fetch('../controlador/venta.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(datos) + }) + .then(response => response.json()) + .then(result => { + if (result.success) { + // Redireccionar a la página de comprobante + window.location.href = result.redirect || 'comprobante.php'; + } else { + mostrarMensaje(result.mensaje || 'Error al procesar la venta', 'error'); + } + }) + .catch(error => { + console.error('Error:', error); + mostrarMensaje('Error de conexión con el servidor', 'error'); + }); + + return false; + } + + // Función para mostrar mensajes + function mostrarMensaje(texto, tipo = 'warning') { + const mensajeElement = document.getElementById('mensajeAlerta'); + if (mensajeElement) { + mensajeElement.textContent = texto; + mensajeElement.className = `alert alert-${tipo === 'error' ? 'danger' : 'warning'}`; + mensajeElement.classList.remove('d-none'); + + // Ocultar después de 5 segundos + setTimeout(() => { + mensajeElement.classList.add('d-none'); + }, 5000); + } else { + // Si no existe el elemento, usar alert + alert(texto); + } + } +}); \ No newline at end of file