false, 'message' => 'Query requerido']); exit; } $query = $_GET['q']; $results = House::search($query); echo json_encode(['success' => true, 'results' => $results]); exit; break; case 'activity_logs': header('Content-Type: application/json'); Auth::requireAdmin(); if (isset($_GET['action']) && $_GET['action'] == 'clear') { if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } ActivityLog::deleteAll(); Auth::logActivity('clear_history', 'Historial de actividad eliminado'); echo json_encode(['success' => true, 'message' => 'Historial eliminado']); exit; } break; case 'house_actions': header('Content-Type: application/json'); Auth::requireAdmin(); if (isset($_GET['action']) && $_GET['action'] == 'save') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!$input || !isset($input['id'])) { echo json_encode(['success' => false, 'message' => 'Datos inválidos']); exit; } if (House::update($input['id'], $input)) { $house = House::findById($input['id']); Auth::logActivity('update_house', "Casa {$house['number']} actualizada"); echo json_encode(['success' => true, 'message' => 'Casa actualizada']); } else { echo json_encode(['success' => false, 'message' => 'Error al actualizar']); } exit; } break; case 'config_actions': header('Content-Type: application/json'); // Permitir acceso a capturistas para ver gráficos if (!Auth::isCapturist()) { echo json_encode(['success' => false, 'message' => 'Acceso denegado']); exit; } if (isset($_GET['action']) && $_GET['action'] == 'get_monthly_data') { $year = $_GET['year'] ?? date('Y'); $monthlyBills = MonthlyBill::getYear($year); $configured = []; $realAmounts = []; $months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; foreach ($months as $month) { $bill = $monthlyBills[$month] ?? null; $configured[$month] = $bill['total_amount'] ?? 0; $realAmounts[$month] = $bill['real_amount'] ?? 0; } echo json_encode([ 'success' => true, 'configured' => $configured, 'realAmounts' => $realAmounts ]); exit; } elseif (isset($_GET['action']) && $_GET['action'] == 'get_payments_data') { $year = $_GET['year'] ?? date('Y'); $matrix = Payment::getMatrix($year); $months = $matrix['months']; $monthTotals = []; foreach ($months as $month) { $monthTotals[$month] = 0; foreach ($matrix['payments'][$month] ?? [] as $houseId => $paymentData) { $monthTotals[$month] += $paymentData['amount'] ?? 0; } } echo json_encode([ 'success' => true, 'payments' => $monthTotals, 'months' => $months ]); exit; } if (isset($_GET['action']) && $_GET['action'] == 'save_monthly_bill') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); $id = MonthlyBill::save($input); if ($id) { // Actualizar montos esperados si aplica logicamente (MonthlyBill::updatePayments no returna nada util pero ejecuta logica) MonthlyBill::updatePayments($input['year'], $input['month']); Auth::logActivity('update_config', "Configuración actualizada: {$input['month']} {$input['year']}"); echo json_encode(['success' => true, 'message' => 'Configuración guardada']); } else { echo json_encode(['success' => false, 'message' => 'Error al guardar']); } exit; } break; case 'import_actions': header('Content-Type: application/json'); Auth::requireAdmin(); if (isset($_GET['action'])) { if ($_GET['action'] == 'history') { $logs = ActivityLog::getByAction('import_data', 20); echo json_encode(['success' => true, 'data' => $logs]); exit; } if ($_GET['action'] == 'import') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { echo json_encode(['success' => false, 'message' => 'Error al subir archivo']); exit; } $type = $_POST['type'] ?? ''; $file = $_FILES['file']['tmp_name']; $handle = fopen($file, "r"); if ($handle === FALSE) { echo json_encode(['success' => false, 'message' => 'No se pudo abrir el archivo']); exit; } $header = fgetcsv($handle, 1000, ","); $count = 0; $errors = 0; try { while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { if (count($data) < 2) continue; // Skip empty lines // Map CSV columns based on type // This logic should be expanded based on CSV structure for each type // For now, implementing basic House import as example and placeholder for others // Real implementation would need specific CSV mapping logic here // Since full implementation of all importers inside this switch might be huge, // we'd typically delegation to a model method, but for now we follow the pattern. $success = false; if ($type == 'houses') { // number,status,consumption_only,owner_name,owner_email,owner_phone // Check if house exists $number = $data[0]; $house = House::findByNumber($number); if ($house) { House::update($house['id'], [ 'status' => $data[1], 'consumption_only' => $data[2], 'owner_name' => $data[3], 'owner_email' => $data[4], 'owner_phone' => $data[5] ]); $success = true; } } elseif ($type == 'payments') { // year,house_number,month,amount,payment_date,payment_method,notes // Simplified logic $year = $data[0]; $houseNumber = $data[1]; $month = $data[2]; $amount = (float)$data[3]; $notes = $data[6] ?? ''; $house = House::findByNumber($houseNumber); if ($house) { Payment::update($house['id'], $year, $month, $amount, Auth::id(), $notes); $success = true; } } if ($success) $count++; else $errors++; } fclose($handle); Auth::logActivity('import_data', "Importación $type: $count registros procesados"); echo json_encode(['success' => true, 'message' => "Importación completada. $count registros exitosos."]); } catch (Exception $e) { echo json_encode(['success' => false, 'message' => 'Error procesando CSV: ' . $e->getMessage()]); } exit; } } break; case 'pagos_actions': header('Content-Type: application/json'); Auth::requireCapturist(); // Admin o Capturista $userId = Auth::id(); if (isset($_GET['action']) && $_GET['action'] == 'save_batch') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!$input || empty($input['changes'])) { echo json_encode(['success' => false, 'message' => 'No hay cambios para guardar']); exit; } $count = 0; foreach ($input['changes'] as $change) { // Validar datos mínimos if (!isset($change['house_id'], $change['year'], $change['month'])) { continue; } $amount = isset($change['amount']) ? (float)$change['amount'] : 0; // Usar el modelo Payment para actualizar Payment::update( $change['house_id'], $change['year'], $change['month'], $amount, $userId ); // Registrar actividad individual por cada cambio $details = "Pago actualizado: Casa {$change['house_number']} - {$change['month']} {$change['year']} - $" . number_format($amount, 2); Auth::logActivity('save_payment', $details); $count++; } if ($count > 0) { echo json_encode(['success' => true, 'message' => "Se guardaron $count cambios exitosamente."]); } else { echo json_encode(['success' => false, 'message' => 'No se procesó ningún cambio válido.']); } exit; } echo json_encode(['success' => false, 'message' => 'Acción no válida']); exit; break; case 'pagos': $matrix = Payment::getMatrix($year); $monthlyBills = MonthlyBill::getYear($year); $accessibleHouseIds = Auth::getAccessibleHouseIds(); $houses = array_filter($matrix['houses'], function($h) use ($accessibleHouseIds) { return in_array($h['id'], $accessibleHouseIds); }); $payments = []; foreach ($matrix['payments'] as $month => $monthPayments) { $payments[$month] = []; foreach ($monthPayments as $houseId => $paymentData) { if (in_array($houseId, $accessibleHouseIds)) { $payments[$month][$houseId] = $paymentData; } } } $months = $matrix['months']; if (isset($_GET['action']) && $_GET['action'] == 'export_pdf') { date_default_timezone_set('America/Mexico_City'); require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/tecnickcom/tcpdf/tcpdf.php'; $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); $pdf->SetCreator(PDF_CREATOR); $pdf->SetAuthor('Ibiza Condominium'); $pdf->SetTitle('Reporte de Pagos de Agua ' . $year); $pdf->SetSubject('Pagos de Agua'); $pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, 'Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Pagos de Agua ' . $year, 'Generado el ' . date('d/m/Y H:i')); $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); if (@file_exists(dirname(__FILE__).'/lang/eng.php')) { require_once(dirname(__FILE__).'/lang/eng.php'); $pdf->setLanguageArray($l); } $pdf->SetFont('helvetica', '', 9); $pdf->AddPage(); $selectedMonths = $_GET['months'] ?? []; if (!empty($selectedMonths)) { $filteredMonths = []; foreach($months as $m) { if (in_array($m, $selectedMonths)) { $filteredMonths[] = $m; } } $months = $filteredMonths; } ob_start(); include __DIR__ . '/views/payments/pdf_template.php'; $html = ob_get_clean(); $pdf->writeHTML($html, true, false, true, false, ''); $monthCount = count($months); $allMonths = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; if ($monthCount == 12) { $filename = 'Pagos_IBIZA_' . $year . '.pdf'; } elseif ($monthCount == 1) { $filename = 'Pagos_IBIZA_' . $months[0] . '_' . $year . '.pdf'; } else { $monthNames = implode('_', $months); $filename = 'Pagos_IBIZA_' . $monthNames . '_' . $year . '.pdf'; } $pdf->Output($filename, 'D'); exit; } $view = 'payments/index'; break; case 'casas': $houses = House::getAccessible(); $view = 'houses/index'; break; case 'house_view': $houseId = $_GET['id'] ?? 0; $accessibleHouseIds = Auth::getAccessibleHouseIds(); if (!in_array($houseId, $accessibleHouseIds) && !Auth::isAdmin()) { header('Location: /dashboard.php?page=casas'); exit; } $house = House::findById($houseId); $waterPayments = Payment::getByHouse($houseId); $totalWaterPayments = array_sum(array_column($waterPayments, 'amount')); $conceptPayments = CollectionPayment::getByHouse($houseId); $totalConceptPayments = array_sum(array_column($conceptPayments, 'amount')); $activityLogs = ActivityLog::all(50); $view = 'houses/view'; break; case 'finanzas': // Determinar si es una solicitud de "API" (GET para datos, POST para guardar/eliminar) if (isset($_GET['action'])) { header('Content-Type: application/json'); $userId = Auth::id(); // Obtener el ID del usuario actual switch ($_GET['action']) { // Conceptos case 'get_concept': $conceptId = $_GET['id'] ?? 0; $concept = CollectionConcept::findById($conceptId); if ($concept) { echo json_encode(['success' => true, 'data' => $concept]); } else { echo json_encode(['success' => false, 'message' => 'Concepto no encontrado']); } exit; case 'save_concept': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { // Validaciones básicas if (empty($input['name']) || empty($input['amount_per_house']) || empty($input['concept_date'])) { echo json_encode(['success' => false, 'message' => 'Faltan campos requeridos: nombre, monto por casa, fecha.']); exit; } if (!is_numeric($input['amount_per_house']) || $input['amount_per_house'] <= 0) { echo json_encode(['success' => false, 'message' => 'Monto por casa debe ser un número positivo.']); exit; } if (isset($input['total_amount']) && !is_numeric($input['total_amount'])) { echo json_encode(['success' => false, 'message' => 'Monto total debe ser un número.']); exit; } $result = CollectionConcept::save($input, $userId); if ($result) { Auth::logActivity('save_concept', 'Concepto ' . ($input['id'] ? 'editado' : 'creado') . ': ' . $input['name']); echo json_encode(['success' => true, 'message' => 'Concepto guardado exitosamente', 'id' => $result]); } else { echo json_encode(['success' => false, 'message' => 'Error al guardar concepto']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos']); } exit; case 'delete_concept': if (!Auth::isAdmin()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado: solo administradores pueden eliminar conceptos.']); exit; } $conceptId = $_GET['id'] ?? 0; if (!$conceptId) { echo json_encode(['success' => false, 'message' => 'ID de concepto no proporcionado']); exit; } $result = CollectionConcept::delete($conceptId); if ($result) { Auth::logActivity('delete_concept', 'Concepto eliminado: ID ' . $conceptId); echo json_encode(['success' => true, 'message' => 'Concepto eliminado exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al eliminar concepto']); } exit; // Gastos case 'get_expense': $expenseId = $_GET['id'] ?? 0; $expense = Expense::findById($expenseId); if ($expense) { $allocations = Expense::getConcepts($expenseId); // Obtener asignaciones $expense['allocations'] = $allocations; // Añadir asignaciones al gasto echo json_encode(['success' => true, 'data' => $expense]); } else { echo json_encode(['success' => false, 'message' => 'Gasto no encontrado']); } exit; case 'save_expense': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } // Datos del formulario $data = $_POST; $receiptFile = $_FILES['receipt'] ?? null; $allocations = json_decode($data['allocations'] ?? '[]', true); // Validaciones básicas if (empty($data['description']) || empty($data['amount']) || empty($data['expense_date'])) { echo json_encode(['success' => false, 'message' => 'Faltan campos requeridos: descripción, monto, fecha.']); exit; } if (!is_numeric($data['amount']) || $data['amount'] <= 0) { echo json_encode(['success' => false, 'message' => 'Monto debe ser un número positivo.']); exit; } $result = Expense::save($data, $userId, $receiptFile, $allocations); if ($result) { Auth::logActivity('save_expense', 'Gasto ' . ($data['id'] ? 'editado' : 'creado') . ': ' . $data['description']); echo json_encode(['success' => true, 'message' => 'Gasto guardado exitosamente', 'id' => $result]); } else { echo json_encode(['success' => false, 'message' => 'Error al guardar gasto. Verifique los logs.']); } exit; case 'delete_expense': if (!Auth::isAdmin()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado: solo administradores pueden eliminar gastos.']); exit; } $expenseId = $_GET['id'] ?? 0; if (!$expenseId) { echo json_encode(['success' => false, 'message' => 'ID de gasto no proporcionado']); exit; } $result = Expense::delete($expenseId); if ($result) { Auth::logActivity('delete_expense', 'Gasto eliminado: ID ' . $expenseId); echo json_encode(['success' => true, 'message' => 'Gasto eliminado exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al eliminar gasto']); } exit; default: echo json_encode(['success' => false, 'message' => 'Acción de finanzas no válida']); exit; } } // Lógica original para mostrar la vista (si no es una acción de "API") $expenses = Expense::all(); $concepts = CollectionConcept::all(true); $view = 'finance/index'; break; case 'concept_view': $conceptId = $_GET['id'] ?? 0; $concept = CollectionConcept::findById($conceptId); if (!$concept) { die('Error: Concepto no encontrado.'); } $status = CollectionConcept::getCollectionStatus($conceptId); $payments = CollectionConcept::getPaymentsByConcept($conceptId); $view = 'finance/concept_view'; break; case 'reportes': $reportType = $_GET['type'] ?? 'general'; $balance = Report::getGeneralBalance(); $expensesByCategory = Report::getExpensesByCategory(); $accessibleHouseIds = Auth::getAccessibleHouseIds(); $filterYear = $_GET['filter_year'] ?? null; $conceptDetails = Report::getConceptDetailsByYear($filterYear); if ($reportType == 'water-debtors') { $filters = [ 'year' => $_GET['filter_year'] ?? null, 'months' => $_GET['filter_months'] ?? null, 'house_id' => $_GET['filter_house'] ?? null, 'accessible_house_ids' => $accessibleHouseIds ]; if ($filters['months'] && !is_array($filters['months'])) { $filters['months'] = explode(',', $filters['months']); } $waterDebtors = Report::getWaterDebtors($filters); } elseif ($reportType == 'concept-debtors') { // Procesar filtros de casas y conceptos $houseFilters = $_GET['filter_houses'] ?? []; $conceptFilters = $_GET['filter_concepts'] ?? []; // Determinar casas a filtrar if (empty($houseFilters) || in_array('all', $houseFilters)) { $filteredHouses = $accessibleHouseIds; } else { // Filtrar solo las casas específicamente seleccionadas $filteredHouses = array_filter($houseFilters, function($houseId) use ($accessibleHouseIds) { return $houseId !== 'all' && in_array($houseId, $accessibleHouseIds); }); } // Determinar conceptos a filtrar if (empty($conceptFilters) || in_array('all', $conceptFilters)) { $filteredConcepts = null; // Todos los conceptos } else { // Filtrar solo los conceptos específicamente seleccionados $filteredConcepts = array_filter($conceptFilters, function($conceptId) { return $conceptId !== 'all'; }); } $conceptDebtors = Report::getConceptDebtorsFiltered($filteredHouses, $filteredConcepts); } $view = 'reports/index'; break; case 'configurar': Auth::requireAdmin(); $months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; $monthlyBills = MonthlyBill::getYear($year); $view = 'configurar/index'; break; case 'usuarios': Auth::requireAdmin(); $view = 'users/index'; break; case 'graficos': $year = $_GET['year'] ?? date('Y'); $matrix = Payment::getMatrix($year); $monthlyBills = MonthlyBill::getYear($year); $months = $matrix['months']; $monthTotals = []; foreach ($months as $month) { $monthTotals[$month] = 0; foreach ($matrix['payments'][$month] ?? [] as $houseId => $paymentData) { $monthTotals[$month] += $paymentData['amount'] ?? 0; } } $view = 'charts/index'; break; case 'importar': Auth::requireAdmin(); $concepts = CollectionConcept::all(true); $view = 'import/index'; break; case 'concept_view_actions': // Nuevo case para acciones AJAX de concept_view if (isset($_GET['action'])) { header('Content-Type: application/json'); $userId = Auth::id(); // Obtener el ID del usuario actual switch ($_GET['action']) { case 'initialize_concept_payments': $conceptId = $_GET['concept_id'] ?? 0; if (!$conceptId) { echo json_encode(['success' => false, 'message' => 'ID de concepto no proporcionado']); exit; } if (!Auth::isCapturist()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado']); exit; } // Se requiere el modelo House para CollectionPayment::initializePayments require_once __DIR__ . '/models/House.php'; $result = CollectionPayment::initializePayments($conceptId, $userId); if ($result) { Auth::logActivity('initialize_concept_payments', 'Pagos de concepto inicializados: ID ' . $conceptId); echo json_encode(['success' => true, 'message' => 'Pagos inicializados exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al inicializar pagos']); } exit; case 'save_concept_payment': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { $conceptId = $input['concept_id'] ?? 0; $houseId = $input['house_id'] ?? 0; $amount = $input['amount'] ?? 0; $paymentDate = $input['payment_date'] ?? null; if (!$conceptId || !$houseId || !is_numeric($amount)) { echo json_encode(['success' => false, 'message' => 'Datos de pago incompletos o inválidos']); exit; } if (!Auth::isCapturist()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado']); exit; } $result = CollectionPayment::update($conceptId, $houseId, $amount, $userId, 'Pago actualizado', $paymentDate); if ($result) { Auth::logActivity('save_concept_payment', 'Pago de concepto guardado: Concepto ' . $conceptId . ', Casa ' . $houseId . ', Monto ' . $amount); echo json_encode(['success' => true, 'message' => 'Pago guardado exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al guardar pago']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos']); } exit; default: echo json_encode(['success' => false, 'message' => 'Acción no válida para la vista de concepto']); exit; } } break; case 'charts_export': // Exportación de gráficos a PDF usando TCPDF (igual que otras exportaciones) date_default_timezone_set('America/Mexico_City'); // Obtener datos del POST $input = json_decode(file_get_contents('php://input'), true); $year = $input['year'] ?? date('Y'); $chartImages = $input['charts'] ?? []; $chartData = $input['data'] ?? []; require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/tecnickcom/tcpdf/tcpdf.php'; $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); $pdf->SetCreator(PDF_CREATOR); $pdf->SetAuthor('Ibiza Condominium'); $pdf->SetTitle('Gráficos de Pagos de Agua - IBIZA ' . $year); $pdf->SetSubject('Análisis Gráfico de Pagos'); $pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, 'Condominio IBIZA-Cto Sierra Morena 152 - Gráficos de Pagos de Agua ' . $year, 'Generado el ' . date('d/m/Y H:i')); $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); if (@file_exists(dirname(__FILE__).'/lang/eng.php')) { require_once(dirname(__FILE__).'/lang/eng.php'); $pdf->setLanguageArray($l); } $pdf->SetFont('helvetica', '', 12); $pdf->AddPage(); // Título principal $pdf->SetFont('helvetica', 'B', 18); $pdf->Cell(0, 15, 'Gráficos de Pagos de Agua - IBIZA', 0, 1, 'C'); $pdf->SetFont('helvetica', '', 10); $pdf->Cell(0, 8, 'Año: ' . $year . ' - Generado: ' . date('d/m/Y H:i'), 0, 1, 'C'); $pdf->Ln(10); // Función para agregar gráfico function addChartToPDF($pdf, $imageData, $title, $description = '') { if ($imageData) { $pdf->SetFont('helvetica', 'B', 14); $pdf->Cell(0, 10, $title, 0, 1, 'L'); if ($description) { $pdf->SetFont('helvetica', 'I', 10); $pdf->Cell(0, 6, $description, 0, 1, 'L'); } // Agregar imagen del gráfico $pdf->Image('@' . base64_decode(preg_replace('#^data:image/[^;]+;base64,#', '', $imageData)), 15, $pdf->GetY(), 180, 80, 'PNG'); $pdf->Ln(90); } } // Página 1: Gráficos principales $pdf->SetFont('helvetica', 'B', 16); $pdf->Cell(0, 12, 'Análisis Comparativo de Pagos', 0, 1, 'L'); $pdf->Ln(5); if (isset($chartImages['comparisonChart'])) { addChartToPDF($pdf, $chartImages['comparisonChart'], '1. Comparación: Pagos Reales vs Monto Configurado', 'Este gráfico compara los pagos efectivamente recibidos con los montos que fueron configurados para cada mes.'); } if (isset($chartImages['trendsChart'])) { addChartToPDF($pdf, $chartImages['trendsChart'], '2. Tendencias de Pagos a lo Largo del Año', 'Visualiza la evolución mensual de los ingresos por concepto de agua.'); } if (isset($chartImages['realAmountChart'])) { addChartToPDF($pdf, $chartImages['realAmountChart'], '3. Comparación: Monto Real del Recibo vs Pagos Efectivos', 'Compara el costo real del servicio con lo que efectivamente se cobra y paga.'); } // Nueva página para resumen ejecutivo $pdf->AddPage(); $pdf->SetFont('helvetica', 'B', 16); $pdf->Cell(0, 12, 'Resumen Ejecutivo', 0, 1, 'L'); $pdf->Ln(5); if (isset($chartImages['summaryChart'])) { addChartToPDF($pdf, $chartImages['summaryChart'], 'Distribución General de Cobranza', 'Vista panorámica del estado de cobranza con métricas clave.'); } // Estadísticas textuales $pdf->SetFont('helvetica', 'B', 12); $pdf->Cell(0, 10, 'Métricas de Rendimiento', 0, 1, 'L'); $pdf->Ln(5); $pdf->SetFont('helvetica', '', 11); $totalPayments = array_sum($chartData['payments'] ?? []); $totalConfigured = array_sum($chartData['configured'] ?? []); $totalReal = array_sum($chartData['realAmounts'] ?? []); $pdf->Cell(80, 8, 'Total Pagos Recibidos:', 0, 0, 'L'); $pdf->Cell(40, 8, '$ ' . number_format($totalPayments, 2), 0, 1, 'R'); $pdf->Cell(80, 8, 'Monto Total Configurado:', 0, 0, 'L'); $pdf->Cell(40, 8, '$ ' . number_format($totalConfigured, 2), 0, 1, 'R'); $pdf->Cell(80, 8, 'Monto Real Total:', 0, 0, 'L'); $pdf->Cell(40, 8, '$ ' . number_format($totalReal, 2), 0, 1, 'R'); if ($totalConfigured > 0) { $efficiency = round(($totalPayments / $totalConfigured) * 100, 1); $pdf->Cell(80, 8, 'Eficiencia de Cobranza:', 0, 0, 'L'); $pdf->Cell(40, 8, $efficiency . '%', 0, 1, 'R'); } // Salida del PDF $pdf->Output('Graficos_Pagos_Agua_IBIZA_' . $year . '_' . date('Y-m-d') . '.pdf', 'D'); exit; case 'reportes_actions': // Nuevo case para acciones de exportación de reportes date_default_timezone_set('America/Mexico_City'); // Asegurar zona horaria if (isset($_GET['action'])) { switch ($_GET['action']) { case 'export_pdf_report': // Lógica para exportar PDF $reportType = $_GET['type'] ?? 'general'; $year = $_GET['year'] ?? date('Y'); // Mapeo de tipos de reporte a nombres en español $reportNames = [ 'balance' => 'Balance_General', 'expenses' => 'Gastos_por_Categoria', 'water-debtors' => 'Deudores_de_Agua', 'concept-debtors' => 'Deudores_de_Conceptos', 'concepts' => 'Conceptos_Especiales' ]; $reportName = $reportNames[$reportType] ?? ucfirst($reportType); // Para conceptos, definir nombre personalizado del archivo PDF $pdfFilename = $reportName . '_IBIZA_' . $year . '.pdf'; // Incluir TCPDF require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/tecnickcom/tcpdf/tcpdf.php'; $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); $pdf->SetCreator(PDF_CREATOR); $pdf->SetAuthor('Ibiza Condominium'); $pdf->SetTitle("Condominio IBIZA-Cto Sierra Morena 152 - " . $reportName . " " . $year); $pdf->SetSubject($reportName); $pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, 'Condominio IBIZA-Cto Sierra Morena 152 - ' . $reportName . ' ' . $year, 'Generado el ' . date('d/m/Y H:i')); $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); $pdf->SetFont('helvetica', '', 9); $pdf->AddPage(); ob_start(); // Aquí incluiremos templates específicos para cada tipo de reporte PDF $accessibleHouseIds = Auth::getAccessibleHouseIds(); // Necesario para algunos reportes // Re-obtener los datos del reporte según el type switch ($reportType) { case 'water-debtors': // Necesitamos los filtros para este reporte $filters = [ 'year' => $_GET['filter_year'] ?? null, 'months' => $_GET['filter_months'] ?? null, 'house_id' => $_GET['filter_house'] ?? null, 'accessible_house_ids' => $accessibleHouseIds ]; if ($filters['months'] && !is_array($filters['months'])) { $filters['months'] = explode(',', $filters['months']); } // Requerimos el modelo Report require_once __DIR__ . '/models/Report.php'; $waterDebtors = Report::getWaterDebtors($filters); include __DIR__ . '/views/reports/pdf_water_debtors.php'; break; case 'concept-debtors': // Requerimos el modelo Report require_once __DIR__ . '/models/Report.php'; // Procesar filtros para exportación $houseFilters = $_GET['filter_houses'] ?? []; $conceptFilters = $_GET['filter_concepts'] ?? []; // Determinar casas a filtrar para exportación if (empty($houseFilters) || in_array('all', $houseFilters)) { $filteredHouses = $accessibleHouseIds; error_log("DEBUG - Using all accessible houses: " . count($filteredHouses)); } else { // Filtrar solo las casas específicamente seleccionadas $filteredHouses = array_filter($houseFilters, function($houseId) use ($accessibleHouseIds) { return $houseId !== 'all' && in_array($houseId, $accessibleHouseIds); }); error_log("DEBUG - Using filtered houses: " . count($filteredHouses) . " - " . implode(',', $filteredHouses)); } // Determinar conceptos a filtrar para exportación if (empty($conceptFilters) || in_array('all', $conceptFilters)) { $filteredConcepts = null; // Todos los conceptos error_log("DEBUG - Using all concepts"); } else { // Filtrar solo los conceptos específicamente seleccionados $filteredConcepts = array_filter($conceptFilters, function($conceptId) { return $conceptId !== 'all'; }); error_log("DEBUG - Using filtered concepts: " . count($filteredConcepts) . " - " . implode(',', $filteredConcepts)); } $conceptDebtors = Report::getConceptDebtorsFiltered($filteredHouses, $filteredConcepts); error_log("DEBUG - Concept debtors result: " . count($conceptDebtors['debtors']) . " concepts with debts"); include __DIR__ . '/views/reports/pdf_concept_debtors.php'; break; case 'expenses': // Requerimos el modelo Expense y Report require_once __DIR__ . '/models/Expense.php'; require_once __DIR__ . '/models/Report.php'; $expenses = Expense::all(); // Assuming all expenses for now $expensesByCategory = Report::getExpensesByCategory(); include __DIR__ . '/views/reports/pdf_expenses.php'; break; case 'concepts': // Requerimos los modelos necesarios require_once __DIR__ . '/models/CollectionConcept.php'; require_once __DIR__ . '/models/Expense.php'; $filterConceptId = $_GET['filter_concept'] ?? null; $conceptsToExport = []; if ($filterConceptId) { $concept = CollectionConcept::findById($filterConceptId); if ($concept) { $status = CollectionConcept::getCollectionStatus($filterConceptId); $payments = CollectionConcept::getPaymentsByConcept($filterConceptId); $conceptsToExport[] = [ 'concept' => $concept, 'status' => $status, 'payments' => $payments ]; $conceptName = preg_replace('/[^a-zA-Z0-9_]/', '_', $concept['name']); $pdfFilename = 'Concepto_' . $conceptName . '_IBIZA.pdf'; } } else { $allConcepts = CollectionConcept::all(true); foreach ($allConcepts as $c) { $status = CollectionConcept::getCollectionStatus($c['id']); $payments = CollectionConcept::getPaymentsByConcept($c['id']); $conceptsToExport[] = [ 'concept' => $c, 'status' => $status, 'payments' => $payments ]; } } include __DIR__ . '/views/reports/pdf_concepts.php'; break; case 'balance': default: // Requerimos el modelo Report require_once __DIR__ . '/models/Report.php'; $balance = Report::getGeneralBalance(); $filterYear = $_GET['filter_year'] ?? null; $conceptDetails = Report::getConceptDetailsByYear($filterYear); include __DIR__ . '/views/reports/pdf_balance.php'; break; } $html = ob_get_clean(); $pdf->writeHTML($html, true, false, true, false, ''); $pdf->Output($pdfFilename, 'D'); exit; case 'export_csv_balance': Auth::requireAdmin(); require_once __DIR__ . '/models/Report.php'; $filterYear = $_GET['filter_year'] ?? null; $balance = Report::getGeneralBalance(); $conceptDetails = Report::getConceptDetailsByYear($filterYear); header('Content-Type: text/csv'); $filename = 'Balance_General_IBIZA'; if ($filterYear) { $filename .= '_' . $filterYear; } header('Content-Disposition: attachment; filename="' . $filename . '.csv"'); fputs(fopen('php://output', 'w'), $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF))); $output = fopen('php://output', 'w'); fputcsv($output, ['Condominio IBIZA-Cto Sierra Morena 152 - Balance General']); if ($filterYear) { fputcsv($output, ['Año: ' . $filterYear]); } fputcsv($output, ['']); // Blank line fputcsv($output, ['Descripción', 'Monto']); fputcsv($output, ['Total Ingresos (Conceptos)', $balance['total_incomes']]); fputcsv($output, ['Total Egresos', $balance['total_expenses']]); fputcsv($output, ['Balance Neto', $balance['balance']]); fputcsv($output, ['']); // Blank line if (!empty($conceptDetails['concepts'])) { fputcsv($output, ['Detalle de Conceptos Especiales']); fputcsv($output, ['']); // Blank line fputcsv($output, ['Concepto', 'Descripción', 'Esperado', 'Recaudado', 'Pendiente', 'Gastos', 'Balance']); foreach ($conceptDetails['concepts'] as $cd) { fputcsv($output, [ $cd['concept']['name'], $cd['concept']['description'] ?? '-', $cd['expected'], $cd['collected'], $cd['pending'], $cd['expenses'], $cd['balance'] ]); } fputcsv($output, ['']); // Blank line fputcsv($output, [ 'TOTALES', '', $conceptDetails['totals']['expected'], $conceptDetails['totals']['collected'], $conceptDetails['totals']['pending'], $conceptDetails['totals']['expenses'], $conceptDetails['totals']['balance'] ]); } fclose($output); exit; case 'export_csv_expenses': // Lógica para exportar CSV de gastos header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="Gastos_por_Categoria_IBIZA_' . date('Y') . '.csv"'); fputs(fopen('php://output', 'w'), $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF))); // BOM for UTF-8 in Excel // Requerimos el modelo Expense require_once __DIR__ . '/models/Expense.php'; $expenses = Expense::all(); // Assuming all expenses for now $output = fopen('php://output', 'w'); fputcsv($output, ['Condominio IBIZA-Cto Sierra Morena 152 - Gastos ' . date('Y')]); fputcsv($output, ['']); // Blank line fputcsv($output, ['Fecha', 'Descripción', 'Categoría', 'Monto']); foreach ($expenses as $exp) { fputcsv($output, [ date('d/m/Y', strtotime($exp['expense_date'])), $exp['description'], $exp['category'] ?? '-', $exp['amount'] ]); } fclose($output); exit; case 'export_csv_concepts': Auth::requireAdmin(); require_once __DIR__ . '/models/CollectionConcept.php'; $filterConceptId = $_GET['filter_concept'] ?? null; $conceptsToExport = []; if ($filterConceptId) { $concept = CollectionConcept::findById($filterConceptId); if ($concept) { $status = CollectionConcept::getCollectionStatus($filterConceptId); $payments = CollectionConcept::getPaymentsByConcept($filterConceptId); $conceptsToExport[] = [ 'concept' => $concept, 'status' => $status, 'payments' => $payments ]; } } else { $allConcepts = CollectionConcept::all(true); foreach ($allConcepts as $c) { $status = CollectionConcept::getCollectionStatus($c['id']); $payments = CollectionConcept::getPaymentsByConcept($c['id']); $conceptsToExport[] = [ 'concept' => $c, 'status' => $status, 'payments' => $payments ]; } } header('Content-Type: text/csv'); if ($filterConceptId && !empty($conceptsToExport)) { $conceptName = $conceptsToExport[0]['concept']['name']; $conceptName = preg_replace('/[^a-zA-Z0-9_]/', '_', $conceptName); $filename = 'Concepto_' . $conceptName . '_IBIZA'; } else { $filename = 'Conceptos_Especiales_IBIZA'; } header('Content-Disposition: attachment; filename="' . $filename . '.csv"'); fputs(fopen('php://output', 'w'), $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF))); $output = fopen('php://output', 'w'); foreach ($conceptsToExport as $conceptData) { $concept = $conceptData['concept']; $status = $conceptData['status']; $payments = $conceptData['payments']; fputcsv($output, ['Condominio IBIZA-Cto Sierra Morena 152 - Concepto: ' . htmlspecialchars($concept['name'])]); fputcsv($output, ['Fecha: ' . date('d/m/Y', strtotime($concept['concept_date']))]); if (!empty($concept['description'])) { fputcsv($output, ['Descripción: ' . htmlspecialchars($concept['description'])]); } fputcsv($output, ['']); // Blank line fputcsv($output, ['Monto por Casa:', $concept['amount_per_house']]); fputcsv($output, ['Total Recaudado:', $status['total_collected']]); fputcsv($output, ['Total Esperado:', $status['total_expected']]); fputcsv($output, ['Balance:', $status['total_collected']]); fputcsv($output, ['']); // Blank line fputcsv($output, ['Casa', 'Propietario', 'Monto Pagado', 'Fecha de Pago']); foreach ($payments as $payment) { fputcsv($output, [ $payment['house_number'], $payment['owner_name'] ?? '-', $payment['amount'] > 0 ? $payment['amount'] : 0, $payment['payment_date'] ? date('d/m/Y', strtotime($payment['payment_date'])) : '-' ]); } fputcsv($output, ['', '', 'Total:', $status['total_collected']]); fputcsv($output, ['']); // Blank line para separar conceptos } fclose($output); exit; default: // Si no es una acción de exportación conocida, continuar al default break; } } break; case 'users_actions': // Nuevo case para acciones AJAX de gestión de usuarios header('Content-Type: application/json'); $userId = Auth::id(); // Usuario que realiza la acción // Incluir modelos necesarios require_once __DIR__ . '/models/User.php'; require_once __DIR__ . '/models/UserPermission.php'; require_once __DIR__ . '/models/House.php'; switch ($_GET['action']) { case 'list': Auth::requireAdmin(); // Solo administradores $users = User::all(); echo json_encode(['success' => true, 'users' => $users]); exit; case 'get_user_houses': Auth::requireAdmin(); // Solo administradores $targetUserId = $_GET['user_id'] ?? 0; $userHouses = UserPermission::getUserHouseIds($targetUserId); echo json_encode(['success' => true, 'houses' => array_map(function($id) { return ['id' => $id]; }, $userHouses)]); exit; case 'create': Auth::requireAdmin(); // Solo administradores if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { // Validaciones básicas if (empty($input['username']) || empty($input['email']) || empty($input['password']) || empty($input['first_name']) || empty($input['role'])) { echo json_encode(['success' => false, 'message' => 'Faltan campos requeridos para crear usuario.']); exit; } $newUserId = User::create($input); if ($newUserId) { Auth::logActivity('create_user', 'Usuario creado: ' . $input['username']); echo json_encode(['success' => true, 'message' => 'Usuario creado exitosamente', 'user_id' => $newUserId]); } else { echo json_encode(['success' => false, 'message' => 'Error al crear usuario.']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos.']); } exit; case 'update': Auth::requireAdmin(); // Solo administradores if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input && isset($input['id'])) { // Validaciones básicas if (empty($input['username']) || empty($input['email']) || empty($input['first_name']) || empty($input['role'])) { echo json_encode(['success' => false, 'message' => 'Faltan campos requeridos para actualizar usuario.']); exit; } $result = User::update($input['id'], $input); if ($result) { Auth::logActivity('update_user', 'Usuario actualizado: ID ' . $input['id'] . ' - ' . $input['username']); echo json_encode(['success' => true, 'message' => 'Usuario actualizado exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al actualizar usuario.']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos o ID de usuario no proporcionado.']); } exit; case 'delete': Auth::requireAdmin(); // Solo administradores $targetUserId = $_GET['id'] ?? 0; if (!$targetUserId) { echo json_encode(['success' => false, 'message' => 'ID de usuario no proporcionado.']); exit; } // Prevenir que el admin se auto-elimine if ($targetUserId == Auth::id()) { echo json_encode(['success' => false, 'message' => 'No puedes eliminar tu propia cuenta de administrador.']); exit; } $result = User::delete($targetUserId); // Esto inactiva al usuario // También eliminar permisos de casa si los tiene UserPermission::assignHousesToUser($targetUserId, []); // Borrar todas las asignaciones if ($result) { Auth::logActivity('delete_user', 'Usuario eliminado (inactivado): ID ' . $targetUserId); echo json_encode(['success' => true, 'message' => 'Usuario eliminado exitosamente (inactivado).']); } else { echo json_encode(['success' => false, 'message' => 'Error al eliminar usuario.']); } exit; case 'assign_houses': Auth::requireAdmin(); // Solo administradores if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input && isset($input['user_id']) && isset($input['house_ids'])) { UserPermission::assignHousesToUser($input['user_id'], $input['house_ids']); Auth::logActivity('assign_user_houses', 'Casas asignadas a usuario ID: ' . $input['user_id']); echo json_encode(['success' => true, 'message' => 'Permisos de casa actualizados.']); } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos o incompletos para asignar casas.']); } exit; case 'update_profile': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { // Validaciones básicas if (empty($input['email']) || empty($input['first_name'])) { echo json_encode(['success' => false, 'message' => 'Faltan campos requeridos: email, nombre.']); exit; } $result = User::updateProfile(Auth::id(), $input); if ($result) { // Actualizar la sesión si la información personal ha cambiado $_SESSION['email'] = $input['email']; $_SESSION['first_name'] = $input['first_name']; $_SESSION['last_name'] = $input['last_name']; Auth::logActivity('update_profile', 'Perfil de usuario actualizado: ID ' . Auth::id()); echo json_encode(['success' => true, 'message' => 'Perfil actualizado exitosamente.']); } else { echo json_encode(['success' => false, 'message' => 'Error al actualizar perfil.']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos.']); } exit; case 'change_password': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { $currentPassword = $input['current_password'] ?? ''; $newPassword = $input['new_password'] ?? ''; if (empty($currentPassword) || empty($newPassword)) { echo json_encode(['success' => false, 'message' => 'Contraseña actual y nueva son requeridas.']); exit; } if (strlen($newPassword) < 6) { echo json_encode(['success' => false, 'message' => 'La nueva contraseña debe tener al menos 6 caracteres.']); exit; } if (User::verifyPassword(Auth::id(), $currentPassword)) { $result = User::changePassword(Auth::id(), $newPassword); if ($result) { Auth::logActivity('change_password', 'Contraseña de usuario cambiada: ID ' . Auth::id()); echo json_encode(['success' => true, 'message' => 'Contraseña cambiada exitosamente.']); } else { echo json_encode(['success' => false, 'message' => 'Error al cambiar contraseña.']); } } else { echo json_encode(['success' => false, 'message' => 'Contraseña actual incorrecta.']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos.']); } exit; default: echo json_encode(['success' => false, 'message' => 'Acción de usuario no válida.']); exit; } break; case 'profile': // Vista de perfil de usuario $view = 'users/profile'; break; case 'concept_view_actions': // Nuevo case para acciones AJAX de concept_view if (isset($_GET['action'])) { header('Content-Type: application/json'); $userId = Auth::id(); // Obtener el ID del usuario actual switch ($_GET['action']) { case 'initialize_concept_payments': $conceptId = $_GET['concept_id'] ?? 0; if (!$conceptId) { echo json_encode(['success' => false, 'message' => 'ID de concepto no proporcionado']); exit; } if (!Auth::isCapturist()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado']); exit; } // Se requiere el modelo House para CollectionPayment::initializePayments require_once __DIR__ . '/models/House.php'; $result = CollectionPayment::initializePayments($conceptId, $userId); if ($result) { Auth::logActivity('initialize_concept_payments', 'Pagos de concepto inicializados: ID ' . $conceptId); echo json_encode(['success' => true, 'message' => 'Pagos inicializados exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al inicializar pagos']); } exit; case 'save_concept_payment': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'message' => 'Método no permitido']); exit; } $input = json_decode(file_get_contents('php://input'), true); if ($input) { $conceptId = $input['concept_id'] ?? 0; $houseId = $input['house_id'] ?? 0; $amount = $input['amount'] ?? 0; $paymentDate = $input['payment_date'] ?? null; if (!$conceptId || !$houseId || !is_numeric($amount)) { echo json_encode(['success' => false, 'message' => 'Datos de pago incompletos o inválidos']); exit; } if (!Auth::isCapturist()) { echo json_encode(['success' => false, 'message' => 'Permiso denegado']); exit; } $result = CollectionPayment::update($conceptId, $houseId, $amount, $userId, 'Pago actualizado', $paymentDate); if ($result) { Auth::logActivity('save_concept_payment', 'Pago de concepto guardado: Concepto ' . $conceptId . ', Casa ' . $houseId . ', Monto ' . $amount); echo json_encode(['success' => true, 'message' => 'Pago guardado exitosamente']); } else { echo json_encode(['success' => false, 'message' => 'Error al guardar pago']); } } else { echo json_encode(['success' => false, 'message' => 'Datos inválidos']); } exit; default: echo json_encode(['success' => false, 'message' => 'Acción no válida para la vista de concepto']); exit; } } break; default: $stats = Report::getDashboardStats($year); $recentActivity = ActivityLog::all(15); $view = 'dashboard/index'; break; } require_once __DIR__ . '/views/layout/base.php';