From 24d0bf55c962fb961cf6327425fa62713d36f72f Mon Sep 17 00:00:00 2001 From: Axel Axt Date: Tue, 4 Aug 2026 19:24:03 -0300 Subject: [PATCH] Carpeta models --- app/models/cajaModel.php | 433 ++++++++++++++++++++++++++ app/models/clienteModel.php | 113 +++++++ app/models/configuracionModel.php | 137 +++++++++ app/models/informeModel.php | 217 +++++++++++++ app/models/logInModel.php | 85 ++++++ app/models/pedidoModel.php | 254 ++++++++++++++++ app/models/presupuestoModel.php | 190 ++++++++++++ app/models/productoModel.php | 490 ++++++++++++++++++++++++++++++ app/models/proveedorModel.php | 126 ++++++++ app/models/servicioModel.php | 447 +++++++++++++++++++++++++++ app/models/usuarioModel.php | 158 ++++++++++ app/models/ventaModel.php | 294 ++++++++++++++++++ 12 files changed, 2944 insertions(+) create mode 100644 app/models/cajaModel.php create mode 100644 app/models/clienteModel.php create mode 100644 app/models/configuracionModel.php create mode 100644 app/models/informeModel.php create mode 100644 app/models/logInModel.php create mode 100644 app/models/pedidoModel.php create mode 100644 app/models/presupuestoModel.php create mode 100644 app/models/productoModel.php create mode 100644 app/models/proveedorModel.php create mode 100644 app/models/servicioModel.php create mode 100644 app/models/usuarioModel.php create mode 100644 app/models/ventaModel.php diff --git a/app/models/cajaModel.php b/app/models/cajaModel.php new file mode 100644 index 0000000..8d0424f --- /dev/null +++ b/app/models/cajaModel.php @@ -0,0 +1,433 @@ +ventaKey])) { + $_SESSION[$this->ventaKey] = []; + } + + if (!isset($_SESSION[$this->pedidoKey])) { + $_SESSION[$this->pedidoKey] = []; + } + + if (!isset($_SESSION[$this->servicioKey])) { + $_SESSION[$this->servicioKey] = []; + } + } + + // MÉTODO PARA OBTENER ERRORES Y MANEJARLOS EN CONTROLLER + + public function getError(): ?string { + return $this->error; + } + + // <---------------------------------- MANEJO DE CAJA PARA VENTA ----------------------------------> + + // AGREGAR PRODUCTO + + public function agregarProducto(array $producto, int $cantidad): bool { + + if ($cantidad <= 0 || empty($producto['id'])) { + $this->error = 'Datos inválidos del producto.'; + return false; + } + + $id = $producto['id']; + $stockDisponible = $producto['stock']; + + // Si el producto ya se encuentra agregado + + if (isset($_SESSION[$this->ventaKey][$id])) { + + if ($cantidad > $stockDisponible) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->ventaKey][$id]['cantidad'] = $cantidad; + return true; + } + + // Si aún no existe en el array continua la ejecución + + if ($cantidad > $stockDisponible) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + // Cálculos + + $descuentoTotal = $producto['descuento_producto'] + $producto['descuento_categoria']; + + $precioFinal = ((100 - $descuentoTotal) * $producto['precio'] / 100); // PRECIO UNITARIO MENOS EL DESCUENTO + + // Se guardan los datos en el array + + $_SESSION[$this->ventaKey][$id] = [ + 'id_producto' => $id, + 'marca' => $producto['marca'], + 'modelo' => $producto['modelo'], + 'precio' => $producto['precio'], + 'descuento_total' => $descuentoTotal, + 'cantidad' => $cantidad, + 'stock' => $producto['stock'], + 'precio_final' => $precioFinal + ]; + + return true; + } + + // ELIMINAR PRODUCTO + + public function eliminarProducto(int $id): bool { + + if (!isset($_SESSION[$this->ventaKey][$id])) { + $this->error = 'El producto no existe en la caja.'; + return false; + } else{ + unset($_SESSION[$this->ventaKey][$id]); + } + + return true; + } + + // EDITAR CANTIDAD DE PRODUCTO + + public function editarCantidad(int $id, int $nuevaCantidad): bool { + + if (!isset($_SESSION[$this->ventaKey][$id])) { + $this->error = 'Producto inexistente en la caja.'; + return false; + } + + if ($nuevaCantidad <= 0) { + $this->error = 'La cantidad debe ser mayor a cero.'; + return false; + } + + if ($nuevaCantidad > $_SESSION[$this->ventaKey][$id]['stock']) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->ventaKey][$id]['cantidad'] = $nuevaCantidad; + return true; + } + + // LISTAR PRODUCTOS + + public function listarProductos(): array { + return $_SESSION[$this->ventaKey]; + } + + // CALCULAR SUBTOTAL + + public function calcularSubtotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->ventaKey] as $item) { + + $precio = (float) $item['precio']; + + $total += $precio * $item['cantidad']; + } + + return round($total, 2); + } + + // CALCULAR PRECIO TOTAL + + public function calcularPrecioTotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->ventaKey] as $item) { + + $precio = (float) $item['precio']; + + if (!empty($item['descuento_total'])) { + $precio -= ($item['descuento_total'] * $precio / 100); + } + + $total += $precio * $item['cantidad']; + } + + return round($total, 2); + } + + // CALCULAR DESCUENTO TOTAL + + public function calcularDescuentoTotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->ventaKey] as $item) { + + $precio = (float) $item['precio']; + $descuento = 0; + + if (!empty($item['descuento_total'])) { + $descuento += ($item['descuento_total'] * $precio / 100); + } + + $total += $descuento * $item['cantidad']; + } + + return round($total, 2); + } + + // VACIAR CAJA DE PRODUCTOS + + public function vaciarCajaVenta(): bool { + $_SESSION[$this->ventaKey] = []; + return true; + } + + // <---------------------------------- MANEJO DE CAJA PARA PEDIDO ----------------------------------> + + // AGREGAR PRODUCTO PARA PEDIDO + + public function agregarProductoPedido(array $producto, float $costo, int $cantidad): bool { + + if ($cantidad <= 0 || $costo <= 0 || empty($producto['id'])) { + $this->error = 'Datos inválidos del producto.'; + return false; + } + + $id = $producto['id']; + $ganancia = (float) ($producto['porcentaje_ganancia'] ?? 0); + + $precio = (100 + $ganancia) * $costo / 100; + $stockfinal = $producto['stock'] + $cantidad; + + // Si el producto se encuentra en el array + + if (isset($_SESSION[$this->pedidoKey][$id])) { + $_SESSION[$this->pedidoKey][$id]['costo'] = $costo; + $_SESSION[$this->pedidoKey][$id]['precio'] = $precio; + $_SESSION[$this->pedidoKey][$id]['cantidad'] = $cantidad; + $_SESSION[$this->pedidoKey][$id]['stock_final'] = $stockfinal; + return true; + } + + // Caso contrario se guarda en el arreglo + + $_SESSION[$this->pedidoKey][$id] = [ + 'id_producto' => $id, + 'marca' => $producto['marca'], + 'modelo' => $producto['modelo'], + 'costo' => $costo, + 'precio' => $precio, + 'cantidad' => $cantidad, + 'stock' => $producto['stock'], + 'stock_final' => $stockfinal, + 'porcentaje_ganancia' => $ganancia + ]; + + return true; + } + + // CALCULAR TOTAL DE PEDIDO + + public function calcularPrecioTotalPedido(): float { + + $total = 0; + + if (empty($_SESSION[$this->pedidoKey])) { + return 0; + } + + foreach ($_SESSION[$this->pedidoKey] as $item) { + + $costo = (float) $item['costo']; + + $total += $costo * $item['cantidad']; + } + + return round($total, 2); + } + + // LISTAR PRODUCTOS PARA PEDIDO + + public function listarProductosPedido(): array { + return $_SESSION[$this->pedidoKey] ?? []; + } + + // ELIMINAR PRODUCTO PARA PEDIDO DE LA CAJA + + public function eliminarProductoPedido($id): bool { + + if (!isset($_SESSION[$this->pedidoKey][$id])) { + $this->error = 'El producto no existe en el pedido.'; + return false; + } else { + unset($_SESSION[$this->pedidoKey][$id]); + } + + return true; + } + + // VACIAR CAJA DE PRODUCTOS PARA PEDIDO + + public function vaciarCajaPedido(): bool { + $_SESSION[$this->pedidoKey] = []; + return true; + } + + // EDITAR DATOS DE PRODUCTO PARA PEDIDO + + public function editarDatosProductoPedido(int $id, $nuevoCosto, $nuevaCantidad): bool { + + if ($nuevoCosto <= 0 || $nuevaCantidad <= 0) { + $this->error = 'Costo o cantidad inválidos.'; + return false; + } + + if (!isset($_SESSION[$this->pedidoKey][$id])) { + return false; + } else { + + $_SESSION[$this->pedidoKey][$id]['costo'] = $nuevoCosto; + $_SESSION[$this->pedidoKey][$id]['cantidad'] = $nuevaCantidad; + + // Actualización de otro datos + + $precio = (100 + $_SESSION[$this->pedidoKey][$id]['porcentaje_ganancia']) * $nuevoCosto / 100; + + $stockfinal = $_SESSION[$this->pedidoKey][$id]['stock'] + $nuevaCantidad; + + $_SESSION[$this->pedidoKey][$id]['precio'] = $precio; + $_SESSION[$this->pedidoKey][$id]['stock_final'] = $stockfinal; + } + + return true; + } + + // <---------------------------------- MANEJO DE CAJA PARA SERVICIO ----------------------------------> + + // LISTAR INSUMOS SERVICIO + + public function listarInsumos(): array { + return $_SESSION[$this->servicioKey]; + } + + // CALCULAR EL TOTAL DEL SERVICIO + + public function calcularPrecioTotalServicio(): float { + + $total = 0; + + if (empty($_SESSION[$this->servicioKey])) { + return 0; + } + + foreach ($_SESSION[$this->servicioKey] as $item) { + + $precio = (float) $item['precio']; + + $total += $precio * $item['cantidad']; + } + + return round($total, 2); + } + + // AGREGAR INSUMO A LA CAJA + + public function agregarInsumo(array $insumo, int $cantidad): bool { + + if ($cantidad <= 0 || empty($insumo['id'])) { + $this->error = 'Datos inválidos del insumo.'; + return false; + } + + $id = $insumo['id']; + $stockDisponible = $insumo['stock']; + + // Si el insumo ya existe en el array + + if (isset($_SESSION[$this->servicioKey][$id])) { + + if ($cantidad > $stockDisponible) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->servicioKey][$id]['cantidad'] = $cantidad; + return true; + } + + // Si no se encuentra en el array, lo inserta + + if ($cantidad > $stockDisponible) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->servicioKey][$id] = [ + 'id_producto' => $id, + 'marca' => $insumo['marca'], + 'modelo' => $insumo['modelo'], + 'precio' => $insumo['precio'], + 'cantidad' => $cantidad, + 'stock' => $insumo['stock'], + ]; + + return true; + } + + // VACIAR CAJA DE INSUMOS PARA SERVICIO + + public function vaciarCajaServicio(): bool { + $_SESSION[$this->servicioKey] = []; + return true; + } + + // ELIMINAR INSUMO PARA SERVICIO DE LA CAJA + + public function eliminarInsumoServicio($id): bool { + + if (!isset($_SESSION[$this->servicioKey][$id])) { + $this->error = 'El insumo no existe en el pedido.'; + return false; + } else { + unset($_SESSION[$this->servicioKey][$id]); + } + + return true; + } + + // EDITAR CANTIDAD DE INSUMO + + public function editarCantidadInsumo(int $id, int $nuevaCantidad): bool { + + if (!isset($_SESSION[$this->servicioKey][$id])) { + $this->error = 'Insumo inexistente en la caja.'; + return false; + } + + if ($nuevaCantidad <= 0) { + $this->error = 'La cantidad debe ser mayor a cero.'; + return false; + } + + if ($nuevaCantidad > $_SESSION[$this->servicioKey][$id]['stock']) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->servicioKey][$id]['cantidad'] = $nuevaCantidad; + return true; + } +} \ No newline at end of file diff --git a/app/models/clienteModel.php b/app/models/clienteModel.php new file mode 100644 index 0000000..350af22 --- /dev/null +++ b/app/models/clienteModel.php @@ -0,0 +1,113 @@ +db = Database::getConnection(); + } + + // LISTAR CLIENTES + + public function listarClientes(): array { + + $sql = "SELECT id, nombre, apellido, telefono, email FROM clientes ORDER BY id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // CREAR CLIENTE + + public function crearCliente(array $data): bool { + + $sql = "INSERT INTO clientes (nombre, apellido, telefono, email, cuit, cond_iva) VALUES (:nombre, :apellido, :telefono, :email, :cuit, :cond_iva)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':nombre' => $data['nombre'], + ':apellido' => $data['apellido'], + ':telefono' => $data['telefono'], + ':email' => $data['email'], + ':cuit' => $data['cuit'], + ':cond_iva' => $data['cond_iva'] + ]); + } + + // OBTENER CLIENTE POR ID + + public function obtenerPorId(int $id): ?array { // El ? lo uso para que pueda devolver NULL + + $sql = "SELECT id, nombre, apellido, telefono, email, cuit, cond_iva FROM clientes WHERE (id = :id) LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); // Le asigna al parámetro nombrado el valor de $id + + $cliente = $stmt->fetch(PDO::FETCH_ASSOC); // Devuelve un array asociativo + + return $cliente ?: null; // Verifica si el array no esta vacío y lo devuelve, sino devuelve null + } + + // ACTUALIZAR DATOS DE CLIENTE + + public function actualizarCliente(int $id, array $data): bool { + + if (empty($data)) { + return false; + } + + $campos = []; + $params = []; // Se utiliza para evitar inyección SQL usando parámetros nombrados de PDO + + foreach ($data as $campo => $valor) { + $campos[] = "$campo = :$campo"; // Arma el string relacionando $campo con los parámetros nombrados (se usa en la consulta) + $params[":$campo"] = $valor; // Arma array relacionando el parámetro nombrado con el valor (se usa en la consulta) + } + + $params[':id'] = $id; + + $sql = "UPDATE clientes SET " . implode(', ', $campos) . " WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute($params); // Relaciona los nombrados con los valores + } + + // VERIFICAR EXISTENCIA DE CLIENTE (al crear) + + public function verificarExistencia(array $datosCliente, ?int $idExcluir = null): bool { + + $sql = "SELECT 1 FROM clientes WHERE ((nombre = :nombre AND apellido = :apellido AND email = :email) OR (email = :email) OR (cuit = :cuit))"; + + if ($idExcluir !== null) { + $sql .= " AND id <> :id_excluir"; + } + + $sql .= " LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $params = [ + ':nombre' => $datosCliente['nombre'], + ':apellido' => $datosCliente['apellido'], + ':email' => $datosCliente['email'], + ':cuit' => $datosCliente['cuit'] + ]; + + if ($idExcluir !== null) { + $params[':id_excluir'] = $idExcluir; + } + + $stmt->execute($params); + + return (bool) $stmt->fetchColumn(); + } +} \ No newline at end of file diff --git a/app/models/configuracionModel.php b/app/models/configuracionModel.php new file mode 100644 index 0000000..fe143af --- /dev/null +++ b/app/models/configuracionModel.php @@ -0,0 +1,137 @@ +db = Database::getConnection(); + } + + // OBTENER CONFIGRACIÓN + + public function obtenerConfiguracion() { + + $sql = "SELECT nombre, direccion, ciudad, codigo_postal, provincia, telefono, email, cond_iva, cuit, iva, inicio_act, + hora_apertura_maniana, hora_cierre_maniana, hora_apertura_tarde, hora_cierre_tarde, mail_automatico, logo_ruta, cargo_cancelacion_servicio, + email_emisor, contrasenia_emisor + FROM configuracion WHERE id = 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + // ACTUALIZAR CONFIGURACIÓN + + public function actualizarConfiguracion(array $datosEmpresa): bool { + + $sql = "UPDATE configuracion + SET nombre = :nombre, + direccion = :direccion, + ciudad = :ciudad, + codigo_postal = :codigo_postal, + provincia = :provincia, + telefono = :telefono, + email = :email, + cond_iva = :cond_iva, + cuit = :cuit, + iva = :iva, + hora_apertura_maniana = :hora_apertura_maniana, + hora_cierre_maniana = :hora_cierre_maniana, + hora_apertura_tarde = :hora_apertura_tarde, + hora_cierre_tarde = :hora_cierre_tarde, + mail_automatico = :mail_automatico, + cargo_cancelacion_servicio = :cargo_cancelacion_servicio, + email_emisor = :email_emisor, + contrasenia_emisor = :contrasenia_emisor + WHERE id = 1"; + + $stmt = $this->db->prepare($sql); + + return (bool) $stmt->execute([ + 'nombre' => $datosEmpresa['nombre'], + 'direccion' => $datosEmpresa['direccion'], + 'ciudad' => $datosEmpresa['ciudad'], + 'codigo_postal' => $datosEmpresa['codigo_postal'], + 'provincia' => $datosEmpresa['provincia'], + 'telefono' => $datosEmpresa['telefono'], + 'email' => $datosEmpresa['email'], + 'cond_iva' => $datosEmpresa['cond_iva'], + 'cuit' => $datosEmpresa['cuit'], + 'iva' => $datosEmpresa['iva'], + 'hora_apertura_maniana' => $datosEmpresa['hora_apertura_maniana'], + 'hora_cierre_maniana' => $datosEmpresa['hora_cierre_maniana'], + 'hora_apertura_tarde' => $datosEmpresa['hora_apertura_tarde'], + 'hora_cierre_tarde' => $datosEmpresa['hora_cierre_tarde'], + 'mail_automatico' => $datosEmpresa['mail_automatico'], + 'cargo_cancelacion_servicio' => $datosEmpresa['cargo_cancelacion_servicio'], + 'email_emisor' => $datosEmpresa['email_emisor'], + 'contrasenia_emisor' => $datosEmpresa['contrasenia_emisor'] + ]); + } + + // OBTENER EL MONTO AL CANCELAR EL SERVICIO + + public function obtenerCargoCancelacion(): float { + + $sql = "SELECT cargo_cancelacion_servicio FROM configuracion WHERE id = 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + return isset($row['cargo_cancelacion_servicio']) ? (float) $row['cargo_cancelacion_servicio'] : 0.0; + } + + // OBTENER DATOS NECESARIOS PARA EL ENVIO DE EMAIL + + public function obtenerDatosEnvioMail(): array { + + $sql = "SELECT direccion, ciudad, telefono, email, hora_apertura_maniana, hora_cierre_maniana, hora_apertura_tarde, hora_cierre_tarde, email_emisor, contrasenia_emisor, mail_automatico, cargo_cancelacion_servicio FROM configuracion + WHERE id = 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + // OBTENER FECHA DE ULTIMO BACKUP + + public function obtenerFechaUltimoBackup(): ?string { + + $backupDir = __DIR__ . '/../backups/'; + + $archivos = glob($backupDir . '*.sql'); + + if (!$archivos) { + return null; + } + + // Ordenar por fecha de modificación (más reciente primero) + + usort($archivos, function ($a, $b) { + return filemtime($b) - filemtime($a); + }); + + // Tomar el archivo más reciente + + $ultimoArchivo = $archivos[0]; + + // Obtener fecha desde filemtime + + $timestamp = filemtime($ultimoArchivo); + + // Formatear a Y-m-d + + return date('Y-m-d', $timestamp); + } +} \ No newline at end of file diff --git a/app/models/informeModel.php b/app/models/informeModel.php new file mode 100644 index 0000000..8069240 --- /dev/null +++ b/app/models/informeModel.php @@ -0,0 +1,217 @@ +db = Database::getConnection(); + } + + // OBTENER BALANCE ANUAL + + public function obtenerBalanceAnual($anio) { + + $sql = "WITH meses AS ( + SELECT generate_series(1,12) AS mes + ), + + -- INGRESOS DE VENTAS + + ingresos_ventas AS ( + SELECT mes, SUM(total) AS total FROM ( + SELECT + EXTRACT(MONTH FROM v.fecha)::int AS mes, + SUM(v.total) AS total + FROM ventas v + WHERE EXTRACT(YEAR FROM v.fecha)::int = :anio + GROUP BY mes + + ) sub GROUP BY mes + ), + + -- INGRESOS DE SERVICIOS + + ingresos_servicios AS ( + SELECT mes, SUM(total) AS total FROM ( + SELECT + EXTRACT(MONTH FROM s.fecha_terminado)::int AS mes, + SUM(s.total) AS total + FROM servicios s + WHERE EXTRACT(YEAR FROM s.fecha_terminado)::int = :anio + GROUP BY mes + ) sub GROUP BY mes + ), + + -- COSTOS DE PEDIDOS + + costos_pedidos AS ( + SELECT mes, SUM(total) AS total FROM ( + SELECT + EXTRACT(MONTH FROM p.fecha)::int AS mes, + SUM(p.total) AS total + FROM pedidos p + WHERE EXTRACT(YEAR FROM p.fecha)::int = :anio + GROUP BY mes + ) sub GROUP BY mes + ) + + -- SUMAS Y CALCULO DE UTILIDAD + + SELECT + m.mes, + COALESCE(SUM(iv.total),0) AS ventas, + COALESCE(SUM(isv.total),0) AS servicios, + COALESCE(SUM(cp.total),0) AS costos, + COALESCE(SUM(iv.total),0) + + COALESCE(SUM(isv.total),0) + - COALESCE(SUM(cp.total),0) AS utilidad + FROM meses m + LEFT JOIN ingresos_ventas iv ON (iv.mes = m.mes) + LEFT JOIN ingresos_servicios isv ON (isv.mes = m.mes) + LEFT JOIN costos_pedidos cp ON (cp.mes = m.mes) + GROUP BY m.mes + ORDER BY m.mes"; + + try { + + $stmt = $this->db->prepare($sql); + $stmt->bindValue(':anio', $anio, PDO::PARAM_INT); + $stmt->execute(); + + $resultados = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $meses = []; + $ventas = []; + $servicios = []; + $costos = []; + $utilidades = []; + + $totalVentas = 0; + $totalServicios = 0; + $totalCostos = 0; + $totalUtilidad = 0; + + $mejorMes = ''; + $maxUtilidad = null; + + // Calculo de totales + + foreach ($resultados as $row) { + + $meses[] = (int) $row['mes']; + $ventas[] = (float) $row['ventas']; + $servicios[] = (float) $row['servicios']; + $costos[] = (float) $row['costos']; + $utilidades[] = (float) $row['utilidad']; + + $totalVentas += $row['ventas']; + $totalServicios += $row['servicios']; + $totalCostos += $row['costos']; + $totalUtilidad += $row['utilidad']; + + if ($maxUtilidad === null || $row['utilidad'] > $maxUtilidad) { + $maxUtilidad = $row['utilidad']; + $mejorMes = $row['mes']; + } + } + + // Mapeo de nombres de mes + + $nombresMeses = [ + 1=>'Enero',2=>'Febrero',3=>'Marzo',4=>'Abril', + 5=>'Mayo',6=>'Junio',7=>'Julio',8=>'Agosto', + 9=>'Septiembre',10=>'Octubre',11=>'Noviembre',12=>'Diciembre' + ]; + + $mejorMesNombre = $nombresMeses[$mejorMes] ?? ''; + + // Retorno de array completo + + return [ + 'meses' => $meses, + 'ventas' => $ventas, + 'servicios' => $servicios, + 'costos' => $costos, + 'utilidad' => $utilidades, + 'kpis' => [ + 'totalIngresos' => $totalVentas + $totalServicios, + 'totalCostos' => $totalCostos, + 'resultado' => $totalUtilidad, + 'mejorMes' => $mejorMesNombre + ] + ]; + + } catch (PDOException $e) { + + // Log interno + + error_log($e->getMessage()); + + return []; + } + } + + // OBTENER MONTOS A COBRAR + + public function obtenerACobrar($anio) { + + $sql = "WITH meses AS ( + SELECT generate_series(1,12) AS mes + ), + + -- SERVICIOS PENDIENTES + + servicios_pendientes AS ( + SELECT + EXTRACT(MONTH FROM s.fecha_registro)::int AS mes, + SUM(s.total) AS total + FROM servicios s + WHERE s.estado = 'pendiente - enviado' + AND EXTRACT(YEAR FROM s.fecha_registro)::int = :anio + GROUP BY mes + ) + + -- RESULTADO FINAL + + SELECT + m.mes, + COALESCE(sp.total,0) AS servicios + FROM meses m + LEFT JOIN servicios_pendientes sp ON (sp.mes = m.mes) + ORDER BY m.mes"; + + try { + $stmt = $this->db->prepare($sql); + $stmt->bindValue(':anio', $anio, PDO::PARAM_INT); + $stmt->execute(); + + $resultados = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $meses = $totales = []; + + foreach ($resultados as $row) { + $meses[] = (int) $row['mes']; + $totales[] = (float) $row['servicios']; + } + + return [ + 'meses' => $meses, + 'totales' => $totales, + 'kpis' => [ + 'totalACobrar' => array_sum($totales) + ] + ]; + + } catch (PDOException $e) { + + // Log interno + + error_log($e->getMessage()); + + return []; + } + } +} \ No newline at end of file diff --git a/app/models/logInModel.php b/app/models/logInModel.php new file mode 100644 index 0000000..7d32f82 --- /dev/null +++ b/app/models/logInModel.php @@ -0,0 +1,85 @@ +db = Database::getConnection(); // Guardamos el objeto PDO en $db + } + + // BUSCAR USUARIO + + public function buscarPorUsuario($usuario) { + + $sql = "SELECT * FROM usuarios WHERE (usuario = :usuario) AND (usuarios.activo = true) LIMIT 1"; // LIMIT 1, más que nada por seguridad u optimizacón. :usuario es reemplazado luego con el valor real + + $stmt = $this->db->prepare($sql); // Uso de prepare para evitar SQL Injection + + $stmt->bindParam(':usuario', $usuario, PDO::PARAM_STR); // Asocia la variable $usuario con el placeholder :usuario en la consulta. + + $stmt->execute(); // Ejecuta la consulta ya preparada con los parámetros vinculados + + return $stmt->fetch(PDO::FETCH_ASSOC); // Obtiene la primera fila del resultado como un array asociativo + } + + // INVALIDAR POR USUARIO + + public function invalidarPorUsuario(int $id): bool { + + $sql = "UPDATE recuperacion_cuenta + SET usado = 1 + WHERE (id_usuario = :id_usuario)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([':id_usuario' => $id]); + } + + // CREAR RECUPERACIÓN + + public function crear(array $data): bool { + + $sql = "INSERT INTO recuperacion_cuenta (id_usuario, codigo, expira_a, usado) VALUES (:id_usuario, :codigo, :expira_a, 0)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':id_usuario' => $data['id_usuario'], + ':codigo' => $data['codigo'], + ':expira_a' => $data['expira_a'] + ]); + } + + // OBTENER ÚLTIMO CÓDIGO VÁLIDO + + public function obtenerCodigoValido(int $idUsuario): ?array { + + $sql = "SELECT id, codigo, expira_a, usado FROM recuperacion_cuenta + WHERE (id_usuario = :id_usuario) AND (usado = 0) + ORDER BY id DESC LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id_usuario' => $idUsuario]); + + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; + } + + // MARCAR COMO USADO + + public function marcarComoUsado(int $id): bool { + + $sql = "UPDATE recuperacion_cuenta + SET usado = 1 + WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([':id' => $id]); + } +} \ No newline at end of file diff --git a/app/models/pedidoModel.php b/app/models/pedidoModel.php new file mode 100644 index 0000000..5c3ee42 --- /dev/null +++ b/app/models/pedidoModel.php @@ -0,0 +1,254 @@ +db = Database::getConnection(); + } + + // MÉTODO PARA OBTENER ERRORES E INFORMARLOS A TRAVÉZ DEL CONTROLLER + + public function getError(): ?string { + return $this->error; + } + + // GUARDAR PEDIDO (retorna false o el ID del pedido generado) + + public function guardarPedido(array $productosFinales, array $proveedor, array $vendedor, float $total): int|false { + + // Validaciones (ya verifica el controller también) + + if (empty($productosFinales)) { + $this->error = 'El pedido no tiene productos.'; + return false; + } + + if (empty($proveedor['id'])) { + $this->error = 'Proveedor inválido.'; + return false; + } + + if (empty($vendedor['id'])) { + $this->error = 'No se pudo vincular al usuario.'; + return false; + } + + if ($total <= 0) { + $this->error = 'Total de pedido inválido.'; + return false; + } + + foreach ($productosFinales as $p) { + if (empty($p['id_producto']) || empty($p['cantidad']) || empty($p['costo']) || empty($p['precio'])) { + $this->error = 'Producto inválido en el pedido.'; + return false; + } + + if ($p['cantidad'] <= 0 || $p['costo'] < 0) { + $this->error = 'Cantidad o costo inválido.'; + return false; + } + } + + try { + $this->db->beginTransaction(); + + // 1. INSERT PEDIDO + + $sqlPedido = " + INSERT INTO pedidos (total, id_proveedor, id_usuario) + VALUES (:total, :id_proveedor, :id_usuario) + RETURNING id + "; + + $stmtPedido = $this->db->prepare($sqlPedido); + + $stmtPedido->execute([ + ':total' => $total, + ':id_proveedor' => $proveedor['id'], + ':id_usuario' => $vendedor['id'] + ]); + + $idPedido = $stmtPedido->fetchColumn(); + + if (!$idPedido) { + $this->error = 'No se pudo crear el pedido.'; + $this->db->rollBack(); + return false; + } + + // 2. INSERT DETALLE (LOOP) + + $sqlDetalle = " + INSERT INTO pedido_detalle + (precio_unitario, cantidad, subtotal, id_pedido, id_producto) + VALUES + (:precio_unitario, :cantidad, :subtotal, :id_pedido, :id_producto) + "; + + $stmtDetalle = $this->db->prepare($sqlDetalle); + + foreach ($productosFinales as $p) { + + $subtotal = $p['costo'] * $p['cantidad']; + + if (!$stmtDetalle->execute([ + ':precio_unitario' => $p['costo'], + ':cantidad' => $p['cantidad'], + ':subtotal' => $subtotal, + ':id_pedido' => $idPedido, + ':id_producto' => $p['id_producto'], + ]) + ) { + $this->error = 'Error al guardar el detalle del pedido.'; + $this->db->rollBack(); + return false; + } + } + + // 3. ACTUALIZAR DATOS DE PRODUCTO + + $sqlStock = " + UPDATE productos + SET + stock = COALESCE(stock, 0) + :cantidad, + precio = :precio + WHERE id = :id_producto + "; + + $stmtStock = $this->db->prepare($sqlStock); + + foreach ($productosFinales as $p) { + + if (!$stmtStock->execute([ + ':cantidad' => $p['cantidad'], + ':precio' => $p['precio'], + ':id_producto' => $p['id_producto'], + ]) + ) { + $this->error = 'Error al actualizar el producto.'; + $this->db->rollBack(); + return false; + } + } + + // 4. COMMIT + + $this->db->commit(); + return $idPedido; + + } catch (Throwable $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + $this->error = 'Error interno al guardar el pedido.'; + return false; + } + } + + // LISTAR PEDIDOS + + public function listarPedidos(int $anio): ?array { + + $sql = "SELECT p.id, p.fecha, p.total, pe.razon_social, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM pedidos AS p + INNER JOIN proveedores AS pe ON (p.id_proveedor = pe.id) + INNER JOIN usuarios AS u ON (p.id_usuario = u.id) + WHERE EXTRACT(YEAR FROM p.fecha) = :anio + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':anio' => $anio]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // OBTENER DETALLE DE PEDIDO + + public function getDetallePedido(int $idPedido): ?array { + + $sql = "SELECT p.id AS id_producto, p.marca, p.modelo, pd.precio_unitario, pd.cantidad, pd.subtotal FROM pedido_detalle AS pd + INNER JOIN productos AS p ON (pd.id_producto = p.id) + WHERE (pd.id_pedido = :id_pedido) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id_pedido' => $idPedido]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // OBTENER PREVIEW DE CODIGO DE REMITO (no se utiliza más) + + /* + + public function getNumeroRemitoPreview(): int { + + $sql = "SELECT nextval('remito_preview_seq')"; + $stmt = $this->db->prepare($sql); + $stmt->execute(); + + return (int) $stmt->fetchColumn(); + } + + */ + + // OBTENER COSTO DE PRODUCTO + + public function obtenerCosto(int $idProducto): ?float { + + $sql = "SELECT pd.precio_unitario FROM pedido_detalle AS pd + INNER JOIN productos AS p ON (pd.id_producto = p.id) + WHERE (p.id = :idProducto) + AND pd.id_pedido = ( + SELECT MAX(pd2.id_pedido) + FROM pedido_detalle AS pd2 + WHERE pd2.id_producto = :idProducto2 + )"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([ + ':idProducto' => $idProducto, + ':idProducto2' => $idProducto + ]); + + $resultado = $stmt->fetchColumn(); + + return $resultado !== false ? (float) $resultado : null; + } + + // OBTENER COSTOS DE PRODUCTOS EN BASE A UNA CATEGORIA + + public function obtenerCostosPorCategoria(int $idCategoria): array { + + // Uso de LATERAL en LEFT JOIN -> permite traer el costo mas reciente (actúa como un foreach, ejecutando la subconsulta por cada producto) + + $sql = "SELECT + p.id AS id_producto, pd.precio_unitario AS costo FROM productos AS p + + LEFT JOIN LATERAL ( + SELECT pd.precio_unitario FROM pedido_detalle AS pd + INNER JOIN pedidos AS pe ON (pe.id = pd.id_pedido) + WHERE (pd.id_producto = p.id) + ORDER BY pe.fecha DESC, pd.id DESC LIMIT 1 + ) pd ON true + + WHERE (p.id_categoria = :idCategoria)"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':idCategoria' => $idCategoria]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} \ No newline at end of file diff --git a/app/models/presupuestoModel.php b/app/models/presupuestoModel.php new file mode 100644 index 0000000..52d7a7b --- /dev/null +++ b/app/models/presupuestoModel.php @@ -0,0 +1,190 @@ +presupuestoKey])) { + $_SESSION[$this->presupuestoKey] = []; + } + } + + // MÉTODO PARA OBTENER ERRORES E INFORMAR A TRAVEZ DEL CONTROLLER + + public function getError(): ?string { + return $this->error; + } + + // AGREGAR PRODUCTO + + public function agregarProducto(array $producto, int $cantidad): bool { + + if ($cantidad <= 0 || empty($producto['id'])) { + $this->error = 'Datos inválidos del producto.'; + return false; + } + + $id = $producto['id']; + + $cantidadActual = $_SESSION[$this->presupuestoKey][$id]['cantidad'] ?? 0; + $stockDisponible = $producto['stock']; + + if (($cantidadActual + $cantidad) > $stockDisponible) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $descuentoTotal = $producto['descuento_producto'] + $producto['descuento_categoria']; + + $precioFinal = ((100 - $descuentoTotal) * $producto['precio'] / 100); // PRECIO UNITARIO MENOS EL DESCUENTO + + if (isset($_SESSION[$this->presupuestoKey][$id])) { + $_SESSION[$this->presupuestoKey][$id]['cantidad'] = $cantidad; + return true; + } + + $_SESSION[$this->presupuestoKey][$id] = [ + 'id_producto' => $id, + 'marca' => $producto['marca'], + 'modelo' => $producto['modelo'], + 'precio' => $producto['precio'], + 'descuento_total' => $descuentoTotal, + 'cantidad' => $cantidad, + 'stock' => $producto['stock'], + 'precio_final' => $precioFinal + ]; + + return true; + } + + // LISTAR PRODUCTOS + + public function listarProductos(): array { + return $_SESSION[$this->presupuestoKey]; + } + + // ELIMINAR PRODUCTO + + public function eliminarProducto(int $id): bool { + + if (!isset($_SESSION[$this->presupuestoKey][$id])) { + $this->error = 'El producto no existe en la caja.'; + return false; + } else{ + unset($_SESSION[$this->presupuestoKey][$id]); + } + + return true; + } + + // EDITAR CANTIDAD DE PRODUCTO + + public function editarCantidad(int $id, int $nuevaCantidad): bool { + + if (!isset($_SESSION[$this->presupuestoKey][$id])) { + $this->error = 'Producto inexistente en la caja.'; + return false; + } + + if ($nuevaCantidad <= 0) { + $this->error = 'La cantidad debe ser mayor a cero.'; + return false; + } + + if ($nuevaCantidad > $_SESSION[$this->presupuestoKey][$id]['stock']) { + $this->error = 'La cantidad supera el stock disponible.'; + return false; + } + + $_SESSION[$this->presupuestoKey][$id]['cantidad'] = $nuevaCantidad; + return true; + } + + // CALCULAR SUBTOTAL + + public function calcularSubtotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->presupuestoKey] as $item) { + + $precio = (float) $item['precio']; + + $total += $precio * $item['cantidad']; + } + + return round($total, 2); + } + + // CALCULAR PRECIO TOTAL + + public function calcularPrecioTotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->presupuestoKey] as $item) { + + $precio = (float) $item['precio']; + + if (!empty($item['descuento_total'])) { + $precio -= ($item['descuento_total'] * $precio / 100); + } + + $total += $precio * $item['cantidad']; + } + + return round($total, 2); + } + + // CALCULAR DESCUENTO TOTAL + + public function calcularDescuentoTotal(): float { + + $total = 0; + + foreach ($_SESSION[$this->presupuestoKey] as $item) { + + $precio = (float) $item['precio']; + $descuento = 0; + + if (!empty($item['descuento_total'])) { + $descuento += ($item['descuento_total'] * $precio / 100); + } + + $total += $descuento * $item['cantidad']; + } + + return round($total, 2); + } + + // VACIAR CAJA DE PRODUCTOS + + public function vaciarCajaPresupuesto(): bool { + $_SESSION[$this->presupuestoKey] = []; + return true; + } + + // CALCULAR TOTAL PROVISORIO APLICANDO DESCUENTO Y RECARGO EN BASE A TIPO Y FORMA DE PAGO + + public function actualizarDatosProvisorio($descuentoPresupuesto, $recargoPresupuesto, $subtotal, $descuento): ?array { + + // El descuento propio del presupuesto se calcula en base al subtotal con los descuento de productos aplicados + + $descuentoProvisorio = ($descuentoPresupuesto * ($subtotal - $descuento) / 100) + $descuento; // Sumamos los dos descuentos + + // Calculo el recargo teniendo en cuenta el subtotal (descuentos de productos ya aplicados) + + $recargoProvisorio = ($recargoPresupuesto * ($subtotal - $descuento) / 100); + + $totalProvisorio = $subtotal + $recargoProvisorio - $descuentoProvisorio; + + return $_SESSION['prov_presupuesto'] = [ + 'recargo' => $recargoProvisorio, + 'descuento' => $descuentoProvisorio, + 'total' => $totalProvisorio + ]; + } +} \ No newline at end of file diff --git a/app/models/productoModel.php b/app/models/productoModel.php new file mode 100644 index 0000000..0fab7e1 --- /dev/null +++ b/app/models/productoModel.php @@ -0,0 +1,490 @@ +db = Database::getConnection(); + } + + // LISTAR PRODUCTOS + + public function listarProductos(): array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock, (COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) AS descuento_total, c.nombre AS categoria, t.nombre AS tipo + FROM productos AS p + INNER JOIN categorias AS c ON (p.id_categoria = c.id) + INNER JOIN tipos AS t ON (p.id_tipo = t.id) + WHERE (p.estado IS TRUE) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // LISTAR PRODUCTOS INACTIVOS + + public function listarProductosInactivos(): array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, c.nombre AS categoria, t.nombre AS tipo + FROM productos AS p + INNER JOIN categorias AS c ON (p.id_categoria = c.id) + INNER JOIN tipos AS t ON (p.id_tipo = t.id) + WHERE (p.estado IS FALSE) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // LISTAR PRODUCTOS PARA VENTA + + public function listarProductosVenta(): array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, CAST(p.precio - ((COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) * p.precio / 100) AS DECIMAL(10,2)) AS precio_final, p.stock, + (COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) AS descuento_total + FROM productos AS p + INNER JOIN categorias AS c ON (p.id_categoria = c.id) + INNER JOIN tipos AS t ON (p.id_tipo = t.id) + WHERE (p.estado IS TRUE) AND (p.id_tipo = 1) AND (p.precio IS NOT NULL) AND (p.stock <> 0) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // LISTAR PORDUCTOS PARA PEDIDO + + public function listarProductosPedido(): array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.stock FROM productos AS p + INNER JOIN tipos AS t ON (p.id_tipo = t.id) + WHERE (p.estado IS TRUE) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // LISTAR INSUMOS PARA SERVICIO + + public function listarInsumosServicio(): array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock + FROM productos AS p + INNER JOIN categorias AS c ON (p.id_categoria = c.id) + INNER JOIN tipos AS t ON (p.id_tipo = t.id) + WHERE (p.estado IS TRUE) AND (p.id_tipo = 2) AND (p.precio IS NOT NULL) AND (p.stock <> 0) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // CREAR PRODUCTO + + public function crearProducto(array $data): bool { + + $sql = "INSERT INTO productos (marca, modelo, precio, stock, descuento, estado, id_categoria, id_tipo) + VALUES (:marca, :modelo, :precio, :stock, :descuento, :estado, :id_categoria, :id_tipo)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':marca' => $data['marca'], + ':modelo' => $data['modelo'], + ':precio' => $data['precio'], + ':stock' => $data['stock'], + ':descuento' => $data['descuento'], + ':estado' => 1, // Cuando un producto se crea, automáticamente se setea el estado en true, es decir está activo + ':id_categoria' => $data['id_categoria'], + ':id_tipo' => $data['id_tipo'] + ]); + } + + // OBTENER PRODUCTO MEDIANTE ID + + public function obtenerPorId(int $id): ?array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock, p.descuento, p.id_categoria, p.id_tipo, pd.id_pedido AS id_pedido FROM productos AS p + LEFT JOIN pedido_detalle AS pd ON (p.id = pd.id_producto) + WHERE p.id = :id + ORDER BY pd.id_pedido DESC LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + $producto = $stmt->fetch(PDO::FETCH_ASSOC); + + return $producto ?: null; + } + + // OBTENER PRODUCTO POR ID PARA CAJA + + public function obtenerPorIdParaCaja(int $id): ?array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, COALESCE(p.descuento, 0) AS descuento_producto, COALESCE(c.descuento, 0) AS descuento_categoria, p.stock + FROM productos AS p + LEFT JOIN categorias AS c ON (c.id = p.id_categoria) + WHERE (p.id = :id) AND (p.estado IS TRUE) AND (p.id_tipo = 1) LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; + } + + // OBTENER PRODUCTOS DE PEDIDO POR ID PARA CAJA + + public function obtenerPorIdParaCajaPedido(int $id): ?array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.stock, COALESCE(c.porcentaje_ganancia, 0) AS porcentaje_ganancia + FROM productos AS p + LEFT JOIN categorias AS c ON (c.id = p.id_categoria) + WHERE (p.id = :id) AND (p.estado IS TRUE) LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; + } + + // OBTENER INSUMO DE SERVICIO POR ID PARA CAJA + + public function obtenerPorIdParaCajaServicio(int $id): ?array { + + $sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock + FROM productos AS p + LEFT JOIN categorias AS c ON (c.id = p.id_categoria) + WHERE (p.id = :id) AND (p.estado IS TRUE) AND (p.id_tipo = 2) LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; + } + + // EDITAR PRODUCTO + + public function actualizarProducto(int $id, array $data): bool { + + if (empty($data)) { + return false; + } + + $campos = []; + $params = []; + + foreach ($data as $campo => $valor) { + $campos[] = "$campo = :$campo"; + $params[":$campo"] = $valor; + } + + $params[':id'] = $id; + + $sql = "UPDATE productos SET " . implode(', ', $campos) . " WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute($params); + } + + // ACTUALIZAR ESTADO (ACTIVO / INACTIVO) + + public function actualizarEstado(int $idProducto, bool $nuevoEstado): bool { + + try { + $this->db->beginTransaction(); + + // Actualizamaos datos del producto + + $sql1 = "UPDATE productos + SET estado = :estado, + precio = NULL, + stock = NULL, + descuento = NULL + WHERE id = :id"; + + $stmt1 = $this->db->prepare($sql1); + + $stmt1->bindValue(':estado', $nuevoEstado, PDO::PARAM_BOOL); + + $stmt1->bindValue(':id', $idProducto, PDO::PARAM_INT); + + $stmt1->execute(); + + // Actualizamos los stock y cantidades de pedido_detalle + + $sql2 = "UPDATE pedido_detalle + SET cantidad_vendida = cantidad + WHERE id_producto = :id"; + + $stmt2 = $this->db->prepare($sql2); + + $stmt2->bindValue(':id', $idProducto, PDO::PARAM_INT); + + $stmt2->execute(); + + $this->db->commit(); + return true; + + } catch (Exception $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + return false; + } + } + + // LISTAR CATEGORIAS + + public function listarCategorias(): array { + + $sql = "SELECT id, nombre, porcentaje_ganancia, descuento FROM categorias ORDER BY nombre ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // VERIFICAR SI EXISTE UNA CATEGORIA + + public function existeCategoria(int $id): bool { + + $sql = "SELECT 1 FROM categorias WHERE id = :id LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + return (bool) $stmt->fetchColumn(); + } + + // OBTENER CATEGORIA POR ID + + public function obtenerPorIdCategoria(int $id): ?array { + + $sql = "SELECT id, nombre, porcentaje_ganancia, descuento FROM categorias WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + $categoria = $stmt->fetch(PDO::FETCH_ASSOC); + + return $categoria ?: null; + } + + // CREAR CATEGORIA + + public function crearCategoria(array $data): bool { + + $sql = "INSERT INTO categorias (nombre, porcentaje_ganancia, descuento) VALUES (:nombre, :porcentaje_ganancia, :descuento)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':nombre' => $data['nombre'], + ':porcentaje_ganancia' => $data['porcentaje_ganancia'], + ':descuento' => $data['descuento'] + ]); + } + + // EDITAR CATEGORIA + + public function actualizarCategoria(int $id, array $data): bool { + + if (empty($data)) { + return false; + } + + $campos = []; + $params = []; + + foreach ($data as $campo => $valor) { + $campos[] = "$campo = :$campo"; + $params[":$campo"] = $valor; + } + + $params[':id'] = $id; + + $sql = "UPDATE categorias SET " . implode(', ', $campos) . " WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute($params); + } + + // ACTUALIZAR PRECIOS DE PRODUCTOS AL EDITAR LA CATEGORIA + + public function actualizarPreciosPorCategoria(int $idCategoria, array $data, array $costosProductos): bool { + + try { + + $this->db->beginTransaction(); + + // Actualizamos la categoría + + $ok = $this->actualizarCategoria($idCategoria, $data); + + if (!$ok) { + $this->db->rollBack(); + return false; + } + + // Obtenemos el nuevo porcentaje de ganancia + + $stmt = $this->db->prepare(" + SELECT porcentaje_ganancia + FROM categorias + WHERE id = :id + "); + + $stmt->execute([':id' => $idCategoria]); + + $categoria = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$categoria) { + $this->db->rollBack(); + return false; + } + + $porcentaje_ganancia = (float) $categoria['porcentaje_ganancia']; + + $stmt = $this->db->prepare(" + UPDATE productos + SET precio = :precio + WHERE (id = :id) AND (id_categoria = :idCategoria) + "); + + foreach ($costosProductos as $c) { + + // Validaciones defensivas + + if (empty($c['id_producto']) || $c['costo'] === null) { + continue; + } + + $costo = (float) $c['costo']; + + // Fórmula correcta + + $nuevoPrecio = (100 + $porcentaje_ganancia) * $costo / 100; + + $stmt->execute([ + ':precio' => $nuevoPrecio, + ':id' => $c['id_producto'], + ':idCategoria' => $idCategoria + ]); + } + + $this->db->commit(); + return true; + + } catch (Exception $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + return false; + } + } + + // LISTAR TIPOS DE PRODUCTOS + + public function listarTipos(): array { + + $sql = "SELECT id, nombre FROM tipos ORDER BY id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // VERIFICAR SI EXISTE EL TIPO DE PRODUCTO + + public function existeTipo(int $id): bool { + + $sql = "SELECT 1 FROM tipos WHERE id = :id LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + return (bool) $stmt->fetchColumn(); + } + + // OBTENER TIPO POR ID + + public function obtenerPorIdTipo(int $tipo): ?array { + + $sql = "SELECT id, nombre FROM tipos WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + $tipo = $stmt->execute(); + + return $tipo ?: null; + } + + // OBTENER DESCUENTOS DE PRODUCTOS EN BASE A UNA CATEGORIA + + public function obtenerDescuento(int $idCategoria): ?array { + + $sql = "SELECT productos.id, productos.descuento FROM productos + INNER JOIN categorias ON (categorias.id = productos.id_categoria) + WHERE (categorias.id = :idCategoria)"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':idCategoria' => $idCategoria]); + + $resultado = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return !empty($resultado) ? $resultado : null; + } + + // BÚSQUEDA EN INPUT DE DESCRIPCIÓN DEL EQUIPO (MÓDULO SERVICIOS) + + public function busquedaParaServicio(): array { + + $sql = "SELECT marca, modelo FROM productos WHERE (id_tipo = 1)"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} \ No newline at end of file diff --git a/app/models/proveedorModel.php b/app/models/proveedorModel.php new file mode 100644 index 0000000..6f20eae --- /dev/null +++ b/app/models/proveedorModel.php @@ -0,0 +1,126 @@ +db = Database::getConnection(); + } + + // LISTAR PROVEEDORES + + public function listarProveedores(): array { + + $sql = "SELECT id, razon_social, telefono, email from proveedores ORDER BY id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // CREAR PROVEEDOR + + public function crearProveedor(array $data): bool { + + $sql = "INSERT INTO proveedores (razon_social, telefono, email, cuit, cond_iva) VALUES (:razon_social, :telefono, :email, :cuit, :cond_iva)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':razon_social' => $data['razon_social'], + ':telefono' => $data['telefono'], + ':email' => $data['email'], + ':cuit' => $data['cuit'], + ':cond_iva' => $data['cond_iva'] + ]); + } + + // BUSCAR PROVEEDOR POR ID + + public function obtenerPorId(int $id): ?array { + + $sql = "SELECT id, razon_social, telefono, email, cuit, cond_iva FROM proveedores WHERE id = :id LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); + + $proveedor = $stmt->fetch(PDO::FETCH_ASSOC); + + return $proveedor ?: null; + } + + // EDITAR PROVEEDOR + + public function actualizarProveedor(int $id, array $data): bool { + + if (empty($data)) { + return false; + } + + $campos = []; + $params = []; + + foreach ($data as $campo => $valor) { + $campos[] = "$campo = :$campo"; + $params[":$campo"] = $valor; + } + + $params[':id'] = $id; + + $sql = "UPDATE proveedores SET " . implode(', ', $campos) . " WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute($params); + } + + // OBTENER ID DE PROVEEDOR CORRESPONDIENTE A UN ID DE PEDIDO + + public function obtenerIDProveedorPorPedido(int $idPedido): ?array { + + $sql = "SELECT p.id, p.razon_social, p.telefono, p.email FROM proveedores AS p + INNER JOIN pedidos AS pe ON (p.id = pe.id_proveedor) + WHERE (pe.id = :id_pedido) LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id_pedido' => $idPedido]); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + // VERIFICAR EXISTENCIA DE PROVEEDOR + + public function verificarExistencia(array $datosProveedor, ?int $idExcluir = null): bool { + + $sql = "SELECT 1 FROM proveedores WHERE (razon_social = :razon_social OR email = :email OR cuit = :cuit)"; + + if ($idExcluir !== null) { + $sql .= " AND id <> :id_excluir"; + } + + $sql .= " LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $params = [ + ':razon_social' => $datosProveedor['razon_social'], + ':email' => $datosProveedor['email'], + ':cuit' => $datosProveedor['cuit'] + ]; + + if ($idExcluir !== null) { + $params[':id_excluir'] = $idExcluir; + } + + $stmt->execute($params); + + return (bool) $stmt->fetchColumn(); + } +} \ No newline at end of file diff --git a/app/models/servicioModel.php b/app/models/servicioModel.php new file mode 100644 index 0000000..31fb282 --- /dev/null +++ b/app/models/servicioModel.php @@ -0,0 +1,447 @@ +db = Database::getConnection(); + } + + // MÉTODO PARA OBTENER ERRORES Y MANEJARLOS EN EL CONTROLLER + + public function getError(): ?string { + return $this->error; + } + + // GUARDADO INCIAL DEL SERVICIO + + public function guardarServicio(int $numero, array $datosServicio, array $cliente, array $vendedor): bool { + + if (!isset($numero)) { + $this->error = 'No se pudo obtener el código de servicio.'; + return false; + } + + if (empty($datosServicio)) { + $this->error = 'El servicio no posee datos.'; + return false; + } + + $sql = "INSERT INTO servicios + (id, equipo, contrasenia_equipo, problema, observacion, cuenta, contrasenia_cuenta, id_cliente, id_usuario) + VALUES + (:id, :equipo, :contrasenia_equipo, :problema, :observacion, :cuenta, :contrasenia_cuenta, :id_cliente, :id_usuario)"; + + $stmt = $this->db->prepare($sql); + + return (bool) $stmt->execute([ + ':id' => $numero, + ':equipo' => $datosServicio['descripcion_equipo'], + ':contrasenia_equipo' => $datosServicio['contrasenia_equipo'] ?? null, + ':problema' => $datosServicio['descripcion_problema'], + ':observacion' => ($datosServicio['observacion'] ?? '') !== '' ? $datosServicio['observacion'] : null, + ':cuenta' => ($datosServicio['cuenta'] ?? '') !== '' ? $datosServicio['cuenta'] : null, + ':contrasenia_cuenta' => $datosServicio['contrasenia_cuenta'] ?? null, + ':id_cliente' => $cliente['id'], + ':id_usuario' => $vendedor['id'] + ]); + } + + // OBTENER PREVIEW DE CODIGO DE SERVICIO + + public function getNumeroServicioPreview(): int { + + $sql = "SELECT nextval('servicio_preview_seq')"; + $stmt = $this->db->prepare($sql); + $stmt->execute(); + + return (int) $stmt->fetchColumn(); + } + + // LISTAR SERVICIOS + + public function listarServicios(int $anio): ?array { + + $sql = "SELECT s.id, s.fecha_registro, s.fecha_terminado, s.equipo, s.estado, c.nombre AS cliente_nombre, c.apellido AS cliente_apellido, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM servicios AS s + INNER JOIN clientes AS c ON (s.id_cliente = c.id) + INNER JOIN usuarios AS u ON (s.id_usuario = u.id) + WHERE EXTRACT(YEAR FROM s.fecha_registro) = :anio + ORDER BY s.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':anio' => $anio]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // OBTENER DETALLE DEL SERVICIO + + public function getDetalleServicio(int $idServicio): ?array { + + $sql = "SELECT id, equipo, contrasenia_equipo, problema, observacion, cuenta, contrasenia_cuenta, detalle_realizado, mano_obra, total, estado FROM servicios + WHERE (id = :id)"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $idServicio]); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + // OBTENER DETALLE DE INSUMOS UTILIZADOS EN SERVICIO + + public function getDetalleInsumos(int $idServicio): ?array { + + $sql = "SELECT p.id AS id_insumo, p.marca, p.modelo, p.stock, sd.precio_unitario, sd.cantidad, sd.subtotal, sd.id_pedido FROM servicio_detalle AS sd + INNER JOIN productos AS p ON (sd.id_producto = p.id) + WHERE (sd.id_servicio = :id_servicio) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id_servicio' => $idServicio]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // ACTUALIZAR SERVICIO + + public function actualizarServicio(array $datosServicio, array $insumos): bool { + + // Validaciones (ya verifica el controller también) + + if (empty($datosServicio) || $datosServicio['id_servicio'] <= 0) { + $this->error = 'Datos de servicio inválidos.'; + return false; + } + + if (!isset($datosServicio['mano_obra']) || $datosServicio['mano_obra'] < 0) { + $this->error = 'Mano de obra inválida.'; + return false; + } + + if ($datosServicio['total_final'] < 0) { + $this->error = 'Total inválido.'; + return false; + } + + foreach ($insumos as $i) { + if (!empty($insumos) && (empty($i['id_producto']) || empty($i['precio']) || empty($i['cantidad']))) { + $this->error = 'Producto inválido en el pedido.'; + return false; + } + + if (!empty($insumos) && ($i['cantidad'] <= 0 || $i['precio'] < 0)) { + $this->error = 'Cantidad o precio inválido.'; + return false; + } + + if (!empty($insumos) && $i['stock'] < 0) { + $this->error = 'Ocurrió un error en el stock.'; + return false; + } + } + + try { + $this->db->beginTransaction(); + + // 1. ACTUALIZAR EL SERVICIO + + $sqlServicio = "UPDATE servicios + SET detalle_realizado = :detalle_realizado, + mano_obra = :mano_obra, + total = :total, + estado = 'pendiente - enviado' -- SE ACTUALIZA EL ESTADO DEL SERVICIO A (PENDIENTE - ENVIADO) + WHERE id = :id + "; + + $stmtServicio = $this->db->prepare($sqlServicio); + + $stmtServicio->execute([ + ':detalle_realizado' => $datosServicio['detalle_realizado'], + ':mano_obra' => $datosServicio['mano_obra'], + ':total' => $datosServicio['total_final'], + ':id' => $datosServicio['id_servicio'], + ]); + + // 2. PREPARAR QUERIES AUXILIARES + + // Pedidos disponibles (FIFO) + + $sqlPedidos = "SELECT id_pedido, (cantidad - cantidad_vendida) AS disponible FROM pedido_detalle + WHERE id_producto = :id_producto + AND (cantidad - cantidad_vendida) > 0 + ORDER BY id_pedido ASC + "; + + $stmtPedidos = $this->db->prepare($sqlPedidos); + + // Insert Detalle Servicio + + $sqlDetalle = "INSERT INTO servicio_detalle (precio_unitario, cantidad, subtotal, id_servicio, id_producto, id_pedido) VALUES + (:precio_unitario, :cantidad, :subtotal, :id_servicio, :id_producto, :id_pedido) + "; + + $stmtDetalle = $this->db->prepare($sqlDetalle); + + // Update Pedido Detalle + + $sqlUpdatePedido = "UPDATE pedido_detalle + SET cantidad_vendida = cantidad_vendida + :cantidad + WHERE id_pedido = :id_pedido + AND id_producto = :id_producto + "; + + $stmtUpdatePedido = $this->db->prepare($sqlUpdatePedido); + + // Update stock del producto + + $sqlStock = "UPDATE productos + SET stock = stock - :cantidad + WHERE id = :id_producto + "; + + $stmtStock = $this->db->prepare($sqlStock); + + // 3. PROCESAR DETALLE + CONSUMO STOCK (FIFO) + + if (!empty($insumos)) { + + foreach ($insumos as $p) { + + $cantidadRestante = $p['cantidad']; + + $stmtPedidos->execute([ + ':id_producto' => $p['id_producto'] + ]); + + $pedidos = $stmtPedidos->fetchAll(PDO::FETCH_ASSOC); + + if (empty($pedidos)) { + $this->db->rollBack(); + $this->error = "No hay stock disponible para el insumo ID {$p['id_producto']}"; + return false; + } + + foreach ($pedidos as $pedido) { + + if ($cantidadRestante <= 0) { + break; + } + + $aVender = min($cantidadRestante, $pedido['disponible']); + $subtotal = $p['precio'] * $aVender; + + // Insert venta_detalle + + $stmtDetalle->execute([ + ':precio_unitario' => $p['precio'], + ':cantidad' => $aVender, + ':subtotal' => $subtotal, + ':id_servicio' => $datosServicio['id_servicio'], + ':id_producto' => $p['id_producto'], + ':id_pedido' => $pedido['id_pedido'], + ]); + + // Update pedido_detalle + + $stmtUpdatePedido->execute([ + ':cantidad' => $aVender, + ':id_pedido' => $pedido['id_pedido'], + ':id_producto' => $p['id_producto'], + ]); + + // Update stock producto + + $stmtStock->execute([ + ':cantidad' => $aVender, + ':id_producto' => $p['id_producto'], + ]); + + $cantidadRestante -= $aVender; + } + + // VALIDACIÓN IMPORTANTE DE STOCK + + if ($cantidadRestante > 0) { + $this->db->rollBack(); + $this->error = "Stock insuficiente para el insumo ID {$p['id_producto']}"; + return false; + } + } + } + + // 4. COMMIT + + $this->db->commit(); + return true; + + } catch (Throwable $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + $this->error = 'Error interno al actualizar el servicio.'; + return false; + } + } + + // FINALIZAR SERVICIO + + public function finalizarServicio(int $idServicio): bool { + + if ($idServicio <= 0) { + $this->error = 'Servicio inválido.'; + return false; + } + + try { + + $stmt = $this->db->prepare( + "UPDATE servicios + SET estado = 'finalizado', + fecha_terminado = CURRENT_DATE + WHERE id = :id" + ); + + return $stmt->execute([ + ':id' => $idServicio + ]); + + } catch (Throwable $e) { + $this->error = 'Error interno al finalizar el servicio.'; + return false; + } + } + + // CANCELAR SERVICIO + + public function cancelarServicio(int $idServicio, float $cargoCancelacion): bool { + + if ($idServicio <= 0) { + $this->error = 'Servicio inválido.'; + return false; + } + + try { + + $this->db->beginTransaction(); + + // 1. OBTENER INSUMOS DEL SERVICIO + + $sqlDetalle = "SELECT id_producto, cantidad, id_pedido FROM servicio_detalle WHERE id_servicio = :id"; + + $stmtDetalle = $this->db->prepare($sqlDetalle); + + $stmtDetalle->execute([ + ':id' => $idServicio + ]); + + $insumos = $stmtDetalle->fetchAll(PDO::FETCH_ASSOC); + + // 2. PREPARAR QUERIES + + $sqlStock = "UPDATE productos + SET stock = stock + :cantidad + WHERE id = :id_producto"; + + $stmtStock = $this->db->prepare($sqlStock); + + $sqlPedido = "UPDATE pedido_detalle + SET cantidad_vendida = cantidad_vendida - :cantidad + WHERE id_pedido = :id_pedido + AND id_producto = :id_producto"; + + $stmtPedido = $this->db->prepare($sqlPedido); + + // 3. REVERTIR INVENTARIO + + if (!empty($insumo)) { + + foreach ($insumos as $i) { + + // devolver stock + + $stmtStock->execute([ + ':cantidad' => $i['cantidad'], + ':id_producto' => $i['id_producto'] + ]); + + // revertir FIFO + + $stmtPedido->execute([ + ':cantidad' => $i['cantidad'], + ':id_pedido' => $i['id_pedido'], + ':id_producto' => $i['id_producto'] + ]); + } + } + + // 4. ACTUALIZAR ESTADO DEL SERVICIO + + $stmt = $this->db->prepare( + "UPDATE servicios + SET estado = 'cancelado', + total = :total, + fecha_terminado = CURRENT_DATE + WHERE id = :id" + ); + + $stmt->execute([ + ':id' => $idServicio, + ':total' => $cargoCancelacion + ]); + + $this->db->commit(); + return true; + + } catch (Throwable $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + $this->error = 'Error al cancelar el servicio.'; + return false; + } + } + + // OBTENER CLIENTE DEL SERVICIO + + public function getClienteServicio(int $idServicio): array { + + $sql = "SELECT c.nombre, c.apellido, c.email FROM clientes AS c + INNER JOIN servicios AS s ON (s.id_cliente = c.id) + WHERE (s.id = :id)"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([ + ':id' => $idServicio, + ]); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + // OBTENER ESTADO DE SERVICIO + + public function getEstadoServicio(int $idServicio): bool { + + $sql = "SELECT estado FROM servicios + WHERE (servicios.id = :id) AND (servicios.estado = 'pendiente') LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $idServicio]); + + return $stmt->fetch() !== false; + } +} \ No newline at end of file diff --git a/app/models/usuarioModel.php b/app/models/usuarioModel.php new file mode 100644 index 0000000..7ce01ad --- /dev/null +++ b/app/models/usuarioModel.php @@ -0,0 +1,158 @@ +db = Database::getConnection(); + } + + // ACTUALIZAR USUARIO + + public function actualizarUsuario(int $id, array $data): bool { + + if (empty($data)) { + return false; + } + + $campos = []; + $params = []; // Se utiliza para evitar inyección SQL usando parámetros nombrados de PDO + + foreach ($data as $campo => $valor) { + $campos[] = "$campo = :$campo"; // Arma el string relacionando $campo con los parámetros nombrados (se usa en la consulta) + $params[":$campo"] = $valor; // Arma array relacionando el parámetro nombrado con el valor (se usa en la consulta) + } + + $params[':id'] = $id; + + $sql = "UPDATE usuarios SET " . implode(', ', $campos) . " WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute($params); // Relaciona los nombrados con los valores + } + + // OBTENER USUARIO POR ID + + public function obtenerPorId(int $id): ?array { // El ? lo uso para que pueda devolver NULL + + $sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE id = :id LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id' => $id]); // Le asigna al parámetro nombrado el valor de $id + + $user = $stmt->fetch(PDO::FETCH_ASSOC); // Devuelve un array asociativo + + return $user ?: null; // Verifica si el array no esta vacío y lo devuelve, sino devuelve null + } + + // LISTAR USUARIOS + + public function listarUsuarios(): array { + + $sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE (activo = true) ORDER BY id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // CREAR USUARIO + + public function crearUsuario(array $data): bool { + + $sql = "INSERT INTO usuarios (nombre, apellido, email, telefono, usuario, password, rol) VALUES (:nombre, :apellido, :email, :telefono, :usuario, :password, :rol)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':nombre' => $data['nombre'], + ':apellido' => $data['apellido'], + ':email' => $data['email'], + ':telefono' => $data['telefono'], + ':usuario' => $data['usuario'], + ':password' => $data['password'], + ':rol' => $data['rol'] + ]); + } + + // VERIFICAR EXISTENCIA DE USUARIO + + public function verificarExistencia(array $datosUsuario, ?int $idExcluir = null): bool { + + $sql = "SELECT 1 FROM usuarios WHERE ((nombre = :nombre AND apellido = :apellido AND email = :email) OR (usuario = :usuario) OR (email = :email))"; + + if ($idExcluir !== null) { + $sql .= " AND id <> :id_excluir"; + } + + $sql .= " LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $params = [ + ':nombre' => $datosUsuario['nombre'], + ':apellido' => $datosUsuario['apellido'], + ':usuario' => $datosUsuario['usuario'], + ':email' => $datosUsuario['email'] + ]; + + if ($idExcluir !== null) { + $params[':id_excluir'] = $idExcluir; + } + + $stmt->execute($params); + + return (bool) $stmt->fetchColumn(); + } + + // ELIMINAR USUARIO (BAJA LÓGICA) + + public function eliminarUsuario(int $id): bool { + + $sql = "UPDATE usuarios + SET activo = false + WHERE (usuarios.id = :id)"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([':id' => $id]); + } + + // OBTENER USUARIO POR MAIL + + public function obtenerPorEmail ($email): ?array { + + $sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE email = :email LIMIT 1"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':email' => $email]); + + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + return $user ?: null; + } + + // RESTABLECER CONTRASENIA + + public function actualizarContrasenia(int $id, string $password): bool { + + $sql = "UPDATE usuarios + SET password = :password + WHERE id = :id"; + + $stmt = $this->db->prepare($sql); + + return $stmt->execute([ + ':password' => $password, + ':id' => $id + ]); + } +} \ No newline at end of file diff --git a/app/models/ventaModel.php b/app/models/ventaModel.php new file mode 100644 index 0000000..14712d5 --- /dev/null +++ b/app/models/ventaModel.php @@ -0,0 +1,294 @@ +db = Database::getConnection(); + } + + // MÉTODO PARA OBTENER ERRORES Y MANEJARLOS A TRAVÉZ DEL CONTROLLER + + public function getError(): ?string { + return $this->error; + } + + // CALCULAR TOTAL PROVISORIO APLICANDO DESCUENTO Y RECARGO EN BASE A TIPO Y FORMA DE PAGO + + public function actualizarDatosProvisorio($descuentoPresupuesto, $recargoPresupuesto, $subtotal, $descuento): ?array { + + // El descuento propio de la venta se calcula en base al subtotal con los descuento de productos aplicados + + $descuentoProvisorio = ($descuentoPresupuesto * ($subtotal - $descuento) / 100) + $descuento; // Sumamos los dos descuentos + + // Calculo el recargo teniendo en cuenta el subtotal (descuentos de productos ya aplicados) + + $recargoProvisorio = ($recargoPresupuesto * ($subtotal - $descuento) / 100); + + $totalProvisorio = $subtotal + $recargoProvisorio - $descuentoProvisorio; + + return $_SESSION['prov_venta'] = [ + 'recargo' => $recargoProvisorio, + 'descuento' => $descuentoProvisorio, + 'total' => $totalProvisorio + ]; + } + + // FUNCION DE GUARDADO DE VENTA + + public function guardarVenta(array $productosFinales, array $vendedor, array $cliente, array $datosExtraVenta, array $datosProvisorios): int|false { + + // Validaciones (ya verifica el controller también) + + if (empty($productosFinales)) { + $this->error = 'El pedido no tiene productos.'; + return false; + } + + if (empty($vendedor['id'])) { + $this->error = 'No se pudo vincular al usuario.'; + return false; + } + + if (empty($cliente['id'])) { + $this->error = 'Cliente inválido.'; + return false; + } + + if ($datosExtraVenta['descuento'] < 0 || $datosExtraVenta['descuento'] > 100) { + $this->error = 'El descuento debe estar entre 0 y 100%'; + return false; + } + + if ($datosExtraVenta['recargo'] < 0 || $datosExtraVenta['recargo'] > 100) { + $this->error = 'El recargo debe estar entre 0 y 100%'; + return false; + } + + if ($datosProvisorios['total'] <= 0) { + $this->error = 'Total de pedido inválido.'; + return false; + } + + foreach ($productosFinales as $p) { + if (empty($p['id_producto']) || empty($p['precio']) || empty($p['cantidad']) || empty($p['precio_final'])) { + $this->error = 'Producto inválido en el pedido.'; + return false; + } + + if ($p['cantidad'] <= 0 || $p['precio'] < 0 || $p['precio_final'] < 0) { + $this->error = 'Cantidad o precio inválido.'; + return false; + } + + if ($p['stock'] < 0) { + $this->error = 'Ocurrió un error en el stock.'; + return false; + } + } + + try { + $this->db->beginTransaction(); + + // 1. INSERT VENTA + + $sqlVenta = "INSERT INTO ventas (total, forma_pago, tipo_pago, descuento_pct, recargo_pct, id_cliente, id_usuario) + VALUES (:total, :forma_pago, :tipo_pago, :descuento_pct, :recargo_pct, :id_cliente, :id_usuario) + RETURNING id + "; + + $stmtVenta = $this->db->prepare($sqlVenta); + + $stmtVenta->execute([ + ':total' => $datosProvisorios['total'], + ':forma_pago' => $datosExtraVenta['forma_pago'], + ':tipo_pago' => $datosExtraVenta['tipo_pago'], + ':descuento_pct' => $datosExtraVenta['descuento'], + ':recargo_pct' => $datosExtraVenta['recargo'], + ':id_cliente' => $cliente['id'], + ':id_usuario' => $vendedor['id'] + ]); + + $idVenta = $stmtVenta->fetchColumn(); + + if (!$idVenta) { + $this->error = 'No se pudo crear la venta.'; + $this->db->rollBack(); + return false; + } + + // 2. PREPARAR QUERIES AUXILIARES + + // Pedidos disponibles (FIFO) + + $sqlPedidos = "SELECT id_pedido, (cantidad - cantidad_vendida) AS disponible FROM pedido_detalle + WHERE id_producto = :id_producto + AND (cantidad - cantidad_vendida) > 0 + ORDER BY id_pedido ASC + "; + + $stmtPedidos = $this->db->prepare($sqlPedidos); + + // Insert Detalle Venta + + $sqlDetalle = "INSERT INTO venta_detalle (precio_unitario, cantidad, subtotal, descuento_pct, id_venta, id_producto, id_pedido) VALUES + (:precio_unitario, :cantidad, :subtotal, :descuento_pct, :id_venta, :id_producto, :id_pedido) + "; + + $stmtDetalle = $this->db->prepare($sqlDetalle); + + // Update Pedido Detalle + + $sqlUpdatePedido = "UPDATE pedido_detalle + SET cantidad_vendida = cantidad_vendida + :cantidad + WHERE id_pedido = :id_pedido + AND id_producto = :id_producto + "; + + $stmtUpdatePedido = $this->db->prepare($sqlUpdatePedido); + + // Update stock del producto + + $sqlStock = "UPDATE productos + SET stock = stock - :cantidad + WHERE id = :id_producto + "; + + $stmtStock = $this->db->prepare($sqlStock); + + // 3. PROCESAR DETALLE + CONSUMO STOCK (FIFO) + + foreach ($productosFinales as $p) { + + $cantidadRestante = $p['cantidad']; + + $stmtPedidos->execute([ + ':id_producto' => $p['id_producto'] + ]); + + $pedidos = $stmtPedidos->fetchAll(PDO::FETCH_ASSOC); + + if (empty($pedidos)) { + $this->db->rollBack(); + $this->error = "No hay stock disponible para el producto ID {$p['id_producto']}"; + return false; + } + + foreach ($pedidos as $pedido) { + + if ($cantidadRestante <= 0) { + break; + } + + $aVender = min($cantidadRestante, $pedido['disponible']); + $subtotal = $p['precio_final'] * $aVender; + + // Insert venta_detalle + + $stmtDetalle->execute([ + ':precio_unitario' => $p['precio'], + ':cantidad' => $aVender, + ':subtotal' => $subtotal, + ':descuento_pct' => $p['descuento_total'], + ':id_venta' => $idVenta, + ':id_producto' => $p['id_producto'], + ':id_pedido' => $pedido['id_pedido'], + ]); + + // Update pedido_detalle + + $stmtUpdatePedido->execute([ + ':cantidad' => $aVender, + ':id_pedido' => $pedido['id_pedido'], + ':id_producto' => $p['id_producto'], + ]); + + // Update stock producto + + $stmtStock->execute([ + ':cantidad' => $aVender, + ':id_producto' => $p['id_producto'], + ]); + + $cantidadRestante -= $aVender; + } + + // VALIDACIÓN IMPORTANTE DE STOCK + + if ($cantidadRestante > 0) { + $this->error = "Stock insuficiente para el producto ID {$p['id_producto']}"; + return false; + } + } + + // 4. COMMIT + + $this->db->commit(); + return $idVenta; + + } catch (Throwable $e) { + $this->db->rollBack(); + + // Log interno + + error_log($e->getMessage()); + + $this->error = 'Error interno al guardar la venta.'; + return false; + } + } + + // OBTENER PREVIEW DE CODIGO DE VENTA (no implementado) + + /* + + public function getNumeroVentaPreview(): int { + + $sql = "SELECT nextval('venta_preview_seq')"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(); + + return (int) $stmt->fetchColumn(); + } + + */ + + // LISTAR VENTAS + + public function listarVentas(int $anio): ?array { + + $sql = "SELECT v.id, v.fecha, v.total, v.forma_pago, v.tipo_pago, v.descuento_pct, v.recargo_pct, c.nombre AS cliente_nombre, c.apellido AS cliente_apellido, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM ventas AS v + INNER JOIN clientes AS c ON (v.id_cliente = c.id) + INNER JOIN usuarios AS u ON (v.id_usuario = u.id) + WHERE EXTRACT(YEAR FROM v.fecha) = :anio + ORDER BY v.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute(['anio' => $anio]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // OBTENER DETALLE DE VENTA + + public function getDetalleVenta(int $idVenta): ?array { + + $sql = "SELECT p.id AS id_producto, p.marca, p.modelo, vd.precio_unitario, vd.cantidad, vd.subtotal, vd.descuento_pct, vd.id_pedido FROM venta_detalle AS vd + INNER JOIN productos AS p ON (vd.id_producto = p.id) + WHERE (vd.id_venta = :id_venta) + ORDER BY p.id ASC"; + + $stmt = $this->db->prepare($sql); + + $stmt->execute([':id_venta' => $idVenta]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} \ No newline at end of file