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; } }