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: LecturasController.php
<?php use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class LecturasController { public function index() { $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = 15; $filters = [ 'q' => trim($_GET['q'] ?? ''), 'customer_id' => isset($_GET['customer_id']) && $_GET['customer_id'] !== '' ? (int)$_GET['customer_id'] : null, 'meter_id' => isset($_GET['meter_id']) && $_GET['meter_id'] !== '' ? (int)$_GET['meter_id'] : null, 'date_from' => $_GET['date_from'] ?? null, 'date_to' => $_GET['date_to'] ?? null, ]; $result = Reading::getAll($filters, $page, $perPage); $items = $result['data']; $total = $result['total']; $perPage = $result['perPage']; $lastPage = (int)max(1, ceil($total / max(1, $perPage))); $customers = Customer::getActive(); $metersFilter = $filters['customer_id'] ? Meter::getByCustomer((int)$filters['customer_id']) : []; // CSRF token for inline delete form if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } $csrf = $_SESSION['csrf']; require __DIR__ . '/../views/lecturas/index.php'; } public function create() { // CSRF token if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } $csrf = $_SESSION['csrf']; $customers = Customer::getActive(); $selectedCustomer = isset($_GET['customer_id']) ? (int)$_GET['customer_id'] : null; $meters = $selectedCustomer ? Meter::getByCustomer($selectedCustomer) : []; require __DIR__ . '/../views/lecturas/create.php'; } public function store() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $csrf = $_POST['csrf'] ?? ''; if (!hash_equals($_SESSION['csrf'] ?? '', $csrf)) { $_SESSION['flash_error'] = 'Sesión inválida. Intenta nuevamente.'; header('Location: ' . BASE_URL . '?route=lecturas.create'); exit; } $customer_id = (int)($_POST['customer_id'] ?? 0); $meter_id = (int)($_POST['meter_id'] ?? 0); $reading_date = trim($_POST['reading_date'] ?? ''); $period_start = trim($_POST['period_start'] ?? ''); $period_end = trim($_POST['period_end'] ?? ''); $previous_reading = (float)($_POST['previous_reading'] ?? 0); $reading_value = (float)($_POST['reading_value'] ?? 0); $notes = trim($_POST['notes'] ?? ''); // Bloquear creación de lecturas para clientes sin contrato if ($customer_id > 0 && !Contract::existsForCustomer($customer_id)) { $_SESSION['flash_error'] = 'El cliente no tiene contrato registrado. No puedes registrar lecturas.'; header('Location: ' . BASE_URL . '?route=lecturas.create'); exit; } if ($customer_id <= 0 || $meter_id <= 0 || $reading_date === '' || $period_start === '' || $period_end === '') { $_SESSION['flash_error'] = 'Datos obligatorios faltantes.'; header('Location: ' . BASE_URL . '?route=lecturas.create'); exit; } // Validar que el medidor pertenezca al cliente seleccionado $meter = Meter::findById($meter_id); if (!$meter || (int)($meter['customer_id'] ?? 0) !== $customer_id) { $_SESSION['flash_error'] = 'El medidor seleccionado no pertenece al cliente indicado.'; header('Location: ' . BASE_URL . '?route=lecturas.create'); exit; } if ($reading_value < $previous_reading) { $_SESSION['flash_error'] = 'La lectura actual no puede ser menor que la anterior.'; header('Location: ' . BASE_URL . '?route=lecturas.create&customer_id=' . $customer_id); exit; } $reader_user_id = (int)($_SESSION['user']['id'] ?? 0); $id = Reading::create([ 'meter_id' => $meter_id, 'customer_id' => $customer_id, 'period_start' => $period_start ?: null, 'period_end' => $period_end ?: null, 'reading_date' => $reading_date, 'previous_reading' => $previous_reading, 'reading_value' => $reading_value, 'reader_user_id' => $reader_user_id, 'voucher_printed' => 0, 'notes' => $notes, ]); if ($id > 0) { $_SESSION['flash_success'] = 'Lectura registrada correctamente.'; // Redirigir al Comprobante de Lectura sin parámetro de impresión automática header('Location: ' . BASE_URL . '?route=lecturas.voucher&id=' . $id); exit; } $_SESSION['flash_error'] = 'No se pudo registrar la lectura.'; header('Location: ' . BASE_URL . '?route=lecturas.create'); exit; } public function edit() { // CSRF token if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } $csrf = $_SESSION['csrf']; $id = (int)($_GET['id'] ?? 0); $reading = $id ? Reading::findById($id) : null; if (!$reading) { http_response_code(404); echo 'Lectura no encontrada'; return; } $customers = Customer::getActive(); $meters = Meter::getByCustomer((int)$reading['customer_id']); require __DIR__ . '/../views/lecturas/edit.php'; } public function update() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $csrf = $_POST['csrf'] ?? ''; if (!hash_equals($_SESSION['csrf'] ?? '', $csrf)) { $_SESSION['flash_error'] = 'Sesión inválida. Intenta nuevamente.'; header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $id = (int)($_POST['id'] ?? 0); // Load the original reading to lock the customer on edit $reading = $id ? Reading::findById($id) : null; if (!$reading) { $_SESSION['flash_error'] = 'Lectura no encontrada.'; header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } // Ignore any posted customer_id: the lectura's customer cannot change in edit $customer_id = (int)($reading['customer_id'] ?? 0); $meter_id = (int)($_POST['meter_id'] ?? 0); $reading_date = trim($_POST['reading_date'] ?? ''); $period_start = trim($_POST['period_start'] ?? ''); $period_end = trim($_POST['period_end'] ?? ''); $previous_reading = (float)($_POST['previous_reading'] ?? 0); $reading_value = (float)($_POST['reading_value'] ?? 0); $notes = trim($_POST['notes'] ?? ''); if ($id <= 0 || $meter_id <= 0 || $reading_date === '' || $period_start === '' || $period_end === '') { $_SESSION['flash_error'] = 'Datos obligatorios faltantes.'; header('Location: ' . BASE_URL . '?route=lecturas.edit&id=' . $id); exit; } // Validate meter belongs to the lectura's customer $meter = Meter::findById($meter_id); if (!$meter || (int)($meter['customer_id'] ?? 0) !== $customer_id) { $_SESSION['flash_error'] = 'El medidor seleccionado no pertenece al cliente asignado a esta lectura.'; header('Location: ' . BASE_URL . '?route=lecturas.edit&id=' . $id); exit; } if ($reading_value < $previous_reading) { $_SESSION['flash_error'] = 'La lectura actual no puede ser menor que la anterior.'; header('Location: ' . BASE_URL . '?route=lecturas.edit&id=' . $id); exit; } $reader_user_id = (int)($_SESSION['user']['id'] ?? 0); $ok = Reading::update($id, [ 'meter_id' => $meter_id, 'customer_id' => $customer_id, 'period_start' => $period_start ?: null, 'period_end' => $period_end ?: null, 'reading_date' => $reading_date, 'previous_reading' => $previous_reading, 'reading_value' => $reading_value, 'reader_user_id' => $reader_user_id, 'voucher_printed' => 0, 'notes' => $notes, ]); if ($ok) { $_SESSION['flash_success'] = 'Lectura actualizada.'; header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $_SESSION['flash_error'] = 'No se pudo actualizar la lectura.'; header('Location: ' . BASE_URL . '?route=lecturas.edit&id=' . $id); exit; } public function delete() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $csrf = $_POST['csrf'] ?? ''; if (!hash_equals($_SESSION['csrf'] ?? '', $csrf)) { $_SESSION['flash_error'] = 'Sesión inválida.'; header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } $id = (int)($_POST['id'] ?? 0); if ($id > 0) { Reading::delete($id); $_SESSION['flash_success'] = 'Lectura eliminada.'; } header('Location: ' . BASE_URL . '?route=lecturas.index'); exit; } public function export() { requireAuth(['ADMIN','CAJERO','LECTOR']); $filters = [ 'q' => trim($_GET['q'] ?? ''), 'customer_id' => isset($_GET['customer_id']) && $_GET['customer_id'] !== '' ? (int)$_GET['customer_id'] : null, 'meter_id' => isset($_GET['meter_id']) && $_GET['meter_id'] !== '' ? (int)$_GET['meter_id'] : null, 'date_from' => $_GET['date_from'] ?? null, 'date_to' => $_GET['date_to'] ?? null, ]; $rows = $this->fetchAllReadings($filters); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=lecturas_' . date('Ymd_His') . '.csv'); $out = fopen('php://output', 'w'); fputcsv($out, ['Cliente','Medidor','Lectura anterior','Lectura actual','Consumo (m3)','Fecha','Observaciones','Lector']); foreach ($rows as $r) { fputcsv($out, [ (string)($r['customer_name'] ?? ''), (string)($r['meter_number'] ?? ''), number_format((float)$r['previous_reading'], 3, '.', ''), number_format((float)$r['reading_value'], 3, '.', ''), number_format((float)($r['consumption_m3'] ?? ($r['reading_value'] - $r['previous_reading'])), 3, '.', ''), (string)$r['reading_date'], (string)($r['notes'] ?? ''), (string)($r['reader_name'] ?? ''), ]); } fclose($out); exit; } public function exportExcel(): void { requireAuth(['ADMIN','CAJERO','LECTOR']); $filters = [ 'q' => trim($_GET['q'] ?? ''), 'customer_id' => isset($_GET['customer_id']) && $_GET['customer_id'] !== '' ? (int)$_GET['customer_id'] : null, 'meter_id' => isset($_GET['meter_id']) && $_GET['meter_id'] !== '' ? (int)$_GET['meter_id'] : null, 'date_from' => $_GET['date_from'] ?? null, 'date_to' => $_GET['date_to'] ?? null, ]; $rows = $this->fetchAllReadings($filters); $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } if (!class_exists(Spreadsheet::class)) { http_response_code(500); echo 'PhpSpreadsheet no está disponible.'; return; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Lecturas'); $row = 1; $sheet->mergeCells("A{$row}:J{$row}"); $sheet->setCellValue("A{$row}", 'SISCAPS - Reporte de Lecturas'); $sheet->getStyle("A{$row}") ->getFont()->setBold(true)->setSize(15); $sheet->getStyle("A{$row}") ->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $row++; $sheet->mergeCells("A{$row}:J{$row}"); $sheet->setCellValue("A{$row}", 'Generado: ' . date('d/m/Y')); $row++; $sheet->mergeCells("A{$row}:J{$row}"); $sheet->setCellValue("A{$row}", 'Filtros: ' . $this->describeReadingFilters($filters)); $row += 2; $headerRow = $row; $headers = [ 'Cliente', 'Medidor', 'Periodo', 'Lectura anterior (m³)', 'Lectura actual (m³)', 'Consumo (m³)', 'Fecha lectura', 'Registrado por', 'Observaciones', ]; $sheet->fromArray($headers, null, "A{$headerRow}"); $sheet->getStyle("A{$headerRow}:I{$headerRow}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '173e62'], ], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); $row = $headerRow + 1; foreach ($rows as $reading) { $period = trim(format_date($reading['period_start'] ?? '') . ' - ' . format_date($reading['period_end'] ?? '')); $sheet->fromArray([ $reading['customer_name'] ?? '', $reading['meter_number'] ?? '', $period, (float)($reading['previous_reading'] ?? 0), (float)($reading['reading_value'] ?? 0), (float)($reading['consumption_m3'] ?? (($reading['reading_value'] ?? 0) - ($reading['previous_reading'] ?? 0))), format_date($reading['reading_date'] ?? null), $reading['reader_name'] ?? '', $reading['notes'] ?? '', ], null, "A{$row}"); $row++; } if ($row > $headerRow + 1) { $sheet->getStyle("A" . ($headerRow + 1) . ":I" . ($row - 1))->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("D" . ($headerRow + 1) . ":F" . ($row - 1)) ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_00); } foreach (range('A', 'I') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->freezePane('A' . ($headerRow + 1)); $filename = 'Lecturas_' . date('Ymd_His') . '.xlsx'; header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Cache-Control: max-age=0'); $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); $writer->save('php://output'); exit; } public function exportPdf(): void { requireAuth(['ADMIN','CAJERO','LECTOR']); $filters = [ 'q' => trim($_GET['q'] ?? ''), 'customer_id' => isset($_GET['customer_id']) && $_GET['customer_id'] !== '' ? (int)$_GET['customer_id'] : null, 'meter_id' => isset($_GET['meter_id']) && $_GET['meter_id'] !== '' ? (int)$_GET['meter_id'] : null, 'date_from' => $_GET['date_from'] ?? null, 'date_to' => $_GET['date_to'] ?? null, ]; $rows = $this->fetchAllReadings($filters); $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderReadingReportHtml($rows, $filters); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $dompdf->stream('Lecturas_' . date('Ymd_His') . '.pdf', ['Attachment' => true]); return; } header('Content-Type: text/html; charset=UTF-8'); echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } private function fetchAllReadings(array $filters): array { $probe = Reading::getAll($filters, 1, 1); $total = (int)($probe['total'] ?? 0); if ($total === 0) { return []; } $res = Reading::getAll($filters, 1, max(1, $total)); return $res['data'] ?? []; } private function describeReadingFilters(array $filters): string { $parts = []; if (!empty($filters['q'])) { $parts[] = 'Búsqueda: "' . $filters['q'] . '"'; } if (!empty($filters['customer_id'])) { $customer = Customer::findById((int)$filters['customer_id']); $parts[] = 'Cliente: ' . ($customer['name'] ?? ('ID ' . $filters['customer_id'])); } if (!empty($filters['meter_id'])) { $meter = Meter::findById((int)$filters['meter_id']); $label = $meter ? ($meter['number'] ?? ('ID ' . $filters['meter_id'])) : ('ID ' . $filters['meter_id']); $parts[] = 'Medidor: ' . $label; } if (!empty($filters['date_from']) || !empty($filters['date_to'])) { $parts[] = 'Fechas: ' . ($filters['date_from'] ?: 'inicio') . ' a ' . ($filters['date_to'] ?: 'hoy'); } return $parts ? implode(' | ', $parts) : 'Sin filtros'; } private function renderReadingReportHtml(array $rows, array $filters): string { $generatedAt = date('d/m/Y H:i'); $filtersDesc = $this->describeReadingFilters($filters); $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'] ?? '')); $municipalCertificate = trim((string)($systemData['municipal_certificate'] ?? '')); $ruc = trim((string)($systemData['ruc_number'] ?? '')); $municipality = trim((string)($systemData['municipality'] ?? '')); $department = 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([$municipality, $department]))); $regRucLine = trim(implode(' | ', array_filter([ $providerReg !== '' ? ('Reg. prestador: ' . $providerReg) : '', $municipalCertificate !== '' ? ('Cert. municipal: ' . $municipalCertificate) : '', $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>Reporte de Lecturas</title> <style> * { box-sizing: border-box; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color:#222; margin: 12px; } .membrete { border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 8px; } .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: 240px; 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; word-break: break-word; } table { width:100%; border-collapse: collapse; table-layout: fixed; } th, td { border:1px solid #dee2e6; padding:5px 6px; text-align:left; font-size: 9px; vertical-align: top; word-wrap: break-word; } th { background:#f8f9fa; } .right { text-align:right; } </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">Lecturas</p> <div class="membrete-meta"><strong>Total:</strong> <?= count($rows) ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Filtro:</strong> <?= htmlspecialchars($filtersDesc) ?></div> </td> </tr> </table> </div> <table> <thead> <tr> <th>Cliente</th> <th>Medidor</th> <th>Periodo</th> <th class="right">Anterior (m³)</th> <th class="right">Actual (m³)</th> <th class="right">Consumo (m³)</th> <th>Fecha</th> <th>Registrado por</th> <th>Observaciones</th> </tr> </thead> <tbody> <?php if (!empty($rows)): foreach ($rows as $reading): $period = trim(format_date($reading['period_start'] ?? '') . ' - ' . format_date($reading['period_end'] ?? '')); $consumo = (float)($reading['consumption_m3'] ?? (($reading['reading_value'] ?? 0) - ($reading['previous_reading'] ?? 0))); ?> <tr> <td><?= htmlspecialchars($reading['customer_name'] ?? '') ?></td> <td><?= htmlspecialchars($reading['meter_number'] ?? '') ?></td> <td><?= htmlspecialchars($period) ?></td> <td class="right"><?= format_num($reading['previous_reading'] ?? 0, 3) ?></td> <td class="right"><?= format_num($reading['reading_value'] ?? 0, 3) ?></td> <td class="right"><?= format_num($consumo, 3) ?></td> <td><?= htmlspecialchars(format_date($reading['reading_date'] ?? '')) ?></td> <td><?= htmlspecialchars($reading['reader_name'] ?? '') ?></td> <td><?= htmlspecialchars($reading['notes'] ?? '') ?></td> </tr> <?php endforeach; else: ?> <tr><td colspan="9" class="right" style="text-align:center;">No existen lecturas para los filtros aplicados.</td></tr> <?php endif; ?> </tbody> </table> </body> </html> <?php return (string)ob_get_clean(); } public function meters() { header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $cid = (int)($_GET['customer_id'] ?? 0); if ($cid <= 0) { echo json_encode([]); return; } $meters = Meter::getByCustomer($cid); echo json_encode($meters); } public function last() { header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $mid = (int)($_GET['meter_id'] ?? 0); if ($mid <= 0) { echo json_encode(['previous_reading' => 0]); return; } $last = Reading::getLastByMeter($mid); if ($last) { echo json_encode([ 'previous_reading' => (float)$last['reading_value'], 'reading_date' => $last['reading_date'], ]); return; } // If no previous reading, fallback to meter initial_reading $meter = Meter::findById($mid); $prev = $meter && isset($meter['initial_reading']) ? (float)$meter['initial_reading'] : 0.0; echo json_encode(['previous_reading' => $prev, 'reading_date' => null]); } public function searchMeters() { header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $q = trim((string)($_GET['q'] ?? '')); if ($q === '') { echo json_encode([]); return; } $rows = Meter::searchByNumber($q, 10); echo json_encode($rows); } public function findMeter() { header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $number = trim((string)($_GET['number'] ?? '')); if ($number === '') { http_response_code(400); echo json_encode(['error' => 'number requerido']); return; } $row = Meter::findByNumber($number); if (!$row) { http_response_code(404); echo json_encode(['error' => 'not_found']); return; } // Restringir si el cliente no tiene contrato $cid = (int)($row['customer_id'] ?? 0); if ($cid > 0 && !Contract::existsForCustomer($cid)) { http_response_code(403); echo json_encode(['error' => 'no_contract']); return; } echo json_encode($row); } public function customerByCode() { header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $code = trim((string)($_GET['code'] ?? '')); if ($code === '') { http_response_code(400); echo json_encode(['error' => 'code requerido']); return; } $customer = Customer::findByCode($code); if (!$customer) { http_response_code(404); echo json_encode(['error' => 'not_found']); return; } // Restringir si el cliente no tiene contrato if (!Contract::existsForCustomer((int)$customer['id'])) { http_response_code(403); echo json_encode(['error' => 'no_contract']); return; } $meters = Meter::getByCustomer((int)$customer['id']); echo json_encode([ 'customer_id' => (int)$customer['id'], 'customer_name' => (string)($customer['name'] ?? ''), 'customer_code' => (string)($customer['customer_code'] ?? ''), 'address' => (string)($customer['address'] ?? ''), 'sector' => (string)($customer['sector'] ?? ''), 'meters' => $meters, ]); } public function voucher() { $id = (int)($_GET['id'] ?? 0); $reading = $id ? Reading::findById($id) : null; if (!$reading) { http_response_code(404); echo 'Voucher no disponible'; return; } require __DIR__ . '/../views/lecturas/voucher.php'; } public function downloadTicket(): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $reading = Reading::findById($id); if (!$reading) { http_response_code(404); echo 'Lectura no encontrada'; return; } $role = $_SESSION['user']['role'] ?? ''; if ($role === 'CLIENTE') { $cid = (int)($_SESSION['user']['customer_id'] ?? 0); if ($cid <= 0 || (int)($reading['customer_id'] ?? 0) !== $cid) { http_response_code(403); echo '403 Prohibido'; return; } } $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $paperHeight = null; $html = $this->renderReadingTicketHtml($reading, $paperHeight); if (class_exists('Dompdf\\Dompdf')) { $width = 162; $minHeight = 260; $maxHeight = 1600; $height = (int)($paperHeight ?: 520); $height = max($minHeight, min($maxHeight, $height + 30)); $dompdf = null; $attempts = 0; while (true) { $dompdf = new \Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper([0, 0, $width, $height], 'portrait'); $dompdf->render(); $pages = (int)$dompdf->getCanvas()->get_page_count(); if ($pages <= 1 || $attempts >= 1 || $height >= $maxHeight) { break; } $height = min($maxHeight, (int)ceil($height * 1.35)); $attempts++; } $dompdf->stream('Ticket-Lectura-' . ($reading['id'] ?? $id) . '.pdf', ['Attachment' => true]); return; } header('Content-Type: text/html; charset=UTF-8'); echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } public function printTicket(): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $reading = Reading::findById($id); if (!$reading) { http_response_code(404); echo 'Lectura no encontrada'; return; } $role = $_SESSION['user']['role'] ?? ''; if ($role === 'CLIENTE') { $cid = (int)($_SESSION['user']['customer_id'] ?? 0); if ($cid <= 0 || (int)($reading['customer_id'] ?? 0) !== $cid) { http_response_code(403); echo '403 Prohibido'; return; } } header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); header('Content-Type: text/html; charset=UTF-8'); $paperHeight = null; $html = $this->renderReadingTicketHtml($reading, $paperHeight); $extraCss = '<style> @media screen { html, body { width: 100% !important; } body { font-size: 12px !important; line-height: 1.3 !important; max-width: none !important; } .small { font-size: 10px !important; } .center .b { font-size: 13px !important; } .label { min-width: 55% !important; } } @media print { @page{ size: auto; margin: 6mm 6mm; } html, body { width: 100% !important; } body { font-size: 12px !important; line-height: 1.3 !important; max-width: none !important; } .small { font-size: 10px !important; } .center .b { font-size: 13px !important; } .label { min-width: 55% !important; } } </style>'; $html = preg_replace('/<\/head>/i', $extraCss . '</head>', $html, 1) ?: $html; echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } private function renderReadingTicketHtml(array $reading, ?int &$paperHeight = null): string { $customerName = (string)($reading['customer_name'] ?? ''); $meterNumber = (string)($reading['meter_number'] ?? ''); $readerName = trim((string)($reading['reader_name'] ?? '')) ?: 'N/D'; $issue = (string)($reading['reading_date'] ?? ''); $due = ''; $pstart = (string)($reading['period_start'] ?? ''); $pend = (string)($reading['period_end'] ?? ''); $prev = isset($reading['previous_reading']) ? (float)$reading['previous_reading'] : 0.0; $curr = isset($reading['reading_value']) ? (float)$reading['reading_value'] : 0.0; $cons = isset($reading['consumption_m3']) ? (float)$reading['consumption_m3'] : max(0.0, $curr - $prev); $avgUnit = null; try { $cid = (int)($reading['customer_id'] ?? 0); if ($cid > 0) { $pdo = (new Database())->getConnection(); $pstartQuery = $pstart ?: '0000-01-01'; $pendQuery = $pend ?: '9999-12-31'; $stmt = $pdo->prepare( "SELECT issue_date, due_date, subtotal, consumption_m3 FROM invoices WHERE customer_id = :cid AND ( (period_start IS NOT NULL AND period_end IS NOT NULL AND period_start <= :pend AND period_end >= :pstart) OR (period_start IS NULL AND period_end IS NULL) ) ORDER BY issue_date DESC, id DESC LIMIT 1" ); $stmt->execute([ ':cid' => $cid, ':pstart' => $pstartQuery, ':pend' => $pendQuery, ]); if ($invoice = $stmt->fetch(PDO::FETCH_ASSOC)) { if ($issue === '' && !empty($invoice['issue_date'])) { $issue = (string)$invoice['issue_date']; } $due = (string)($invoice['due_date'] ?? ''); $invCons = isset($invoice['consumption_m3']) ? (float)$invoice['consumption_m3'] : 0.0; $subtotal = isset($invoice['subtotal']) ? (float)$invoice['subtotal'] : 0.0; if ($invCons > 0.00001) { $avgUnit = $subtotal / $invCons; } elseif ($cons > 0.00001 && $subtotal > 0.0) { $avgUnit = $subtotal / $cons; } } } } catch (Throwable $e) { // Ignorar errores para asegurar la generación del ticket } $days = 0; if ($pstart && $pend) { $d1 = strtotime($pstart); $d2 = strtotime($pend); if ($d1 && $d2 && $d2 >= $d1) { $days = (int)round(($d2 - $d1) / 86400) + 1; } } $openInvoicesCount = 0; $openBalance = 0.0; try { $cid = (int)($reading['customer_id'] ?? 0); if ($cid > 0) { $openInvoicesCount = Invoice::countOpenInvoicesByCustomer($cid); $openBalance = Invoice::openBalanceByCustomer($cid); } } catch (Throwable $e) { // Silenciar para evitar romper el ticket } $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'] ?? '')); $municipalCertificate = trim((string)($systemData['municipal_certificate'] ?? '')); $ruc = trim((string)($systemData['ruc_number'] ?? '')); $municipality = trim((string)($systemData['municipality'] ?? '')); $department = trim((string)($systemData['department'] ?? '')); $physicalAddress = trim((string)($systemData['physical_address'] ?? '')); $phone = trim((string)($systemData['phone'] ?? '')); $locationLine = trim(implode(' - ', array_filter([$municipality, $department]))); $regLines = array_values(array_filter([ $providerReg !== '' ? ('Reg. prestador: ' . $providerReg) : '', $municipalCertificate !== '' ? ('Cert. municipal: ' . $municipalCertificate) : '', $ruc !== '' ? ('RUC: ' . $ruc) : '', ])); $contactLines = array_values(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); } } } $issueLabel = $issue ? format_date($issue) : 'N/D'; $dueLabel = $due ? format_date($due) : 'N/D'; $wrapLines = static function (string $text, int $maxChars): int { $t = trim($text); if ($t === '') { return 0; } $len = function_exists('mb_strlen') ? mb_strlen($t) : strlen($t); return (int)max(1, (int)ceil($len / max(1, $maxChars))); }; $headerLines = 0; $headerLines += $wrapLines($committeeName, 28); foreach ($regLines as $l) { $headerLines += $wrapLines((string)$l, 42); } $headerLines += $wrapLines($locationLine, 42); foreach ($contactLines as $l) { $headerLines += $wrapLines((string)$l, 42); } $bodyLines = 0; $bodyLines += 1; $bodyLines += 1 + $wrapLines($customerName, 30); $bodyLines += 8; $bodyLines += 1; $bodyLines += 2; $bodyLines += 3; $bodyLines += 2; $logoBonus = !empty($logoDataUri) ? 55 : 0; $paperHeight = (int)max(260, min(1600, (int)ceil(70 + $logoBonus + (($headerLines * 8) + ($bodyLines * 9)) + 60))); ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Ticket Lectura #<?= (int)($reading['id'] ?? 0) ?></title> <style> @page { margin: 3mm 2mm; } body { font-family: DejaVu Sans, Arial, sans-serif; font-size: 8px; line-height:1.15; width: 57mm; margin: 0 auto; color:#000; } .center { text-align:center; } .b { font-weight:bold; } .sep { height:0; border-top:1px solid #000; margin: 5px 0; } .label { display:inline-block; min-width: 35mm; } .value { display:inline-block; } .small { font-size:6.5px; word-wrap: break-word; overflow-wrap: anywhere; word-break: break-word; } .logo { max-width: 40mm; max-height: 18mm; margin: 0 auto 2px; display:block; object-fit: contain; } </style> </head> <body> <div class="center"> <?php if (!empty($logoDataUri)): ?> <img class="logo" src="<?= htmlspecialchars($logoDataUri) ?>" alt="Logo"> <?php endif; ?> <div class="b"><?= htmlspecialchars($committeeName) ?></div> <?php if (!empty($regLines)): foreach ($regLines as $line): ?> <div class="small"><?= htmlspecialchars($line) ?></div> <?php endforeach; endif; ?> <?php if ($locationLine !== ''): ?> <div class="small"><?= htmlspecialchars($locationLine) ?></div> <?php endif; ?> <?php if (!empty($contactLines)): foreach ($contactLines as $line): ?> <div class="small"><?= htmlspecialchars($line) ?></div> <?php endforeach; endif; ?> </div> <div class="b center" style="margin-top:4px;">DETALLE DE LECTURA DE MEDIDOR</div> <div style="margin-top:6px;"> <div> <span class="label">Nombre Usuario:</span><br> <span class="value b"><?= htmlspecialchars($customerName) ?></span> </div> <div><span class="label">Fecha de emisión:</span> <span class="value"><?= htmlspecialchars($issueLabel) ?></span></div> <div><span class="label">Fecha de vencimiento:</span> <span class="value"><?= htmlspecialchars($dueLabel) ?></span></div> <div><span class="label">Días facturados:</span> <span class="value"><?= (int)$days ?></span></div> <div><span class="label">Tarifa C$:</span> <span class="value"><?= $avgUnit !== null ? format_num($avgUnit, 2) : 'N/D' ?></span></div> <div><span class="label">Numero de medidor:</span> <span class="value"><?= htmlspecialchars($meterNumber ?: '-') ?></span></div> <div><span class="label">Lectura anterior:</span> <span class="value"><?= format_num($prev, 3) ?></span></div> <div><span class="label">Lectura actual:</span> <span class="value"><?= format_num($curr, 3) ?></span></div> <div><span class="label">Consumo m³ actual:</span> <span class="value b"><?= format_num($cons, 3) ?></span></div> </div> <div class="b" style="margin-top:6px;">Pendiente de pago</div> <div style="margin-top:6px;"> <div><span class="label">Cantidad de facturas:</span> <span class="value"><?= (int)$openInvoicesCount ?></span></div> <div><span class="label">Saldo en Mora:</span> <span class="value b"><?= format_currency($openBalance) ?></span></div> </div> <div class="sep"></div> <div><span class="label">Periodo de consumo:</span><br> <span class="value">Desde <?= htmlspecialchars($pstart ? format_date($pstart) : 'N/D') ?></span> <span class="value" style="margin-left:2mm;">Hasta <?= htmlspecialchars($pend ? format_date($pend) : 'N/D') ?></span> </div> <div class="sep"></div> <div><span class="label">Nombre del lector:</span><br> <span class="value"><?= htmlspecialchars($readerName) ?></span> </div> </body> </html> <?php return (string)ob_get_clean(); } }
Coded With 💗 by
0x6ick