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: CuentasController.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 CuentasController { private function ensureCsrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } private function fetchAllAccounts(array $filters): array { $res = Account::paginate($filters, 1, 100000); return $res['items'] ?? []; } private function describeAccountFilters(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['type'])) { $parts[] = 'Tipo: ' . $filters['type']; } if ($filters['is_active'] !== '') { $parts[] = 'Estado: ' . ($filters['is_active'] === '1' ? 'Activas' : 'Inactivas'); } return $parts ? implode(' | ', $parts) : 'Sin filtros'; } private function collectAccountFilters(): array { return [ 'q' => trim($_GET['q'] ?? ''), 'category_id' => $_GET['category_id'] ?? '', 'type' => $_GET['type'] ?? '', 'is_active' => $_GET['is_active'] ?? '', ]; } public function index(): void { requireAuth(['ADMIN','CAJERO']); $csrf = $this->ensureCsrf(); Account::ensureDefaultCashAccount(); $filters = $this->collectAccountFilters(); $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = max(1, min(100, (int)($_GET['perPage'] ?? 15))); $result = Account::paginate($filters, $page, $perPage); $accounts = $result['items']; $total = $result['total']; $page = $result['page']; $perPage = $result['perPage']; $lastPage = $result['lastPage']; $categories = AccountCategory::all(); $types = ['Caja','Caja chica','Banco','Otro']; require __DIR__ . '/../views/cuentas/index.php'; } public function exportPdf(): void { requireAuth(['ADMIN','CAJERO']); $filters = $this->collectAccountFilters(); $rows = $this->fetchAllAccounts($filters); // Intentar cargar Dompdf si existe $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderAccountsPdfHtml($rows, $filters); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $dompdf->stream('Plan_de_Cuentas_' . 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>'; } public function downloadExcel(): void { requireAuth(['ADMIN','CAJERO']); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $account = Account::find($id); if (!$account) { http_response_code(404); echo 'Cuenta no encontrada'; return; } $transactions = AccountTransaction::recentForAccount($id, 500); $transfers = AccountTransfer::recentForAccount($id, 200); $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; } $currency = $account['currency'] ?? 'C$ '; $totalIngresos = 0.0; $totalEgresos = 0.0; foreach ($transactions as $tx) { $amount = (float)($tx['amount'] ?? 0); if ($amount >= 0) { $totalIngresos += $amount; } else { $totalEgresos += abs($amount); } } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Movimientos'); $headerColor = '173e62'; $row = 1; $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'SISCAPS - Detalle de Cuenta'); $sheet->getStyle("A{$row}")->getFont()->setBold(true)->setSize(15); $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $row++; $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'Cuenta: ' . ($account['name'] ?? '') . ' (' . ($account['code'] ?? '') . ')'); $row++; $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'Generado: ' . date('d/m/Y H:i') . ' — Moneda: ' . $currency); $row++; $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'Saldo actual: ' . format_currency($account['current_balance'] ?? 0, $currency)); $row += 2; $headerRow = $row; $headers = [ 'Fecha', 'Tipo', 'Concepto', 'Referencia', 'Notas', 'Método', 'Monto', 'Saldo después', 'Registrado por', ]; $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' => strtoupper($headerColor)], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); $row = $headerRow + 1; foreach ($transactions as $tx) { $amount = (float)($tx['amount'] ?? 0); $sheet->fromArray([ format_datetime($tx['transacted_at'] ?? ''), $tx['direction'] ?? '', $tx['concept'] ?? '', $tx['reference'] ?? '', $tx['notes'] ?? '', $tx['method'] ?? '', $amount, (float)($tx['balance_after'] ?? 0), $tx['user_name'] ?? 'Sistema', ], null, "A{$row}"); $row++; } $dataEndRow = $row - 1; if ($dataEndRow >= $headerRow + 1) { $sheet->getStyle("A" . ($headerRow + 1) . ":I{$dataEndRow}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("G" . ($headerRow + 1) . ":H{$dataEndRow}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $sheet->setAutoFilter("A{$headerRow}:I{$dataEndRow}"); } $sheet->mergeCells("A{$row}:F{$row}"); $sheet->setCellValue("A{$row}", 'Totales de movimientos'); $sheet->setCellValue("G{$row}", $totalIngresos); $sheet->setCellValue("H{$row}", $totalEgresos > 0 ? -$totalEgresos : 0); $sheet->getStyle("A{$row}:I{$row}")->applyFromArray([ 'font' => ['bold' => true], 'borders' => ['top' => ['borderStyle' => Border::BORDER_THIN]], ]); $sheet->getStyle("G{$row}:H{$row}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $row++; foreach (range('A', 'I') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->freezePane('A' . ($headerRow + 1)); // Segunda hoja para transferencias $transferSheet = $spreadsheet->createSheet(); $transferSheet->setTitle('Transferencias'); $transferSheet->setCellValue('A1', 'Historial de transferencias de la cuenta'); $transferSheet->mergeCells('A1:H1'); $transferSheet->getStyle('A1')->getFont()->setBold(true)->setSize(14); $transferSheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $transferHeaders = [ 'Fecha', 'Tipo', 'Cuenta origen', 'Cuenta destino', 'Concepto', 'Referencia', 'Monto', 'Registrado por', ]; $transferSheet->fromArray($transferHeaders, null, 'A3'); $transferSheet->getStyle('A3:H3')->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]], ]); $tRow = 4; foreach ($transfers as $transfer) { $direction = ((int)$transfer['source_account_id'] === $id) ? 'Salida' : 'Entrada'; $transferSheet->fromArray([ format_datetime($transfer['transacted_at'] ?? ''), $direction, ($transfer['source_name'] ?? '') . ' (' . ($transfer['source_code'] ?? '') . ')', ($transfer['target_name'] ?? '') . ' (' . ($transfer['target_code'] ?? '') . ')', $transfer['concept'] ?? '', $transfer['reference'] ?? '', (float)($transfer['amount'] ?? 0), $transfer['user_name'] ?? 'Sistema', ], null, "A{$tRow}"); $tRow++; } if ($tRow > 4) { $transferSheet->getStyle("A4:H" . ($tRow - 1))->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $transferSheet->getStyle("G4:G" . ($tRow - 1)) ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $transferSheet->setAutoFilter("A3:H" . ($tRow - 1)); } foreach (range('A', 'H') as $col) { $transferSheet->getColumnDimension($col)->setAutoSize(true); } $transferSheet->freezePane('A4'); $filename = 'Cuenta_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)($account['code'] ?? $id)) . '_' . 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 exportExcel(): void { requireAuth(['ADMIN','CAJERO']); $filters = $this->collectAccountFilters(); $rows = $this->fetchAllAccounts($filters); $totalBalance = 0.0; foreach ($rows as $row) { $totalBalance += (float)($row['current_balance'] ?? 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('Plan de Cuentas'); $headerColor = '173e62'; $row = 1; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'SISCAPS - Plan de Cuentas'); $sheet->getStyle("A{$row}")->getFont()->setBold(true)->setSize(15); $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $row++; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'Generado: ' . date('d/m/Y')); $row++; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'Filtros: ' . $this->describeAccountFilters($filters)); $row += 2; $headerRow = $row; $headers = [ 'Código', 'Nombre', 'Categoría', 'Tipo', 'Moneda', 'Saldo apertura', 'Saldo actual', 'Banco', 'Nº cuenta', 'Estado', 'Actualizado', ]; $sheet->fromArray($headers, null, "A{$headerRow}"); $sheet->getStyle("A{$headerRow}:K{$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]], ]); $row = $headerRow + 1; foreach ($rows as $account) { $sheet->fromArray([ $account['code'] ?? '', $account['name'] ?? '', $account['category_name'] ?? '', $account['type'] ?? '', $account['currency'] ?? 'C$', (float)($account['opening_balance'] ?? 0), (float)($account['current_balance'] ?? 0), $account['bank_name'] ?? '', $account['account_number'] ?? '', ((int)($account['is_active'] ?? 1) === 1) ? 'Activa' : 'Inactiva', format_datetime($account['updated_at'] ?? ''), ], null, "A{$row}"); $row++; } $dataEndRow = $row - 1; if ($dataEndRow >= $headerRow + 1) { $sheet->getStyle("A" . ($headerRow + 1) . ":K{$dataEndRow}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("F" . ($headerRow + 1) . ":G{$dataEndRow}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $sheet->setAutoFilter("A{$headerRow}:K{$dataEndRow}"); } if ($dataEndRow >= $headerRow + 1) { $sheet->mergeCells("A{$row}:F{$row}"); $sheet->setCellValue("A{$row}", 'Total saldo actual'); $sheet->setCellValue("G{$row}", $totalBalance); $sheet->getStyle("A{$row}:K{$row}")->applyFromArray([ 'font' => ['bold' => true], 'borders' => ['top' => ['borderStyle' => Border::BORDER_THIN]], ]); $sheet->getStyle("G{$row}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $row++; } foreach (range('A', 'K') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->freezePane('A' . ($headerRow + 1)); $filename = 'Plan_de_Cuentas_' . 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; } private function renderAccountsPdfHtml(array $rows, array $filters): 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['type'])) { $applied[] = 'Tipo: ' . htmlspecialchars($filters['type']); } if ($filters['is_active'] !== '') { $applied[] = 'Estado: ' . ($filters['is_active'] === '1' ? 'Activas' : 'Inactivas'); } $filtersDesc = $this->describeAccountFilters($filters); $generatedAt = date('d/m/Y H:i'); $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); } } } ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Plan de Cuentas</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: 320px; 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 { padding:5px 6px; border-bottom:1px solid #eee; text-align:left; font-size: 9px; vertical-align: top; word-wrap: break-word; } thead th { background:#f8f9fa; border-bottom:1px solid #ddd; } .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">Plan de Cuentas</p> <div class="membrete-meta"><strong>Total cuentas:</strong> <?= (int)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 style="width:10%">Código</th> <th style="width:22%">Nombre</th> <th style="width:14%">Categoría</th> <th style="width:12%">Tipo</th> <th style="width:12%">Moneda</th> <th class="right" style="width:15%">Saldo Apertura</th> <th class="right" style="width:15%">Saldo Actual</th> </tr> </thead> <tbody> <?php if ($rows): foreach ($rows as $r): ?> <tr> <td><?= htmlspecialchars($r['code'] ?? '') ?></td> <td><?= htmlspecialchars($r['name'] ?? '') ?></td> <td><?= htmlspecialchars($r['category_name'] ?? '') ?></td> <td><?= htmlspecialchars($r['type'] ?? '') ?></td> <td><?= htmlspecialchars($r['currency'] ?? 'C$ ') ?></td> <td class="right"><?= format_currency($r['opening_balance'] ?? 0, (string)($r['currency'] ?? 'C$ ')) ?></td> <td class="right"><strong><?= format_currency($r['current_balance'] ?? 0, (string)($r['currency'] ?? 'C$ ')) ?></strong></td> </tr> <?php endforeach; else: ?> <tr><td colspan="7" class="muted">No hay cuentas registradas.</td></tr> <?php endif; ?> </tbody> </table> </body> </html> <?php return (string)ob_get_clean(); } public function downloadPdf(): void { requireAuth(['ADMIN','CAJERO']); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $account = Account::find($id); if (!$account) { http_response_code(404); echo 'Cuenta no encontrada'; return; } $transactions = AccountTransaction::recentForAccount($id, 100); $transfers = AccountTransfer::recentForAccount($id, 50); $totals = ['ingresos' => 0.0, 'egresos' => 0.0]; foreach ($transactions as $tx) { $amount = (float)($tx['amount'] ?? 0); if ($amount >= 0) { $totals['ingresos'] += $amount; } else { $totals['egresos'] += abs($amount); } } $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderAccountPdfHtml($account, $transactions, $transfers, $totals); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $filename = 'Cuenta_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)($account['code'] ?? $id)) . '_' . date('Ymd_His') . '.pdf'; $dompdf->stream($filename, ['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 renderAccountPdfHtml(array $account, array $transactions, array $transfers, array $totals): string { $generatedAt = date('d/m/Y H:i'); $currency = $account['currency'] ?? 'C$ '; $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); } } } ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Detalle de Cuenta</title> <style> * { box-sizing: border-box; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #222; margin: 12px; } h1 { font-size: 18px; margin: 0 0 6px 0; } h2 { font-size: 14px; margin: 14px 0 6px 0; } .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: 300px; 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 #ccc; padding: 4px 6px; font-size: 9px; } thead th { background: #f2f2f2; font-weight: bold; } .no-border td, .no-border th { border: none; } .summary-table td { font-size: 10px; } .muted { color: #666; font-size: 9px; } .text-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">Detalle de cuenta</p> <div class="membrete-meta"><strong>Cuenta:</strong> <?= htmlspecialchars(($account['code'] ?? '') !== '' ? (($account['code'] ?? '') . ' - ' . ($account['name'] ?? '')) : ($account['name'] ?? '')) ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Moneda:</strong> <?= htmlspecialchars($currency) ?></div> </td> </tr> </table> </div> <h1><?= htmlspecialchars($account['name'] ?? '') ?></h1> <table class="summary-table" style="margin-bottom: 12px;"> <tbody> <tr class="no-border"> <td><strong>Código:</strong> <?= htmlspecialchars($account['code'] ?? '') ?></td> <td><strong>Tipo:</strong> <?= htmlspecialchars($account['type'] ?? '') ?></td> <td><strong>Categoría:</strong> <?= htmlspecialchars($account['category_name'] ?? 'Sin categoría') ?></td> </tr> <tr class="no-border"> <td><strong>Moneda:</strong> <?= htmlspecialchars($currency) ?></td> <td><strong>Saldo apertura:</strong> <?= format_currency($account['opening_balance'] ?? 0, $currency) ?></td> <td><strong>Saldo actual:</strong> <?= format_currency($account['current_balance'] ?? 0, $currency) ?></td> </tr> <tr class="no-border"> <td><strong>Total ingresos (listados):</strong> <?= format_currency($totals['ingresos'], $currency) ?></td> <td><strong>Total egresos (listados):</strong> <?= format_currency($totals['egresos'], $currency) ?></td> <td><strong>Diferencia:</strong> <?= format_currency($totals['ingresos'] - $totals['egresos'], $currency) ?></td> </tr> <tr class="no-border"> <td><strong>Estado:</strong> <?= ((int)($account['is_active'] ?? 0) === 1) ? 'Activa' : 'Inactiva' ?></td> <td><strong>Titular/Responsable:</strong> <?= $account['holder'] ? htmlspecialchars($account['holder']) : '---' ?></td> <td><strong>Última actualización:</strong> <?= $account['updated_at'] ? htmlspecialchars(format_datetime($account['updated_at'])) : '---' ?></td> </tr> <?php if (($account['type'] ?? '') === 'Banco'): ?> <tr class="no-border"> <td><strong>Banco:</strong> <?= $account['bank_name'] ? htmlspecialchars($account['bank_name']) : '---' ?></td> <td colspan="2"><strong>Número de cuenta:</strong> <?= $account['account_number'] ? htmlspecialchars($account['account_number']) : '---' ?></td> </tr> <?php endif; ?> <tr class="no-border"> <td colspan="3"><strong>Notas:</strong> <?= $account['notes'] ? nl2br(htmlspecialchars($account['notes'])) : 'Sin notas adicionales' ?></td> </tr> </tbody> </table> <h2>Movimientos recientes</h2> <table> <thead> <tr> <th style="width:15%">Fecha</th> <th style="width:12%">Tipo</th> <th style="width:33%">Concepto</th> <th style="width:15%">Método</th> <th style="width:12%">Monto</th> <th style="width:13%">Saldo</th> </tr> </thead> <tbody> <?php if (!empty($transactions)): ?> <?php foreach ($transactions as $tx): ?> <tr> <td><?= htmlspecialchars(format_datetime($tx['transacted_at'] ?? '')) ?></td> <td><?= htmlspecialchars($tx['direction'] ?? '') ?></td> <td> <strong><?= htmlspecialchars($tx['concept'] ?? '') ?></strong> <?php if (!empty($tx['reference'])): ?><br><span class="muted">Ref: <?= htmlspecialchars($tx['reference']) ?></span><?php endif; ?> <?php if (!empty($tx['notes'])): ?><br><span class="muted"><?= htmlspecialchars($tx['notes']) ?></span><?php endif; ?> </td> <td><?= htmlspecialchars($tx['method'] ?? '') ?></td> <td class="text-right"><?= format_currency($tx['amount'] ?? 0, $currency) ?></td> <td class="text-right"><?= format_currency($tx['balance_after'] ?? 0, $currency) ?></td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="6" class="muted" style="text-align:center;">No hay movimientos registrados.</td></tr> <?php endif; ?> </tbody> </table> <h2>Transferencias recientes</h2> <table> <thead> <tr> <th style="width:18%">Fecha</th> <th style="width:24%">Desde</th> <th style="width:24%">Hacia</th> <th style="width:12%">Monto</th> <th style="width:22%">Concepto / Ref</th> </tr> </thead> <tbody> <?php if (!empty($transfers)): ?> <?php foreach ($transfers as $tr): ?> <tr> <td><?= htmlspecialchars(format_datetime($tr['transacted_at'] ?? '')) ?></td> <td><?= htmlspecialchars(($tr['source_name'] ?? '') . ' (' . ($tr['source_code'] ?? '') . ')') ?></td> <td><?= htmlspecialchars(($tr['target_name'] ?? '') . ' (' . ($tr['target_code'] ?? '') . ')') ?></td> <td class="text-right"><?= format_currency($tr['amount'] ?? 0, $currency) ?></td> <td> <strong><?= htmlspecialchars($tr['concept'] ?? '') ?></strong> <?php if (!empty($tr['reference'])): ?><br><span class="muted">Ref: <?= htmlspecialchars($tr['reference']) ?></span><?php endif; ?> <?php if (!empty($tr['notes'])): ?><br><span class="muted"><?= htmlspecialchars($tr['notes']) ?></span><?php endif; ?> </td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="5" class="muted" style="text-align:center;">Sin transferencias registradas.</td></tr> <?php endif; ?> </tbody> </table> <p class="muted" style="margin-top:12px;">Este reporte incluye los últimos registros asociados a la cuenta seleccionada.</p> </body> </html> <?php return (string)ob_get_clean(); } public function create(): void { requireAuth(['ADMIN']); $csrf = $this->ensureCsrf(); $categories = AccountCategory::all(); $types = ['Caja','Caja chica','Banco','Otro']; $account = [ 'category_id' => '', 'code' => '', 'name' => '', 'type' => 'Caja', 'currency' => 'C$ ', 'opening_balance' => 0, 'current_balance' => 0, 'bank_name' => '', 'account_number' => '', 'holder' => '', 'is_active' => 1, 'notes' => '', ]; require __DIR__ . '/../views/cuentas/create.php'; } public function store(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cuentas.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $code = trim($_POST['code'] ?? ''); $name = trim($_POST['name'] ?? ''); $type = $_POST['type'] ?? 'Caja'; $currency = trim($_POST['currency'] ?? 'C$ '); $category_id = (int)($_POST['category_id'] ?? 0) ?: null; $opening = (float)($_POST['opening_balance'] ?? 0); $bank = trim($_POST['bank_name'] ?? ''); $accNumber = trim($_POST['account_number'] ?? ''); $holder = trim($_POST['holder'] ?? ''); $notes = trim($_POST['notes'] ?? ''); $isActive = isset($_POST['is_active']) ? 1 : 0; if ($code === '' || $name === '') { setFlashMessage('error', 'Código y nombre son obligatorios.'); redirect('cuentas.create'); } if (!in_array($type, ['Caja','Caja chica','Banco','Otro'], true)) { $type = 'Caja'; } if ($opening < 0) { setFlashMessage('error', 'El saldo de apertura no puede ser negativo.'); redirect('cuentas.create'); } try { $id = Account::create([ 'category_id' => $category_id, 'code' => $code, 'name' => $name, 'type' => $type, 'currency' => $currency !== '' ? $currency : 'C$ ', 'opening_balance' => $opening, 'current_balance' => $opening, 'bank_name' => $bank, 'account_number' => $accNumber, 'holder' => $holder, 'is_active' => $isActive, 'notes' => $notes, ]); setFlashMessage('success', 'Cuenta creada correctamente.'); header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $id); exit; } catch (Throwable $e) { $msg = $e->getMessage(); if ($e instanceof PDOException && (int)$e->getCode() === 23000) { $msg = 'El código de cuenta ya existe.'; } setFlashMessage('error', 'No se pudo crear la cuenta: ' . $msg); redirect('cuentas.create'); } } public function show(): void { requireAuth(['ADMIN','CAJERO']); $csrf = $this->ensureCsrf(); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $account = Account::find($id); if (!$account) { http_response_code(404); echo 'Cuenta no encontrada'; return; } $categories = AccountCategory::all(); $types = ['Caja','Caja chica','Banco','Otro']; $transactions = AccountTransaction::recentForAccount($id, 30); $transfers = AccountTransfer::recentForAccount($id, 20); $otherAccounts = Account::allActive(); require __DIR__ . '/../views/cuentas/show.php'; } public function edit(): void { requireAuth(['ADMIN']); $csrf = $this->ensureCsrf(); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $account = Account::find($id); if (!$account) { http_response_code(404); echo 'Cuenta no encontrada'; return; } $categories = AccountCategory::all(); $types = ['Caja','Caja chica','Banco','Otro']; require __DIR__ . '/../views/cuentas/edit.php'; } public function update(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cuentas.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('cuentas.index'); } $account = Account::find($id); if (!$account) { setFlashMessage('error', 'Cuenta no encontrada.'); redirect('cuentas.index'); } $code = trim($_POST['code'] ?? ''); $name = trim($_POST['name'] ?? ''); $type = $_POST['type'] ?? 'Caja'; $currency = trim($_POST['currency'] ?? 'C$ '); $category_id = (int)($_POST['category_id'] ?? 0) ?: null; $opening = (float)($_POST['opening_balance'] ?? $account['opening_balance']); $bank = trim($_POST['bank_name'] ?? ''); $accNumber = trim($_POST['account_number'] ?? ''); $holder = trim($_POST['holder'] ?? ''); $notes = trim($_POST['notes'] ?? ''); $isActive = isset($_POST['is_active']) ? 1 : 0; if ($code === '' || $name === '') { setFlashMessage('error', 'Código y nombre son obligatorios.'); header('Location: ' . BASE_URL . '?route=cuentas.edit&id=' . $id); exit; } if (!in_array($type, ['Caja','Caja chica','Banco','Otro'], true)) { $type = 'Caja'; } if ($opening < 0) { setFlashMessage('error', 'El saldo de apertura no puede ser negativo.'); header('Location: ' . BASE_URL . '?route=cuentas.edit&id=' . $id); exit; } try { Account::update($id, [ 'category_id' => $category_id, 'code' => $code, 'name' => $name, 'type' => $type, 'currency' => $currency !== '' ? $currency : 'C$ ', 'opening_balance' => $opening, 'bank_name' => $bank, 'account_number' => $accNumber, 'holder' => $holder, 'is_active' => $isActive, 'notes' => $notes, ]); setFlashMessage('success', 'Cuenta actualizada correctamente.'); header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $id); exit; } catch (Throwable $e) { $msg = $e->getMessage(); if ($e instanceof PDOException && (int)$e->getCode() === 23000) { $msg = 'El código de cuenta ya existe.'; } setFlashMessage('error', 'No se pudo actualizar la cuenta: ' . $msg); header('Location: ' . BASE_URL . '?route=cuentas.edit&id=' . $id); exit; } } public function delete(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cuentas.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('cuentas.index'); } $account = Account::find($id); if (!$account) { setFlashMessage('error', 'Cuenta no encontrada.'); redirect('cuentas.index'); } $defaultId = Account::defaultCashAccountId(); if ($id === (int)$defaultId || (($account['name'] ?? '') === 'Caja')) { setFlashMessage('error', 'No se puede eliminar la cuenta "Caja" porque es la cuenta predeterminada para cierres de caja.'); redirect('cuentas.index'); } try { $ok = Account::delete($id); if ($ok) { setFlashMessage('success', 'Cuenta eliminada.'); } else { setFlashMessage('error', 'No se pudo eliminar la cuenta.'); } } catch (Throwable $e) { setFlashMessage('error', 'Error al eliminar la cuenta: ' . $e->getMessage()); } redirect('cuentas.index'); } public function storeTransaction(): void { requireAuth(['ADMIN','CAJERO']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cuentas.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $accountId = (int)($_POST['account_id'] ?? 0); if ($accountId <= 0) { redirect('cuentas.index'); } $account = Account::find($accountId); if (!$account) { setFlashMessage('error', 'Cuenta no encontrada.'); redirect('cuentas.index'); } $direction = trim($_POST['direction'] ?? ''); $concept = trim($_POST['concept'] ?? ''); $amount = (float)($_POST['amount'] ?? 0); $reference = trim($_POST['reference'] ?? ''); $method = trim($_POST['method'] ?? ''); $counterparty = trim($_POST['counterparty'] ?? ''); $notes = trim($_POST['notes'] ?? ''); $transacted_at = $_POST['transacted_at'] ?? date('Y-m-d H:i:s'); if ($concept === '' || $amount <= 0 || !in_array($direction, ['Ingreso','Egreso'], true)) { setFlashMessage('error', 'Completa concepto, monto y tipo de movimiento.'); header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $accountId); exit; } try { AccountTransaction::record( $accountId, $direction, $amount, $concept, $reference ?: null, $method ?: 'Efectivo', $counterparty ?: null, $notes ?: null, (int)($_SESSION['user']['id'] ?? 0), $transacted_at ?: null ); setFlashMessage('success', 'Movimiento registrado correctamente.'); } catch (Throwable $e) { setFlashMessage('error', 'No se pudo registrar el movimiento: ' . $e->getMessage()); } header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $accountId); exit; } public function storeTransfer(): void { requireAuth(['ADMIN','CAJERO']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cuentas.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $sourceId = (int)($_POST['source_account_id'] ?? 0); $targetId = (int)($_POST['target_account_id'] ?? 0); $amount = (float)($_POST['amount'] ?? 0); $concept = trim($_POST['concept'] ?? ''); $reference = trim($_POST['reference'] ?? ''); $notes = trim($_POST['notes'] ?? ''); $transacted_at = $_POST['transacted_at'] ?? date('Y-m-d H:i:s'); if ($sourceId <= 0 || $targetId <= 0 || $concept === '' || $amount <= 0) { setFlashMessage('error', 'Completa todos los campos de la transferencia.'); header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $sourceId); exit; } try { AccountTransfer::record( $sourceId, $targetId, $amount, $concept, $reference ?: null, $notes ?: null, (int)($_SESSION['user']['id'] ?? 0), $transacted_at ?: null ); setFlashMessage('success', 'Transferencia registrada con éxito.'); } catch (Throwable $e) { setFlashMessage('error', 'No se pudo registrar la transferencia: ' . $e->getMessage()); } header('Location: ' . BASE_URL . '?route=cuentas.show&id=' . $sourceId); exit; } }
Coded With 💗 by
0x6ick