fetchAll( "SELECT h.id, h.number, h.status, h.consumption_only, h.owner_name FROM houses h ORDER BY CAST(h.number AS UNSIGNED)" ); $months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; // OPTIMIZADO: Una sola query en lugar de 12 queries separadas $allPayments = $db->fetchAll( "SELECT house_id, month, amount, payment_date FROM payments WHERE year = ? ORDER BY FIELD(month, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre')", [$year] ); // Organizar pagos por mes $payments = []; foreach ($months as $month) { $payments[$month] = []; } foreach ($allPayments as $p) { $payments[$p['month']][$p['house_id']] = $p; } return ['houses' => $houses, 'payments' => $payments, 'months' => $months]; } /** * Obtener monto esperado con caché de monthly_bills (OPTIMIZADO) * Usar esta versión cuando monthly_bills ya está cargado */ public static function getExpectedAmount($house, $year, $month, $monthlyBills = null) { // Si no se pasa caché, usar query (backward compatibility) if ($monthlyBills === null) { $db = Database::getInstance(); $bill = $db->fetchOne( "SELECT * FROM monthly_bills WHERE year = ? AND month = ?", [$year, $month] ); } else { $bill = $monthlyBills[$month] ?? null; } if (!$bill) { return 0; } $monto_base = $bill['amount_per_house']; if ($house['consumption_only'] && $year >= 2025) { $monto_base = max(0, $monto_base - 100.00); } return round($monto_base, 2); } /** * Obtener monto esperado sin descuento con caché (OPTIMIZADO) * Usar esta versión cuando monthly_bills ya está cargado */ public static function getExpectedAmountWithDiscount($house, $year, $month, $monthlyBills = null) { // Si no se pasa caché, usar query (backward compatibility) if ($monthlyBills === null) { $db = Database::getInstance(); $bill = $db->fetchOne( "SELECT * FROM monthly_bills WHERE year = ? AND month = ?", [$year, $month] ); } else { $bill = $monthlyBills[$month] ?? null; } if (!$bill) { return 0; } $monto_base = $bill['amount_per_house']; return round($monto_base, 2); } public static function update($houseId, $year, $month, $amount, $userId, $notes = null, $paymentMethod = null) { $db = Database::getInstance(); $existing = $db->fetchOne( "SELECT id FROM payments WHERE house_id = ? AND year = ? AND month = ?", [$houseId, $year, $month] ); if ($amount == 0 && $existing) { $db->execute( "DELETE FROM payments WHERE id = ?", [$existing['id']] ); return ['success' => true, 'deleted' => true]; } if ($existing) { $db->execute( "UPDATE payments SET amount = ?, payment_date = NOW(), notes = ?, payment_method = ?, created_by = ? WHERE id = ?", [$amount, $notes, $paymentMethod, $userId, $existing['id']] ); } else { $db->execute( "INSERT INTO payments (house_id, year, month, amount, payment_date, notes, payment_method, created_by) VALUES (?, ?, ?, ?, NOW(), ?, ?, ?)", [$houseId, $year, $month, $amount, $notes, $paymentMethod, $userId] ); } return ['success' => true, 'deleted' => false]; } public static function getByHouse($houseId, $year = null) { $db = Database::getInstance(); if ($year) { return $db->fetchAll( "SELECT * FROM payments WHERE house_id = ? AND year = ? ORDER BY FIELD(month, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre')", [$houseId, $year] ); } return $db->fetchAll( "SELECT * FROM payments WHERE house_id = ? ORDER BY year DESC, FIELD(month, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC", [$houseId] ); } public static function getTotalByYear($year) { $db = Database::getInstance(); $result = $db->fetchOne( "SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE year = ?", [$year] ); return $result['total'] ?? 0; } /** * Actualizar múltiples pagos en batch con transacción * Mucho más rápido que llamar update() múltiples veces * * @param array $changes Array de cambios [{house_id, year, month, amount}, ...] * @param int $userId ID del usuario que realiza los cambios * @return array ['success' => bool, 'count' => int, 'error' => string] */ public static function updateBatch($changes, $userId) { $db = Database::getInstance(); $pdo = $db->getPDO(); try { $pdo->beginTransaction(); $updateCount = 0; $deleteCount = 0; // Preparar statements una sola vez (reutilización) $insertStmt = $pdo->prepare( "INSERT INTO payments (house_id, year, month, amount, payment_date, created_by) VALUES (?, ?, ?, ?, NOW(), ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount), payment_date = VALUES(payment_date), created_by = VALUES(created_by)" ); $deleteStmt = $pdo->prepare( "DELETE FROM payments WHERE house_id = ? AND year = ? AND month = ?" ); foreach ($changes as $change) { // Validar que tenemos los datos mínimos if (!isset($change['house_id'], $change['year'], $change['month'])) { continue; } $amount = isset($change['amount']) ? (float)$change['amount'] : 0; if ($amount == 0) { // Eliminar si el monto es 0 $deleteStmt->execute([ $change['house_id'], $change['year'], $change['month'] ]); $deleteCount++; } else { // Insertar o actualizar $insertStmt->execute([ $change['house_id'], $change['year'], $change['month'], $amount, $userId ]); $updateCount++; } } $pdo->commit(); return [ 'success' => true, 'count' => $updateCount + $deleteCount, 'updated' => $updateCount, 'deleted' => $deleteCount ]; } catch (Exception $e) { $pdo->rollback(); error_log("Error en Payment::updateBatch: " . $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage() ]; } } }