Tul xxx Tul
User / IP
:
216.73.217.33
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
siscaps
/
controllers
/
Viewing: ContratosController.php
<?php class ContratosController { private function ensureCsrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } public function create(): void { $csrf = $this->ensureCsrf(); $customer_id = (int)($_GET['customer_id'] ?? 0); if ($customer_id <= 0) { http_response_code(400); echo 'ID de cliente inválido'; return; } $pdo = (new Database())->getConnection(); // Asegurar la tabla de contratos (previene errores en instalaciones nuevas) try { Contract::ensureTable($pdo); } catch (Throwable $e) { /* noop */ } $stmt = $pdo->prepare('SELECT * FROM customers WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $customer_id]); $customer = $stmt->fetch(PDO::FETCH_ASSOC); if (!$customer) { http_response_code(404); echo 'Cliente no encontrado'; return; } // Cargar medidor principal (primero asociado) $stmtM = $pdo->prepare('SELECT * FROM meters WHERE customer_id = :cid ORDER BY id ASC LIMIT 1'); $stmtM->execute([':cid' => $customer_id]); $meter = $stmtM->fetch(PDO::FETCH_ASSOC) ?: null; // Último contrato (si existe) $lastContract = Contract::findLatestByCustomer($customer_id); // Valores previos del formulario (si falló validación) $oldFields = $_SESSION['old_contract_fields'] ?? []; $oldContractDate = $_SESSION['old_contract_date'] ?? ''; unset($_SESSION['old_contract_fields'], $_SESSION['old_contract_date']); require __DIR__ . '/../views/contratos/create.php'; } public function store(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('clientes.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $customer_id = (int)($_POST['customer_id'] ?? 0); if ($customer_id <= 0) { redirect('clientes.index'); } $pdo = (new Database())->getConnection(); $stmt = $pdo->prepare('SELECT * FROM customers WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $customer_id]); $customer = $stmt->fetch(PDO::FETCH_ASSOC); if (!$customer) { $_SESSION['flash_error'] = 'Cliente no encontrado.'; redirect('clientes.index'); } // Crear carpetas si no existen $contractsDir = __DIR__ . '/../public/contracts'; $signDir = $contractsDir . '/signatures'; if (!is_dir($contractsDir)) { @mkdir($contractsDir, 0777, true); } if (!is_dir($signDir)) { @mkdir($signDir, 0777, true); } // Guardar firma si fue enviada $signaturePath = null; $sigData = $_POST['signature_data'] ?? ''; if ($sigData) { if (strpos($sigData, 'data:image/png;base64,') === 0) { $sigData = substr($sigData, strlen('data:image/png;base64,')); } $img = base64_decode(str_replace(' ', '+', $sigData)); if ($img !== false) { $sigName = 'sig_' . $customer_id . '_' . time() . '.png'; $sigFull = $signDir . '/' . $sigName; file_put_contents($sigFull, $img); $signaturePath = 'contracts/signatures/' . $sigName; // relative to public } } // Campos libres del contrato $fields = $_POST['fields'] ?? []; $contractDate = $_POST['contract_date'] ?? date('Y-m-d'); // Validaciones: todos obligatorios excepto domicilio_extra, service_address y observations $errors = []; $labels = [ 'sector' => 'Sector o Barrio', 'community' => 'Comunidad', 'municipality' => 'Municipio', 'domicilio' => 'Domicilio', 'regimen' => 'Régimen de la propiedad', 'installments' => 'Nº de cuotas', 'monthly_fee' => 'Monto mensual', 'connection_fee' => 'Cuota de conexión', ]; $requiredFields = array_keys($labels); foreach ($requiredFields as $k) { $val = trim((string)($fields[$k] ?? '')); if ($val === '') { $errors[] = 'El campo "' . ($labels[$k] ?? $k) . '" es obligatorio.'; } } if (empty($contractDate)) { $errors[] = 'La fecha de contrato es obligatoria.'; } // Numéricos if (($fields['installments'] ?? '') !== '' && !preg_match('/^\d+$/', (string)$fields['installments'])) { $errors[] = 'El Nº de cuotas debe ser un número entero.'; } if (($fields['monthly_fee'] ?? '') !== '' && !is_numeric($fields['monthly_fee'])) { $errors[] = 'El Monto mensual debe ser numérico.'; } if (($fields['connection_fee'] ?? '') !== '' && !is_numeric($fields['connection_fee'])) { $errors[] = 'La Cuota de conexión debe ser numérica.'; } if (!empty($errors)) { // Persistir valores del formulario $_SESSION['old_contract_fields'] = $fields; $_SESSION['old_contract_date'] = $contractDate; $_SESSION['flash_error'] = implode("\n", $errors); header('Location: ' . BASE_URL . '?route=contratos.create&customer_id=' . $customer_id); exit; } // Insertar con número único de forma atómica usando un contador diario $pdfPath = null; $docxPath = null; $contractNumber = ''; $id = 0; // Asegurar tabla de contadores try { $pdo->exec("CREATE TABLE IF NOT EXISTS contract_counters (\n day CHAR(8) NOT NULL PRIMARY KEY,\n seq INT NOT NULL DEFAULT 0\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); } catch (Throwable $e) { /* noop */ } $ymd = date('Ymd'); $prefix = 'CONT-' . $ymd . '-'; $maxAttempts = 10; for ($i = 0; $i < $maxAttempts; $i++) { try { $pdo->beginTransaction(); // Sembrar fila del día si no existe con el máximo sufijo actual $stmt = $pdo->prepare('SELECT seq FROM contract_counters WHERE day = :d FOR UPDATE'); $stmt->execute([':d' => $ymd]); $seq = $stmt->fetchColumn(); if ($seq === false) { $stmtM = $pdo->prepare("SELECT IFNULL(MAX(CAST(RIGHT(contract_number,5) AS UNSIGNED)),0) FROM contracts WHERE contract_number LIKE :pref"); $stmtM->execute([':pref' => $prefix . '%']); $maxSeq = (int)$stmtM->fetchColumn(); $stmtI = $pdo->prepare('INSERT INTO contract_counters(day, seq) VALUES (:d, :s)'); $stmtI->execute([':d' => $ymd, ':s' => $maxSeq]); $seq = $maxSeq; } else { $seq = (int)$seq; } // Incrementar y obtener el nuevo valor $stmtU = $pdo->prepare('UPDATE contract_counters SET seq = seq + 1 WHERE day = :d'); $stmtU->execute([':d' => $ymd]); $stmtG = $pdo->prepare('SELECT seq FROM contract_counters WHERE day = :d'); $stmtG->execute([':d' => $ymd]); $newSeq = (int)$stmtG->fetchColumn(); if ($newSeq <= 0) { throw new RuntimeException('Counter failed'); } $candidate = $prefix . str_pad((string)$newSeq, 5, '0', STR_PAD_LEFT); // Insertar contrato $stmtC = $pdo->prepare("INSERT INTO contracts (customer_id, contract_number, contract_date, fields_json, signature_path, pdf_path)\n VALUES (:cid, :num, :cd, :fj, :sig, :pdf)"); $stmtC->execute([ ':cid' => $customer_id, ':num' => $candidate, ':cd' => !empty($contractDate) ? $contractDate : null, ':fj' => json_encode($fields, JSON_UNESCAPED_UNICODE), ':sig' => $signaturePath ?: null, ':pdf' => null, ]); $id = (int)$pdo->lastInsertId(); $pdo->commit(); $contractNumber = $candidate; break; } catch (PDOException $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } $driver = (int)($e->errorInfo[1] ?? 0); $sqlState = (string)($e->errorInfo[0] ?? $e->getCode()); $msg = (string)$e->getMessage(); // 1062 duplicate, 1213 deadlock, 1205 lock wait timeout if (in_array($driver, [1062,1213,1205], true) || stripos($sqlState, '23000') !== false || stripos($msg, 'deadlock') !== false) { usleep(180000 + ($i * 10000)); continue; } throw $e; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $e; } } // Fallback: si no obtuvimos lastInsertId, buscar por número único if ($id <= 0 && $contractNumber !== '') { try { $stmtFind = $pdo->prepare('SELECT id FROM contracts WHERE contract_number = :num LIMIT 1'); $stmtFind->execute([':num' => $contractNumber]); $id = (int)$stmtFind->fetchColumn(); } catch (Throwable $e) { /* noop */ } } // Segundo fallback: calcular desde el máximo sufijo del día y reintentar insert if ($id <= 0 || $contractNumber === '') { $ymd = date('Ymd'); $prefix = 'CONT-' . $ymd . '-'; for ($j = 0; $j < 20 && $id <= 0; $j++) { try { $stmtM = $pdo->prepare("SELECT IFNULL(MAX(CAST(RIGHT(contract_number,5) AS UNSIGNED)),0) FROM contracts WHERE contract_number LIKE :pref"); $stmtM->execute([':pref' => $prefix . '%']); $maxSeq = (int)$stmtM->fetchColumn(); $candidate = $prefix . str_pad((string)($maxSeq + 1), 5, '0', STR_PAD_LEFT); $stmtC = $pdo->prepare("INSERT INTO contracts (customer_id, contract_number, contract_date, fields_json, signature_path, pdf_path)\n VALUES (:cid, :num, :cd, :fj, :sig, :pdf)"); $stmtC->execute([ ':cid' => $customer_id, ':num' => $candidate, ':cd' => !empty($contractDate) ? $contractDate : null, ':fj' => json_encode($fields, JSON_UNESCAPED_UNICODE), ':sig' => $signaturePath ?: null, ':pdf' => null, ]); $id = (int)$pdo->lastInsertId(); $contractNumber = $candidate; break; } catch (PDOException $e) { $driver = (int)($e->errorInfo[1] ?? 0); $sqlState = (string)($e->errorInfo[0] ?? $e->getCode()); $msg = (string)$e->getMessage(); if ($driver === 1062 || stripos($sqlState, '23000') !== false || stripos($msg, 'Duplicate') !== false) { continue; } throw $e; } } } if ($id <= 0 || $contractNumber === '') { // Último intento: usar sufijo aleatorio evitando colisiones por UNIQUE for ($j = 0; $j < 20 && $id <= 0; $j++) { $rand = random_int(1, 99999); $candidate = $prefix . str_pad((string)$rand, 5, '0', STR_PAD_LEFT); try { $stmtC = $pdo->prepare("INSERT INTO contracts (customer_id, contract_number, contract_date, fields_json, signature_path, pdf_path)\n VALUES (:cid, :num, :cd, :fj, :sig, :pdf)"); $stmtC->execute([ ':cid' => $customer_id, ':num' => $candidate, ':cd' => !empty($contractDate) ? $contractDate : null, ':fj' => json_encode($fields, JSON_UNESCAPED_UNICODE), ':sig' => $signaturePath ?: null, ':pdf' => null, ]); $id = (int)$pdo->lastInsertId(); $contractNumber = $candidate; break; } catch (PDOException $e) { $driver = (int)($e->errorInfo[1] ?? 0); $sqlState = (string)($e->errorInfo[0] ?? $e->getCode()); $msg = (string)$e->getMessage(); if ($driver === 1062 || stripos($sqlState, '23000') !== false || stripos($msg, 'Duplicate') !== false) { continue; // probar otro aleatorio } throw $e; } } if ($id <= 0 || $contractNumber === '') { $_SESSION['flash_error'] = 'No se pudo generar un número de contrato único. Intenta nuevamente.'; header('Location: ' . BASE_URL . '?route=contratos.create&customer_id=' . $customer_id); exit; } } // Activar servicio del cliente al crear el contrato try { $pdoU = (new Database())->getConnection(); $stmtU = $pdoU->prepare("UPDATE customers SET status = 'Activo' WHERE id = :id"); $stmtU->execute([':id' => $customer_id]); } catch (Throwable $e) { /* noop */ } // Generar archivos con el número definitivo (opcional) $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderContractHtml($customer, $fields, $contractDate, $signaturePath ? (BASE_URL . $signaturePath) : null, $contractNumber); // 1) DOCX opcional $templatePath = __DIR__ . '/../img/contrato.docx'; if (is_file($templatePath) && class_exists('PhpOffice\\PhpWord\\TemplateProcessor')) { try { $processor = new PhpOffice\PhpWord\TemplateProcessor($templatePath); $name = (string)($customer['name'] ?? ''); $document = trim((string)($customer['document_type'] ?? '') . ' ' . (string)($customer['document_number'] ?? '')); $processor->setValue('name', $name); $processor->setValue('document', $document); $processor->setValue('address', (string)($customer['address'] ?? '')); $processor->setValue('customer_code', (string)($customer['customer_code'] ?? '')); $processor->setValue('phone', (string)($customer['phone'] ?? '')); $processor->setValue('email', (string)($customer['email'] ?? '')); $processor->setValue('service_address', (string)($fields['service_address'] ?? ($customer['address'] ?? ''))); $processor->setValue('contract_date', (string)$contractDate); $processor->setValue('connection_fee', (string)($fields['connection_fee'] ?? '')); $processor->setValue('observations', (string)($fields['observations'] ?? '')); $processor->setValue('sector', (string)($fields['sector'] ?? ($customer['sector'] ?? ''))); $processor->setValue('community', (string)($fields['community'] ?? '')); $processor->setValue('municipality', (string)($fields['municipality'] ?? '')); $processor->setValue('regimen', (string)($fields['regimen'] ?? '')); $processor->setValue('installments', (string)($fields['installments'] ?? '')); $processor->setValue('monthly_fee', (string)($fields['monthly_fee'] ?? '')); $processor->setValue('domicilio', (string)($fields['domicilio'] ?? ($customer['address'] ?? ''))); $processor->setValue('domicilio_extra', (string)($fields['domicilio_extra'] ?? '')); if (!empty($signaturePath)) { $sigAbs = __DIR__ . '/../public/' . ltrim($signaturePath, '/'); if (is_file($sigAbs) && method_exists($processor, 'setImageValue')) { $processor->setImageValue('signature', [ 'path' => $sigAbs, 'width' => 300, 'height' => 80, 'ratio' => true, ]); } } $docxFile = 'contract_' . $contractNumber . '.docx'; $docxFull = $contractsDir . '/' . $docxFile; $processor->saveAs($docxFull); $docxPath = 'contracts/' . $docxFile; } catch (Throwable $e) { /* noop */ } } // 2) PDF opcional: lo generamos aquí (y si falla, lo generará el endpoint de descarga) if (class_exists('Dompdf\\Dompdf')) { try { $dompdf = new Dompdf\Dompdf(['isRemoteEnabled' => true]); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $pdfFile = 'contract_' . $contractNumber . '.pdf'; $pdfFull = $contractsDir . '/' . $pdfFile; file_put_contents($pdfFull, $dompdf->output()); $pdfPath = 'contracts/' . $pdfFile; } catch (Throwable $e) { $pdfPath = null; } } // Actualizar rutas si se generaron if ($docxPath || $pdfPath) { try { $fieldsToSave = $fields; if ($docxPath) { $fieldsToSave['_docx_path'] = $docxPath; } $pdoU = (new Database())->getConnection(); $stmtU = $pdoU->prepare('UPDATE contracts SET pdf_path = :pdf, fields_json = :fj WHERE id = :id'); $stmtU->execute([ ':pdf' => $pdfPath, ':fj' => json_encode($fieldsToSave, JSON_UNESCAPED_UNICODE), ':id' => $id, ]); } catch (Throwable $e) { /* noop */ } } $_SESSION['flash_success'] = '¡Contrato generado y guardado correctamente!'; // Volver al detalle de cliente y abrir el contrato en una pestaña nueva automáticamente header('Location: ' . BASE_URL . '?route=clientes.show&id=' . (int)$customer_id . '&open_contract=' . (int)$id); exit; } public function download(): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $c = Contract::findById($id); if (!$c) { http_response_code(404); echo 'Contrato no encontrado'; return; } // ¿Forzar descarga? $download = false; if (isset($_GET['dl'])) { $download = ((int)$_GET['dl'] === 1); } elseif (isset($_GET['download'])) { $download = ((int)$_GET['download'] === 1); } if (!empty($c['pdf_path'])) { $full = __DIR__ . '/../public/' . ltrim($c['pdf_path'], '/'); if (is_file($full)) { header('Content-Type: application/pdf'); header('Content-Disposition: ' . ($download ? 'attachment' : 'inline') . '; filename="' . basename($full) . '"'); readfile($full); return; } } // Intentar DOCX desde fields_json $fields = json_decode((string)($c['fields_json'] ?? '[]'), true) ?: []; if (!empty($fields['_docx_path'])) { $docxFull = __DIR__ . '/../public/' . ltrim($fields['_docx_path'], '/'); if (is_file($docxFull)) { header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document'); header('Content-Disposition: attachment; filename="' . basename($docxFull) . '"'); readfile($docxFull); return; } } // Fallback: regenerar desde JSON y si es posible crear PDF al vuelo $pdo = (new Database())->getConnection(); $stmt = $pdo->prepare('SELECT * FROM customers WHERE id = :id LIMIT 1'); $stmt->execute([':id' => (int)$c['customer_id']]); $customer = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; $sigUrl = !empty($c['signature_path']) ? (BASE_URL . ltrim($c['signature_path'], '/')) : null; $html = $this->renderContractHtml($customer, $fields, (string)($c['contract_date'] ?? date('Y-m-d')), $sigUrl, (string)($c['contract_number'] ?? '')); // Intentar cargar composer si existe $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } // Si Dompdf está disponible, generar PDF y guardarlo para futuras visitas if (class_exists('Dompdf\\Dompdf')) { $contractsDir = __DIR__ . '/../public/contracts'; if (!is_dir($contractsDir)) { @mkdir($contractsDir, 0777, true); } $dompdf = new Dompdf\Dompdf(['isRemoteEnabled' => true]); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $safeNum = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($c['contract_number'] ?? ('ID'.$c['id']))); $pdfFile = 'contract_' . $safeNum . '.pdf'; $pdfFull = $contractsDir . '/' . $pdfFile; file_put_contents($pdfFull, $dompdf->output()); // Persistir ruta $rel = 'contracts/' . $pdfFile; $up = $pdo->prepare('UPDATE contracts SET pdf_path = :p WHERE id = :id'); $up->execute([':p' => $rel, ':id' => (int)$c['id']]); header('Content-Type: application/pdf'); header('Content-Disposition: ' . ($download ? 'attachment' : 'inline') . '; filename="' . basename($pdfFull) . '"'); readfile($pdfFull); return; } // Si no hay Dompdf, devolver HTML (descarga o inline) if ($download) { $filename = 'contract_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($c['contract_number'] ?? '')) . '.html'; header('Content-Type: text/html; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo $html; return; } else { header('Content-Type: text/html; charset=UTF-8'); echo $html; return; } } public function delete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('dashboard'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $customer_id = (int)($_POST['customer_id'] ?? 0); if ($id <= 0 || $customer_id <= 0) { redirect('clientes.index'); } $c = Contract::findById($id); if (!$c) { $_SESSION['flash_error'] = 'Contrato no encontrado.'; header('Location: ' . BASE_URL . '?route=clientes.show&id=' . $customer_id); return; } // Eliminar archivos asociados si existen $basePublic = __DIR__ . '/../public/'; // PDF if (!empty($c['pdf_path'])) { $pdfFull = $basePublic . ltrim($c['pdf_path'], '/'); if (is_file($pdfFull)) { @unlink($pdfFull); } } // Firma if (!empty($c['signature_path'])) { $sigFull = $basePublic . ltrim($c['signature_path'], '/'); if (is_file($sigFull)) { @unlink($sigFull); } } // DOCX (si fue generado) $fj = json_decode((string)($c['fields_json'] ?? '[]'), true) ?: []; if (!empty($fj['_docx_path'])) { $docxFull = $basePublic . ltrim($fj['_docx_path'], '/'); if (is_file($docxFull)) { @unlink($docxFull); } } try { Contract::delete($id); $_SESSION['flash_success'] = 'Contrato eliminado correctamente.'; } catch (Throwable $e) { $_SESSION['flash_error'] = 'No se pudo eliminar el contrato.'; } header('Location: ' . BASE_URL . '?route=clientes.show&id=' . $customer_id); } public function preview(): void { // Renderiza la plantilla DOCX como HTML. Si existe PhpOffice, rellena con valores parciales. $customer_id = (int)($_GET['customer_id'] ?? 0); if ($customer_id <= 0) { http_response_code(400); echo 'ID de cliente inválido'; return; } $pdo = (new Database())->getConnection(); $stmt = $pdo->prepare('SELECT * FROM customers WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $customer_id]); $customer = $stmt->fetch(PDO::FETCH_ASSOC); if (!$customer) { http_response_code(404); echo 'Cliente no encontrado'; return; } $contractsDir = __DIR__ . '/../public/contracts'; $tmpDir = $contractsDir . '/tmp'; if (!is_dir($contractsDir)) { @mkdir($contractsDir, 0777, true); } if (!is_dir($tmpDir)) { @mkdir($tmpDir, 0777, true); } $fields = [ 'service_address' => $_GET['f_service_address'] ?? ($customer['address'] ?? ''), 'connection_fee' => $_GET['f_connection_fee'] ?? '', 'observations' => $_GET['f_observations'] ?? '', // nuevos campos 'sector' => $_GET['f_sector'] ?? ($customer['sector'] ?? ''), 'community' => $_GET['f_community'] ?? '', 'municipality' => $_GET['f_municipality'] ?? '', 'regimen' => $_GET['f_regimen'] ?? '', 'installments' => $_GET['f_installments'] ?? '', 'monthly_fee' => $_GET['f_monthly_fee'] ?? '', 'domicilio' => $_GET['f_domicilio'] ?? ($customer['address'] ?? ''), 'domicilio_extra' => $_GET['f_domicilio_extra'] ?? '', ]; $contractDate = $_GET['contract_date'] ?? date('Y-m-d'); // Intentar usar PhpOffice para convertir DOCX a HTML $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $templatePath = __DIR__ . '/../img/contrato.docx'; $canTemplate = is_file($templatePath) && class_exists('PhpOffice\\PhpWord\\IOFactory'); if ($canTemplate) { try { // Si existe TemplateProcessor, rellenar marcadores if (class_exists('PhpOffice\\PhpWord\\TemplateProcessor')) { $processor = new PhpOffice\PhpWord\TemplateProcessor($templatePath); $processor->setValue('name', (string)($customer['name'] ?? '')); $processor->setValue('document', trim((string)($customer['document_type'] ?? '') . ' ' . (string)($customer['document_number'] ?? ''))); $processor->setValue('address', (string)($customer['address'] ?? '')); $processor->setValue('customer_code', (string)($customer['customer_code'] ?? '')); $processor->setValue('phone', (string)($customer['phone'] ?? '')); $processor->setValue('email', (string)($customer['email'] ?? '')); $processor->setValue('service_address', (string)$fields['service_address']); $processor->setValue('contract_date', (string)$contractDate); $processor->setValue('connection_fee', (string)$fields['connection_fee']); $processor->setValue('observations', (string)$fields['observations']); // nuevos campos $processor->setValue('sector', (string)$fields['sector']); $processor->setValue('community', (string)$fields['community']); $processor->setValue('municipality', (string)$fields['municipality']); $processor->setValue('regimen', (string)$fields['regimen']); $processor->setValue('installments', (string)$fields['installments']); $processor->setValue('monthly_fee', (string)$fields['monthly_fee']); $processor->setValue('domicilio', (string)$fields['domicilio']); $processor->setValue('domicilio_extra', (string)$fields['domicilio_extra']); $tempDocx = $tmpDir . '/prev_' . uniqid() . '.docx'; $processor->saveAs($tempDocx); } else { $tempDocx = $templatePath; // sin reemplazos } // Convertir a HTML $phpWord = PhpOffice\PhpWord\IOFactory::load($tempDocx); $writer = PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML'); ob_start(); $writer->save('php://output'); $htmlBody = ob_get_clean(); if (isset($processor) && is_file($tempDocx) && $tempDocx !== $templatePath) { @unlink($tempDocx); } header('Content-Type: text/html; charset=UTF-8'); echo '<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#222} .docx{max-width:900px;margin:0 auto} img{max-width:100%}</style></head><body class="docx">' . $htmlBody . '</body></html>'; return; } catch (Throwable $e) { // Fallthrough al fallback HTML } } // Fallback: usar la plantilla HTML interna para previsualizar $html = $this->renderContractHtml($customer, $fields, $contractDate, null, 'PREVIEW'); header('Content-Type: text/html; charset=UTF-8'); echo $html; } private function generateContractNumber(PDO $pdo): string { $ymd = date('Ymd'); $prefix = 'CONT-' . $ymd . '-'; // Asegurar tabla de contadores diarios try { $pdo->exec("CREATE TABLE IF NOT EXISTS contract_counters (\n day CHAR(8) NOT NULL PRIMARY KEY,\n seq INT NOT NULL DEFAULT 0\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); } catch (Throwable $e) { /* noop: si falla, usaremos fallback */ } try { // Semilla inicial: si no existe fila del día, crearla con el máximo sufijo actual del día $stmtSeed = $pdo->prepare( "INSERT INTO contract_counters (day, seq)\n SELECT :day, IFNULL(MAX(CAST(RIGHT(contract_number, 5) AS UNSIGNED)), 0)\n FROM contracts\n WHERE contract_number LIKE :pref\n ON DUPLICATE KEY UPDATE seq = seq" ); $stmtSeed->execute([':day' => $ymd, ':pref' => $prefix . '%']); // Incremento atómico y obtener el valor nuevo mediante LAST_INSERT_ID $stmtInc = $pdo->prepare('UPDATE contract_counters SET seq = LAST_INSERT_ID(seq + 1) WHERE day = :day'); $stmtInc->execute([':day' => $ymd]); $seq = (int)$pdo->lastInsertId(); if ($seq <= 0) { $stmtGet = $pdo->prepare('SELECT seq FROM contract_counters WHERE day = :day'); $stmtGet->execute([':day' => $ymd]); $seq = (int)$stmtGet->fetchColumn(); if ($seq <= 0) { $seq = 1; } } return $prefix . str_pad((string)$seq, 5, '0', STR_PAD_LEFT); } catch (Throwable $e) { // Fallback seguro en caso de error $seq = (int)(microtime(true) * 1000) % 100000; if ($seq < 1) { $seq = rand(1, 99999); } return $prefix . str_pad((string)$seq, 5, '0', STR_PAD_LEFT); } } private function renderContractHtml(array $customer, array $fields, string $contractDate, ?string $signatureUrl = null, string $contractNumber = ''): string { $name = trim((string)($customer['name'] ?? '')); $docNum = trim((string)($customer['document_number'] ?? '')); $address = trim((string)($fields['domicilio'] ?? ($customer['address'] ?? ''))); $addressExtra = trim((string)($fields['domicilio_extra'] ?? '')); $sector = trim((string)($fields['sector'] ?? ($customer['sector'] ?? ''))); $community = trim((string)($fields['community'] ?? '')); $municipality = trim((string)($fields['municipality'] ?? '')); $regimen = trim((string)($fields['regimen'] ?? '')); $installments = trim((string)($fields['installments'] ?? '')); $monthlyFee = trim((string)($fields['monthly_fee'] ?? '')); $systemData = SystemData::get(); $committeeName = trim((string)($systemData['committee_name'] ?? '')); if ($committeeName === '') { $committeeName = 'Comité de Agua Potable y Saneamiento'; } $providerReg = trim((string)($systemData['provider_registration_number'] ?? '')); $ruc = trim((string)($systemData['ruc_number'] ?? '')); $sysMunicipality = trim((string)($systemData['municipality'] ?? '')); $sysDepartment = trim((string)($systemData['department'] ?? '')); $physicalAddress = trim((string)($systemData['physical_address'] ?? '')); $phone = trim((string)($systemData['phone'] ?? '')); if ($physicalAddress !== '') { $maxAddr = 110; if (mb_strlen($physicalAddress) > $maxAddr) { $physicalAddress = rtrim(mb_substr($physicalAddress, 0, $maxAddr)) . '...'; } } $locationLine = trim(implode(' - ', array_filter([$sysMunicipality, $sysDepartment]))); $regRucLine = trim(implode(' | ', array_filter([ $providerReg !== '' ? ('Reg. prestador: ' . $providerReg) : '', $ruc !== '' ? ('RUC: ' . $ruc) : '', ]))); $contactLine = trim(implode(' | ', array_filter([ $physicalAddress !== '' ? ('Dir.: ' . $physicalAddress) : '', $phone !== '' ? ('Tel.: ' . $phone) : '', ]))); $logoDataUri = null; $logoRel = trim((string)($systemData['logo_path'] ?? '')); if ($logoRel !== '') { $logoAbs = dirname(__DIR__) . '/public/' . ltrim($logoRel, '/'); if (is_file($logoAbs)) { $ext = strtolower(pathinfo($logoAbs, PATHINFO_EXTENSION)); $mimeMap = [ 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', 'gif' => 'image/gif', ]; $mime = $mimeMap[$ext] ?? 'application/octet-stream'; $bin = @file_get_contents($logoAbs); if ($bin !== false) { $logoDataUri = 'data:' . $mime . ';base64,' . base64_encode($bin); } } } ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Solicitud de Suscripción de Servicio de Agua <?= htmlspecialchars($contractNumber ?: '') ?></title> <style> @page { margin: 12px; } body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.5; color:#111; margin: 0; } .membrete { border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 10px; } .membrete-table { width: 100%; border-collapse: collapse; table-layout: auto; } .membrete-logo { width: 64px; vertical-align: top; padding-right: 8px; } .membrete-logo img { width: 64px; height: 64px; object-fit: contain; } .membrete-info { vertical-align: top; } .membrete-right { width: 170px; vertical-align: top; text-align: right; } .membrete-title { font-size: 12px; font-weight: bold; margin: 0; line-height: 1.2; } .membrete-line { font-size: 9px; margin: 0; line-height: 1.2; color: #333; } .membrete-meta { font-size: 9px; color: #555; } /* Header */ .doc-header { position: fixed; top: -90px; left: 0; right: 0; } .doc-header .wrap { display:flex; justify-content:space-between; align-items:center; border-bottom:2px solid #0d6efd; padding-bottom:8px; } .brand { display:flex; align-items:center; gap:10px; } .brand .logo { width:34px; height:34px; object-fit:contain; } .brand .name { font-weight:700; font-size:14px; color:#0d6efd; line-height:1.2; } .brand .line { font-size:10px; color:#374151; line-height:1.2; } .brand .desc { font-size:11px; color:#4b5563; } .meta { text-align:right; font-size:11px; color:#374151; } .meta div strong { color:#111; } /* Footer */ .doc-footer { position: fixed; bottom: -50px; left: 0; right: 0; border-top:1px solid #e5e7eb; color:#6b7280; font-size:10px; padding-top:6px; display:flex; justify-content:space-between; } .pagenum:after { content: counter(page) ' / ' counter(pages); } /* Watermark */ .watermark { position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%) rotate(-12deg); font-size: 110px; color: rgba(13,110,253,0.06); white-space:nowrap; } /* Content helpers */ .title { display:none; } .subtitle { display:none; } .row { display:flex; gap:10px; align-items:center; margin: 10px 0; flex-wrap:wrap; } .label { white-space:nowrap; color:#111; } .fill { display:inline-block; min-width:120px; border-bottom:1px dotted #6b7280; padding:0 6px; margin:0 4px; color:#111; } .fill.wide { min-width:280px; } .fill.narrow { min-width:90px; } .section { margin: 6px 0; } .block { margin: 12px 0; } .indent { text-indent: 1.5em; } .regimen { display:flex; gap:24px; margin: 8px 0; } .check { display:inline-block; width:14px; height:14px; border:1px solid #0d6efd; margin-right:6px; vertical-align:middle; border-radius:2px; } .checked { background:#0d6efd; } .signature-area { margin-top: 50px; text-align:center; } .signature-line { border-top:1px solid #111; width:340px; margin: 6px auto 0; padding-top:6px; } .muted { color:#6b7280; } .small { font-size: 11px; } </style> </head> <body> <div class="membrete"> <table class="membrete-table"> <tr> <td class="membrete-logo"> <?php if (!empty($logoDataUri)): ?> <img src="<?= htmlspecialchars($logoDataUri) ?>" alt="Logo"> <?php endif; ?> </td> <td class="membrete-info"> <p class="membrete-title"><?= htmlspecialchars($committeeName) ?></p> <?php if ($regRucLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($regRucLine) ?></p> <?php endif; ?> <?php if ($locationLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($locationLine) ?></p> <?php endif; ?> <?php if ($contactLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($contactLine) ?></p> <?php endif; ?> </td> <td class="membrete-right"> <p class="membrete-title">Contrato</p> <div class="membrete-meta"><strong>Nº:</strong> <?= htmlspecialchars($contractNumber ?: '—') ?></div> <div class="membrete-meta"><strong>Fecha:</strong> <?= htmlspecialchars(format_date($contractDate)) ?></div> </td> </tr> </table> </div> <div class="content"> <div class="title">Comité de Agua Potable y Saneamiento, San Carlos.</div> <div class="subtitle">SOLICITUD DE SUSCRIPCION DE SERVICIO DE AGUA</div> <div class="row"> <span class="label">No. de Contrato o solicitud:</span> <span class="fill narrow"><?= htmlspecialchars($contractNumber ?: '') ?></span> <span class="label">Sector o Barrio:</span> <span class="fill wide"><?= htmlspecialchars($sector) ?></span> <span class="label">Fecha:</span> <span class="fill narrow"><?= htmlspecialchars(format_date($contractDate)) ?></span> </div> <div class="section"> <div class="row"> <span class="label">Comunidad:</span> <span class="fill wide"><?= htmlspecialchars($community) ?></span> </div> <div class="row"> <span class="label">Municipio:</span> <span class="fill wide"><?= htmlspecialchars($municipality) ?></span> </div> <div class="row"> <span class="label">Nombre del Solicitante:</span> <span class="fill wide"><?= htmlspecialchars($name) ?></span> </div> <div class="row"> <span class="label">Cedula de identidad:</span> <span class="fill narrow"><?= htmlspecialchars($docNum) ?></span> <span class="label">Domicilio:</span> <span class="fill wide"><?= htmlspecialchars($address) ?></span> </div> <?php if ($addressExtra !== ''): ?> <div class="row"> <span class="fill" style="min-width:100%;"><?= htmlspecialchars($addressExtra) ?></span> </div> <?php endif; ?> </div> <div class="block"> <div class="label" style="font-weight:700;">REGIMEN DE LA PROPIEDAD</div> <div class="regimen"> <div><span class="check <?= $regimen==='Propietario' ? 'checked' : '' ?>"></span> Propietario: ___________</div> <div><span class="check <?= $regimen==='Inquilino' ? 'checked' : '' ?>"></span> Inquilino: ____________</div> <div><span class="check <?= $regimen==='Ocupante' ? 'checked' : '' ?>"></span> Ocupante: ______________</div> </div> </div> <div class="block"> <div class="row"> <span>Solicito pagar el derecho de conexión en un numero de:</span> <span class="fill narrow"><?= htmlspecialchars($installments) ?></span> <span>cuotas mensuales de C$</span> <span class="fill narrow"><?= htmlspecialchars($monthlyFee) ?></span>. </div> <div class="row"> <span>Adicional al pago correspondiente al mes de servicio una vez goce del derecho del agua potable</span> </div> </div> <div class="block"> <div>Me comprometo a instalar el servicio cuando se me conceda y a cumplir las disposiciones siguientes:</div> <div class="indent">a) Exigir el estricto cumplimiento de las normativas aplicables del sector de agua potable.</div> <div class="indent">b) Denunciar cualquier conducta irregular u omision de los prestadores o agentes que pudiera afectar sus derechos o perjudicar la calidad del servicio y el ambiente.</div> <div class="indent">c) Recurrir de queja cuando la resolución del prestador fuere denegatoria o hubiere silencio administrativo.</div> <div class="indent">d) Pagar oportunamente los consumos y demás cargos tarifarios que le facturen. No existirá gratuidad para la prestación de servicios a ninguna persona natural o jurídica publica o privada.</div> <div class="indent">e) El precio que establezca el prestador, autorizado por el Ente Regulador podrá ser pagado al contado o plazos fijados por el mismo prestador, de acuerdo a criterios técnicos como el diámetro de la conexión entre otros.</div> <div class="indent">f) El prestador podrá negarse a suministrar el servicio de agua potable a cualquier persona que le adeude cantidad alguna por facturas pendientes o que viole en alguna forma las disposiciones del prestador o del Ente Regulador.</div> <div class="indent">g) Pagar los servicios prestados de acuerdo a lo establecido en el Acuerdo Tarifario aprobado por el Ente Regulador.</div> <div class="indent">h) Conocer el regigen tarifario vigente aprobado por el Ente Regulador y sus sucesivas modificaciones con una anticipación no menor de treinta días calendario.</div> <div class="indent">i) Recibir factura en el domicilio señalado en el contrato de servicio, sin costo alguno y con antelación suficiente a la fecha de cortes no menor de quince días calendarios. En caso de no recibir la factura en tiempo oportuno, subsistirá la obligación de pago y deberá ser cancelada en las oficinas comerciales o en cualquier lugar autorizado, o cobrador habilitado por el prestador.</div> <div class="indent">j) Conocer a través de la factura, todos los elementos constitutivos de la misma.</div> <div class="indent">k) Pagar los cargos de cortes y reconexión del servicio cuando este haya sido cortado por falta de pago, según el monto establecido por el prestador y aprobado por el Ente Regulador y los artículos, 15 y 19, parte I de la Guia para la organización y administración de acueductos rurales, establecido por el INAA.</div> </div> <div class="signature-area"> <?php if ($signatureUrl): ?> <img src="<?= htmlspecialchars($signatureUrl) ?>" alt="Firma" style="max-height:90px; max-width:100%; object-fit:contain;" /> <?php endif; ?> <div class="signature-line">FIRMA DEL SOLICITANTE</div> </div> </div> </body> </html> <?php return (string)ob_get_clean(); } }
Coded With 💗 by
0x6ick