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: CashClosuresController.php
<?php class CashClosuresController extends BaseController { private function ensureCsrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } private function normalizeDateTime(string $value, bool $endOfDay = false): string { $value = trim($value); if ($value === '') { return $endOfDay ? date('Y-m-d 23:59:59') : date('Y-m-d 00:00:00'); } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00'); } $ts = strtotime($value); if ($ts === false) { return $endOfDay ? date('Y-m-d 23:59:59') : date('Y-m-d 00:00:00'); } return date('Y-m-d H:i:s', $ts); } private function collectPeriodFilters(int $cashierId): array { $lastClosure = CashClosure::lastForCashier($cashierId); $defaultFrom = $lastClosure['period_end'] ?? date('Y-m-01 00:00:00'); $defaultTo = date('Y-m-d H:i:s'); $dateFrom = $_GET['date_from'] ?? substr($defaultFrom, 0, 10); $dateTo = $_GET['date_to'] ?? substr($defaultTo, 0, 10); $fromDateTime = $this->normalizeDateTime($dateFrom, false); $toDateTime = $this->normalizeDateTime($dateTo, true); if (strtotime($fromDateTime) > strtotime($toDateTime)) { [$fromDateTime, $toDateTime] = [$toDateTime, $fromDateTime]; [$dateFrom, $dateTo] = [$dateTo, $dateFrom]; } return [ 'date_from' => $dateFrom, 'date_to' => $dateTo, 'fromDateTime' => $fromDateTime, 'toDateTime' => $toDateTime, 'lastClosure' => $lastClosure, ]; } private function buildPreview(int $cashierId, string $fromDateTime, string $toDateTime): array { $payments = Payment::getUnclosedByCashier($cashierId, $fromDateTime, $toDateTime); $expenses = Expense::getUnclosedByUser($cashierId, $fromDateTime, $toDateTime); $totalIncome = 0.0; $totalOutflow = 0.0; foreach ($payments as $payment) { $totalIncome += (float)($payment['amount'] ?? 0); } foreach ($expenses as $expense) { $totalOutflow += (float)($expense['amount'] ?? 0); } $net = $totalIncome - $totalOutflow; $ledger = []; foreach ($payments as $p) { $ledger[] = [ 'direction' => 'Ingreso', 'date' => $p['payment_date'] ?? '', 'detail' => 'Recibo ' . ($p['receipt_number'] ?? ('#' . ($p['id'] ?? ''))) . ' · ' . ($p['customer_name'] ?? ''), 'amount' => (float)($p['amount'] ?? 0), ]; } foreach ($expenses as $e) { $ledger[] = [ 'direction' => 'Egreso', 'date' => $e['expense_date'] ?? '', 'detail' => ($e['category_name'] ?? 'Egreso') . ($e['description'] ? (' · ' . $e['description']) : ''), 'amount' => (float)($e['amount'] ?? 0), ]; } usort($ledger, static function ($a, $b) { $ta = strtotime($a['date'] ?? '') ?: 0; $tb = strtotime($b['date'] ?? '') ?: 0; if ($ta === $tb) { return strcmp($a['direction'], $b['direction']); } return $ta <=> $tb; }); return [ 'payments' => $payments, 'expenses' => $expenses, 'totalIncome' => $totalIncome, 'totalOutflow' => $totalOutflow, 'net' => $net, 'ledger' => $ledger, ]; } private function getDeliveryOptions(): array { return User::getAll(['role' => 'ADMIN', 'status' => '1']); } private function ensureClosureAccountConsistency(int $closureId, int $accountId, int $userId, PDO $pdo): void { $stmt = $pdo->prepare("SELECT p.id, p.amount, p.method, p.note, p.payment_date, p.account_id, p.account_transaction_id, i.invoice_number, c.name AS customer_name FROM payments p LEFT JOIN invoices i ON i.id = p.invoice_id LEFT JOIN customers c ON c.id = p.customer_id WHERE p.cash_closure_id = :closure ORDER BY p.payment_date ASC, p.id ASC"); $stmt->execute([':closure' => $closureId]); $payments = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; foreach ($payments as $p) { $pid = (int)($p['id'] ?? 0); if ($pid <= 0) { continue; } $currentAcc = (int)($p['account_id'] ?? 0); $txnId = (int)($p['account_transaction_id'] ?? 0); if ($currentAcc !== $accountId && $txnId > 0) { AccountTransaction::deleteCascade($txnId, $pdo); $txnId = 0; } if ($currentAcc !== $accountId || $txnId <= 0) { $invoiceNumber = $p['invoice_number'] ?? null; $concept = 'Cobro factura ' . ($invoiceNumber ?: ('#' . $pid)); $reference = $invoiceNumber ?: null; $counterparty = $p['customer_name'] ?? null; $notes = $p['note'] ?? null; $transactedAt = $p['payment_date'] ?? date('Y-m-d H:i:s'); $method = $p['method'] ?? 'Efectivo'; $ledger = AccountTransaction::record( $accountId, 'Ingreso', (float)($p['amount'] ?? 0), $concept, $reference, $method, $counterparty, $notes, $userId, $transactedAt, $pdo, $pid ); Payment::setAccountAndTransaction($pid, $accountId, (int)($ledger['id'] ?? 0), $pdo); } } $stmt = $pdo->prepare("SELECT e.id, e.amount, e.method, e.description, e.ref_number, e.vendor, e.expense_date, e.account_id, e.account_transaction_id FROM expenses e WHERE e.cash_closure_id = :closure ORDER BY e.expense_date ASC, e.id ASC"); $stmt->execute([':closure' => $closureId]); $expenses = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; foreach ($expenses as $e) { $eid = (int)($e['id'] ?? 0); if ($eid <= 0) { continue; } $currentAcc = (int)($e['account_id'] ?? 0); $txnId = (int)($e['account_transaction_id'] ?? 0); if ($currentAcc !== $accountId && $txnId > 0) { AccountTransaction::deleteCascade($txnId, $pdo); $txnId = 0; } if ($currentAcc !== $accountId || $txnId <= 0) { $ref = $e['ref_number'] ?? null; $concept = 'Egreso ' . ($ref ? $ref : ('#' . $eid)); $counterparty = $e['vendor'] ?? null; $notes = $e['description'] ?? null; $transactedAt = $e['expense_date'] ?? date('Y-m-d H:i:s'); $method = $e['method'] ?? 'Efectivo'; $ledger = AccountTransaction::record( $accountId, 'Egreso', (float)($e['amount'] ?? 0), $concept, $ref ?: null, $method, $counterparty, $notes, $userId, $transactedAt, $pdo, null, $eid ); Expense::setAccountTransaction($eid, $accountId, (int)($ledger['id'] ?? 0), $pdo); } } } public function index(): void { $currentUser = getCurrentUser(); if (!$currentUser) { redirect('login'); return; } $cashierId = (int)($currentUser['id'] ?? 0); $csrf = $this->ensureCsrf(); $filters = $this->collectPeriodFilters($cashierId); $preview = $this->buildPreview($cashierId, $filters['fromDateTime'], $filters['toDateTime']); $lastClosure = $filters['lastClosure']; $deliveryUsers = $this->getDeliveryOptions(); $recentClosures = CashClosure::listRecent($cashierId, 10); require __DIR__ . '/../views/caja/cierres.php'; } public function show(): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $closure = CashClosure::findById($id); if (!$closure) { http_response_code(404); echo 'Cierre no encontrado'; return; } $payments = Payment::getByClosure($id); $expenses = Expense::getByClosure($id); $pendingBatch = CashPendingBatch::findByClosure($id); $pendingMovements = $pendingBatch ? CashPendingMovement::forBatch((int)$pendingBatch['id']) : []; require __DIR__ . '/../views/caja/cierres_show.php'; } public function downloadPdf(): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $closure = CashClosure::findById($id); if (!$closure) { http_response_code(404); echo 'Cierre no encontrado'; return; } $payments = Payment::getByClosure($id); $expenses = Expense::getByClosure($id); $cashierId = (int)($closure['cashier_user_id'] ?? 0); $cashierRow = $cashierId > 0 ? User::findById($cashierId) : null; $cashierName = $cashierRow ? trim((string)($cashierRow['first_name'] ?? '') . ' ' . (string)($cashierRow['last_name'] ?? '')) : ''; if ($cashierName === '') { $cashierName = $cashierId > 0 ? ('Usuario #' . $cashierId) : '---'; } $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderClosurePdfHtml($closure, $payments, $expenses, $cashierName); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $num = (int)($closure['consecutive'] ?? $closure['id'] ?? $id); $filename = 'Cierre_Caja_' . $num . '_' . 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 renderClosurePdfHtml(array $closure, array $payments, array $expenses, string $cashierName): string { $generatedAt = date('d/m/Y H:i'); $num = (int)($closure['consecutive'] ?? $closure['id'] ?? 0); $period = format_period($closure['period_start'] ?? '', $closure['period_end'] ?? ''); $createdAt = $closure['created_at'] ?? ''; $totalIncome = (float)($closure['total_income'] ?? 0); $totalOutflow = (float)($closure['total_outflow'] ?? 0); $net = (float)($closure['net_amount'] ?? ($totalIncome - $totalOutflow)); $notes = $closure['notes'] ?? null; $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 Cierre</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: 13px; 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; vertical-align: top; } 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; } .text-center { text-align: center; } </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 cierre</p> <div class="membrete-meta"><strong>Cierre:</strong> #<?= (int)$num ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Período:</strong> <?= htmlspecialchars($period) ?></div> <div class="membrete-meta"><strong>Cajero:</strong> <?= htmlspecialchars($cashierName) ?></div> </td> </tr> </table> </div> <h1>Cierre #<?= (int)$num ?></h1> <table class="summary-table" style="margin-bottom: 12px;"> <tbody> <tr class="no-border"> <td><strong>Período:</strong> <?= htmlspecialchars($period) ?></td> <td><strong>Estado:</strong> <?= htmlspecialchars((string)($closure['status'] ?? '')) ?></td> <td><strong>Cajero:</strong> <?= htmlspecialchars($cashierName) ?></td> </tr> <tr class="no-border"> <td><strong>Ingresos:</strong> <?= format_currency($totalIncome) ?></td> <td><strong>Egresos:</strong> <?= format_currency($totalOutflow) ?></td> <td><strong>Neto:</strong> <?= format_currency($net) ?></td> </tr> <tr class="no-border"> <td><strong>Generado el:</strong> <?= $createdAt ? htmlspecialchars(format_datetime($createdAt)) : '---' ?></td> <td><strong>Total cobros:</strong> <?= (int)count($payments) ?></td> <td><strong>Total egresos:</strong> <?= (int)count($expenses) ?></td> </tr> <tr class="no-border"> <td colspan="3"><strong>Notas:</strong> <?= $notes ? nl2br(htmlspecialchars((string)$notes)) : 'Sin notas' ?></td> </tr> </tbody> </table> <h2>Cobros incluidos</h2> <table> <thead> <tr> <th style="width:14%">Recibo</th> <th style="width:28%">Cliente</th> <th style="width:18%">Fecha</th> <th style="width:18%">Método</th> <th style="width:22%" class="text-right">Monto</th> </tr> </thead> <tbody> <?php if (!empty($payments)): ?> <?php foreach ($payments as $p): ?> <tr> <td><?= htmlspecialchars($p['receipt_number'] ?? ('#' . ($p['id'] ?? ''))) ?></td> <td><?= htmlspecialchars($p['customer_name'] ?? '') ?></td> <td><?= htmlspecialchars(format_datetime($p['payment_date'] ?? '')) ?></td> <td><?= htmlspecialchars($p['method'] ?? '') ?></td> <td class="text-right"><?= format_currency($p['amount'] ?? 0) ?></td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="5" class="muted text-center">No se asociaron cobros a este cierre.</td></tr> <?php endif; ?> </tbody> </table> <h2>Egresos incluidos</h2> <table> <thead> <tr> <th style="width:18%">Fecha</th> <th style="width:22%">Categoría</th> <th style="width:38%">Detalle</th> <th style="width:12%">Método</th> <th style="width:10%" class="text-right">Monto</th> </tr> </thead> <tbody> <?php if (!empty($expenses)): ?> <?php foreach ($expenses as $e): ?> <?php $detail = $e['description'] ?? ''; if ($detail === '' && !empty($e['vendor'])) { $detail = (string)$e['vendor']; } $cat = $e['category_name'] ?? 'Egreso'; if (!empty($e['subcategory_name'])) { $cat .= ' / ' . $e['subcategory_name']; } ?> <tr> <td><?= htmlspecialchars(format_datetime($e['expense_date'] ?? '')) ?></td> <td><?= htmlspecialchars($cat) ?></td> <td><?= htmlspecialchars($detail) ?></td> <td><?= htmlspecialchars($e['method'] ?? '') ?></td> <td class="text-right"><?= format_currency($e['amount'] ?? 0) ?></td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="5" class="muted text-center">No hubo egresos en este cierre.</td></tr> <?php endif; ?> </tbody> </table> <p class="muted" style="margin-top:12px;">Este reporte incluye los movimientos asociados al cierre seleccionado.</p> </body> </html> <?php return (string)ob_get_clean(); } public function exportPdf(): void { $currentUser = getCurrentUser(); if (!$currentUser) { redirect('login'); return; } $cashierId = (int)($currentUser['id'] ?? 0); $role = getCurrentUserRole(); $isAdmin = $role === 'ADMIN'; $rows = $isAdmin ? CashClosure::listAllWithCashierName() : CashClosure::listForCashierAllWithCashierName($cashierId); $cashierName = trim((string)($currentUser['first_name'] ?? '') . ' ' . (string)($currentUser['last_name'] ?? '')); if ($cashierName === '') { $cashierName = $currentUser['username'] ?? ('Usuario #' . $cashierId); } $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $periodLabel = 'Todos los cierres'; $html = $this->renderClosuresReportPdfHtml($rows, $periodLabel, (string)$cashierName, $isAdmin); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $filename = 'Cierres_Caja_' . date('Ymd') . '.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 renderClosuresReportPdfHtml(array $rows, string $periodLabel, string $cashierName, bool $includeCashierColumn): string { $generatedAt = date('d/m/Y H:i'); $period = $periodLabel; $sumIncome = 0.0; $sumOutflow = 0.0; $sumNet = 0.0; foreach ($rows as $r) { $sumIncome += (float)($r['total_income'] ?? 0); $sumOutflow += (float)($r['total_outflow'] ?? 0); $sumNet += (float)($r['net_amount'] ?? 0); } $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>Reporte de Cierres de Caja</title> <style> * { box-sizing: border-box; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #222; margin: 12px; } h1 { font-size: 16px; margin: 0 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: 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 { border: 1px solid #ccc; padding: 4px 6px; font-size: 9px; vertical-align: top; } thead th { background: #f2f2f2; font-weight: bold; } .no-border td, .no-border th { border: none; } .summary td { font-size: 10px; } .text-right { text-align: right; } .text-center { text-align: center; } .muted { color: #666; font-size: 9px; } </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">Cierres de Caja</p> <div class="membrete-meta"><strong>Total cierres:</strong> <?= (int)count($rows) ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Período:</strong> <?= htmlspecialchars($period) ?></div> <div class="membrete-meta"><strong>Cajero:</strong> <?= htmlspecialchars($cashierName) ?></div> </td> </tr> </table> </div> <h1>Reporte de cierres</h1> <table class="summary" style="margin-bottom:10px;"> <tbody> <tr class="no-border"> <td><strong>Cajero:</strong> <?= htmlspecialchars($cashierName) ?></td> <td><strong>Período:</strong> <?= htmlspecialchars($period) ?></td> <td class="text-right"><strong>Total cierres:</strong> <?= (int)count($rows) ?></td> </tr> <tr class="no-border"> <td><strong>Total ingresos:</strong> <?= format_currency($sumIncome) ?></td> <td><strong>Total egresos:</strong> <?= format_currency($sumOutflow) ?></td> <td class="text-right"><strong>Total neto:</strong> <?= format_currency($sumNet) ?></td> </tr> </tbody> </table> <table> <thead> <tr> <th style="width:8%">#</th> <?php if ($includeCashierColumn): ?> <th style="width:16%">Cajero</th> <?php endif; ?> <th style="width:20%">Período</th> <th style="width:12%">Estado</th> <th style="width:15%" class="text-right">Ingresos</th> <th style="width:15%" class="text-right">Egresos</th> <th style="width:15%" class="text-right">Neto</th> <th style="width:15%">Generado</th> </tr> </thead> <tbody> <?php if (!empty($rows)): ?> <?php foreach ($rows as $c): ?> <tr> <td class="text-center">#<?= (int)($c['consecutive'] ?? $c['id'] ?? 0) ?></td> <?php if ($includeCashierColumn): ?> <td><?= htmlspecialchars(trim((string)($c['cashier_name'] ?? '')) ?: ('Usuario #' . (int)($c['cashier_user_id'] ?? 0))) ?></td> <?php endif; ?> <td><?= htmlspecialchars(format_period($c['period_start'] ?? '', $c['period_end'] ?? '')) ?></td> <td><?= htmlspecialchars($c['status'] ?? '') ?></td> <td class="text-right"><?= format_currency($c['total_income'] ?? 0) ?></td> <td class="text-right"><?= format_currency($c['total_outflow'] ?? 0) ?></td> <td class="text-right"><?= format_currency($c['net_amount'] ?? 0) ?></td> <td><?= !empty($c['created_at']) ? htmlspecialchars(format_datetime($c['created_at'])) : '---' ?></td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="<?= $includeCashierColumn ? 8 : 7 ?>" class="muted text-center">No hay cierres registrados.</td></tr> <?php endif; ?> </tbody> </table> <p class="muted" style="margin-top:10px;">Este reporte incluye todos los cierres registrados.</p> </body> </html> <?php return (string)ob_get_clean(); } public function store(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('cashclosures.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $currentUser = getCurrentUser(); if (!$currentUser) { redirect('login'); return; } $cashierId = (int)($currentUser['id'] ?? 0); $periodFrom = $this->normalizeDateTime((string)($_POST['period_from'] ?? ''), false); $periodTo = $this->normalizeDateTime((string)($_POST['period_to'] ?? ''), true); $notes = trim((string)($_POST['notes'] ?? '')); $deliveryUser = (int)($_POST['delivery_user_id'] ?? 0) ?: null; if (strtotime($periodFrom) > strtotime($periodTo)) { setFlashMessage('error', 'El rango de fechas es inválido.'); redirect('cashclosures.index'); } $preview = $this->buildPreview($cashierId, $periodFrom, $periodTo); $totalMovements = count($preview['payments']) + count($preview['expenses']); if ($totalMovements === 0) { setFlashMessage('error', 'No hay movimientos para cerrar en el período seleccionado.'); redirect('cashclosures.index'); } $pdo = (new Database())->getConnection(); try { $pdo->beginTransaction(); $cashAccountId = Account::defaultCashAccountId($pdo); $closureId = CashClosure::create([ 'cashier_user_id' => $cashierId, 'delivery_user_id' => $deliveryUser, 'period_start' => $periodFrom, 'period_end' => $periodTo, 'total_income' => $preview['totalIncome'], 'total_outflow' => $preview['totalOutflow'], 'net_amount' => $preview['net'], 'delivered_amount' => $preview['net'], 'pending_amount' => 0, 'status' => 'GENERATED', 'notes' => $notes !== '' ? $notes : null, ], $pdo); Payment::assignClosureInRange($cashierId, $periodFrom, $periodTo, $closureId, $pdo); Expense::assignClosureInRange($cashierId, $periodFrom, $periodTo, $closureId, $pdo); $this->ensureClosureAccountConsistency($closureId, $cashAccountId, $cashierId, $pdo); $pdo->commit(); setFlashMessage('success', 'Cierre #' . $closureId . ' generado correctamente.'); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } setFlashMessage('error', 'No se pudo generar el cierre: ' . $e->getMessage()); } redirect('cashclosures.index'); } }
Coded With 💗 by
0x6ick