Primer version funcional

This commit is contained in:
Administrador Ibiza
2025-12-29 23:37:11 -06:00
commit 5289fd4133
294 changed files with 111418 additions and 0 deletions

75
models/ActivityLog.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
class ActivityLog {
public static function all($limit = 100) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT al.*, u.username, u.first_name, u.last_name
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
ORDER BY al.created_at DESC
LIMIT ?",
[$limit]
);
}
public static function getByUser($userId, $limit = 50) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT al.*, u.username, u.first_name, u.last_name
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.user_id = ?
ORDER BY al.created_at DESC
LIMIT ?",
[$userId, $limit]
);
}
public static function getByAction($action, $limit = 50) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT al.*, u.username
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.action = ?
ORDER BY al.created_at DESC
LIMIT ?",
[$action, $limit]
);
}
public static function create($userId, $action, $details = '') {
$db = Database::getInstance();
$db->execute(
"INSERT INTO activity_logs (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)",
[
$userId,
$action,
$details,
$_SERVER['REMOTE_ADDR'] ?? null
]
);
}
public static function deleteAll() {
$db = Database::getInstance();
return $db->execute("DELETE FROM activity_logs");
}
public static function deleteByUser($userId) {
$db = Database::getInstance();
return $db->execute("DELETE FROM activity_logs WHERE user_id = ?", [$userId]);
}
public static function getDistinctUsers() {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT DISTINCT u.id, u.username, u.first_name, u.last_name
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE u.id IS NOT NULL
ORDER BY u.username"
);
}
}

207
models/CollectionConcept.php Executable file
View File

@@ -0,0 +1,207 @@
<?php
class CollectionConcept {
public static function all($activeOnly = false) {
$db = Database::getInstance();
$sql = "SELECT c.*, u.username as created_by_name
FROM finance_collection_concepts c
LEFT JOIN users u ON c.created_by = u.id";
if ($activeOnly) {
$sql .= " WHERE c.is_active = 1";
}
$sql .= " ORDER BY c.created_at DESC";
return $db->fetchAll($sql);
}
public static function findById($id) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM finance_collection_concepts WHERE id = ?",
[$id]
);
}
public static function create($data, $userId) {
$db = Database::getInstance();
$db->execute(
"INSERT INTO finance_collection_concepts
(name, description, total_amount, amount_per_house, concept_date, due_date, category, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[
$data['name'],
$data['description'] ?? null,
$data['total_amount'] ?? null,
$data['amount_per_house'],
$data['concept_date'],
$data['due_date'] ?? null,
$data['category'] ?? null,
$userId
]
);
return $db->lastInsertId();
}
public static function update($id, $data) {
$db = Database::getInstance();
return $db->execute(
"UPDATE finance_collection_concepts
SET name = ?, description = ?, total_amount = ?, amount_per_house = ?, concept_date = ?, due_date = ?, category = ?
WHERE id = ?",
[
$data['name'],
$data['description'] ?? null,
$data['total_amount'] ?? null,
$data['amount_per_house'],
$data['concept_date'],
$data['due_date'] ?? null,
$data['category'] ?? null,
$id
]
);
}
public static function save($data, $userId) {
if (isset($data['id']) && !empty($data['id'])) {
return self::update($data['id'], $data);
} else {
return self::create($data, $userId);
}
}
public static function delete($id) {
$db = Database::getInstance();
return $db->execute(
"DELETE FROM finance_collection_concepts WHERE id = ?",
[$id]
);
}
public static function getPaymentsByConcept($conceptId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT cp.*, h.number as house_number, h.owner_name
FROM finance_collection_payments cp
JOIN houses h ON cp.house_id = h.id
WHERE cp.concept_id = ?
ORDER BY CAST(h.number AS UNSIGNED)",
[$conceptId]
);
}
public static function getTotalCollected($conceptId) {
$db = Database::getInstance();
$result = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total
FROM finance_collection_payments
WHERE concept_id = ?",
[$conceptId]
);
return $result['total'] ?? 0;
}
public static function getCollectionStatus($conceptId) {
$concept = self::findById($conceptId);
$totalHouses = House::countActive();
$totalExpected = $concept['amount_per_house'] * $totalHouses;
$totalCollected = self::getTotalCollected($conceptId);
$totalExpenses = Expense::getTotalByConcept($conceptId);
return [
'total_houses' => $totalHouses,
'total_expected' => $totalExpected,
'total_collected' => $totalCollected,
'total_expenses' => $totalExpenses,
'balance' => $totalCollected - $totalExpenses,
'percentage' => $totalExpected > 0 ? round(($totalCollected / $totalExpected) * 100, 2) : 0,
'pending' => max(0, $totalExpected - $totalCollected)
];
}
}
class CollectionPayment {
public static function getForConcept($conceptId) {
$db = Database::getInstance();
$payments = $db->fetchAll(
"SELECT cp.*, h.number as house_number, h.owner_name
FROM finance_collection_payments cp
JOIN houses h ON cp.house_id = h.id
WHERE cp.concept_id = ?
ORDER BY CAST(h.number AS UNSIGNED)",
[$conceptId]
);
$result = [];
foreach ($payments as $p) {
$result[$p['house_id']] = $p;
}
return $result;
}
public static function update($conceptId, $houseId, $amount, $userId, $notes = null, $paymentDate = null) {
$db = Database::getInstance();
$existing = $db->fetchOne(
"SELECT id FROM finance_collection_payments WHERE concept_id = ? AND house_id = ?",
[$conceptId, $houseId]
);
$currentDateTime = date('Y-m-d H:i:s');
$paymentDate = $paymentDate ? $paymentDate : $currentDateTime;
if ($existing) {
$db->execute(
"UPDATE finance_collection_payments
SET amount = ?, payment_date = ?, notes = ?, created_by = ?
WHERE id = ?",
[$amount, $paymentDate, $notes, $userId, $existing['id']]
);
} else {
$db->execute(
"INSERT INTO finance_collection_payments
(concept_id, house_id, amount, payment_date, notes, created_by)
VALUES (?, ?, ?, ?, ?, ?)",
[$conceptId, $houseId, $amount, $paymentDate, $notes, $userId]
);
}
return ['success' => true, 'deleted' => false];
}
public static function getByHouse($houseId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT cp.*, c.name as concept_name
FROM finance_collection_payments cp
JOIN finance_collection_concepts c ON cp.concept_id = c.id
WHERE cp.house_id = ?
ORDER BY cp.created_at DESC",
[$houseId]
);
}
public static function initializePayments($conceptId, $userId) {
require_once __DIR__ . '/House.php'; // Incluir House model
$houses = House::allActive(); // Obtener solo casas activas
$db = Database::getInstance();
$db->beginTransaction();
try {
// Eliminar pagos existentes para este concepto antes de inicializar
$db->execute("DELETE FROM finance_collection_payments WHERE concept_id = ?", [$conceptId]);
foreach ($houses as $house) {
self::update($conceptId, $house['id'], 0, $userId, 'Pago inicializado', date('Y-m-d H:i:s'));
}
$db->commit();
return true;
} catch (Exception $e) {
$db->rollBack();
error_log("Error al inicializar pagos de concepto: " . $e->getMessage());
return false;
}
}
}

198
models/Expense.php Executable file
View File

@@ -0,0 +1,198 @@
<?php
class Expense {
public static function all($startDate = null, $endDate = null) {
$db = Database::getInstance();
$sql = "SELECT e.*, u.username as created_by_name
FROM expenses e
LEFT JOIN users u ON e.created_by = u.id";
$params = [];
if ($startDate && $endDate) {
$sql .= " WHERE expense_date BETWEEN ? AND ?";
$params[] = $startDate;
$params[] = $endDate;
}
$sql .= " ORDER BY expense_date DESC";
return $db->fetchAll($sql, $params);
}
public static function findById($id) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM expenses WHERE id = ?",
[$id]
);
}
public static function create($data, $userId) {
$db = Database::getInstance();
$db->execute(
"INSERT INTO expenses (description, amount, expense_date, category, receipt_path, notes, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)",
[
$data['description'],
$data['amount'],
$data['expense_date'],
$data['category'] ?? null,
$data['receipt_path'] ?? null,
$data['notes'] ?? null,
$userId
]
);
return $db->lastInsertId();
}
public static function update($id, $data) {
$db = Database::getInstance();
$db->execute(
"UPDATE expenses SET description = ?, amount = ?, expense_date = ?, category = ?, receipt_path = ?, notes = ? WHERE id = ?",
[
$data['description'],
$data['amount'],
$data['expense_date'],
$data['category'] ?? null,
$data['receipt_path'] ?? null,
$data['notes'] ?? null,
$id
]
);
return $id;
}
public static function save($data, $userId, $receiptFile = null, $allocations = []) {
$db = Database::getInstance();
$isUpdate = isset($data['id']) && !empty($data['id']);
$expenseId = $data['id'] ?? null;
// Handle receipt upload
$receipt_path = $data['receipt_path'] ?? null; // Keep existing path if not new file
if ($receiptFile && $receiptFile['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../uploads/expenses/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$filename = uniqid('receipt_') . '_' . basename($receiptFile['name']);
$targetPath = $uploadDir . $filename;
if (move_uploaded_file($receiptFile['tmp_name'], $targetPath)) {
$receipt_path = '/uploads/expenses/' . $filename;
} else {
// Handle upload error, maybe return false or throw exception
error_log("Failed to move uploaded file: " . $receiptFile['tmp_name'] . " to " . $targetPath);
return false;
}
} elseif ($isUpdate && empty($data['existing_receipt'])) {
// If it's an update and no new file, and existing_receipt is empty, clear the path
$receipt_path = null;
}
$data['receipt_path'] = $receipt_path;
if ($isUpdate) {
self::update($expenseId, $data);
} else {
$expenseId = self::create($data, $userId);
}
// Save allocations
if ($expenseId && !empty($allocations)) {
self::saveAllocations($expenseId, $allocations);
} else if ($expenseId && $isUpdate) {
// If update and no allocations, clear existing ones
$db->execute("DELETE FROM expense_concept_allocations WHERE expense_id = ?", [$expenseId]);
}
return $expenseId;
}
public static function saveAllocations($expenseId, $allocations) {
$db = Database::getInstance();
// Delete existing allocations for this expense
$db->execute("DELETE FROM expense_concept_allocations WHERE expense_id = ?", [$expenseId]);
foreach ($allocations as $allocation) {
$db->execute(
"INSERT INTO expense_concept_allocations (expense_id, concept_id, amount)
VALUES (?, ?, ?)",
[$expenseId, $allocation['concept_id'], $allocation['amount']]
);
}
}
public static function delete($id) {
$db = Database::getInstance();
// Optionally delete associated receipt file
$expense = self::findById($id);
if ($expense && $expense['receipt_path']) {
$filePath = __DIR__ . '/..' . $expense['receipt_path'];
if (file_exists($filePath)) {
unlink($filePath);
}
}
// Delete allocations first
$db->execute("DELETE FROM expense_concept_allocations WHERE expense_id = ?", [$id]);
return $db->execute(
"DELETE FROM expenses WHERE id = ?",
[$id]
);
}
public static function getTotalByDateRange($startDate, $endDate) {
$db = Database::getInstance();
$result = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total
FROM expenses
WHERE expense_date BETWEEN ? AND ?",
[$startDate, $endDate]
);
return $result['total'] ?? 0;
}
public static function getTotalByCategory($category, $startDate = null, $endDate = null) {
$db = Database::getInstance();
$sql = "SELECT COALESCE(SUM(amount), 0) as total
FROM expenses
WHERE category = ?";
$params = [$category];
if ($startDate && $endDate) {
$sql .= " AND expense_date BETWEEN ? AND ?";
$params[] = $startDate;
$params[] = $endDate;
}
$result = $db->fetchOne($sql, $params);
return $result['total'] ?? 0;
}
public static function getConcepts($expenseId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT ec.*, c.name as concept_name
FROM expense_concept_allocations ec
JOIN finance_collection_concepts c ON ec.concept_id = c.id
WHERE ec.expense_id = ?",
[$expenseId]
);
}
public static function getTotalByConcept($conceptId) {
$db = Database::getInstance();
$result = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total
FROM expense_concept_allocations
WHERE concept_id = ?",
[$conceptId]
);
return $result['total'] ?? 0;
}
}

93
models/House.php Executable file
View File

@@ -0,0 +1,93 @@
<?php
class House {
public static function all() {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT * FROM houses ORDER BY CAST(number AS UNSIGNED)"
);
}
public static function findById($id) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM houses WHERE id = ?",
[$id]
);
}
public static function findByNumber($number) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM houses WHERE number = ?",
[$number]
);
}
public static function getActive() {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT * FROM houses WHERE status = 'activa' ORDER BY CAST(number AS UNSIGNED)"
);
}
public static function getAccessible() {
$db = Database::getInstance();
$accessibleIds = Auth::getAccessibleHouseIds();
if (empty($accessibleIds)) {
return [];
}
$placeholders = str_repeat('?,', count($accessibleIds) - 1) . '?';
return $db->fetchAll(
"SELECT * FROM houses WHERE id IN ($placeholders) ORDER BY CAST(number AS UNSIGNED)",
$accessibleIds
);
}
public static function update($id, $data) {
$db = Database::getInstance();
return $db->execute(
"UPDATE houses SET status = ?, consumption_only = ?, owner_name = ?, owner_email = ?, owner_phone = ? WHERE id = ?",
[
$data['status'],
$data['consumption_only'] ?? 0,
$data['owner_name'] ?? null,
$data['owner_email'] ?? null,
$data['owner_phone'] ?? null,
$id
]
);
}
public static function countActive() {
$db = Database::getInstance();
$result = $db->fetchOne(
"SELECT COUNT(*) as count FROM houses WHERE status = 'activa'",
[]
);
return $result['count'] ?? 0;
}
public static function countAll() {
$db = Database::getInstance();
$result = $db->fetchOne(
"SELECT COUNT(*) as count FROM houses",
[]
);
return $result['count'] ?? 0;
}
public static function search($query) {
$db = Database::getInstance();
$term = "%$query%";
return $db->fetchAll(
"SELECT * FROM houses
WHERE number LIKE ? OR owner_name LIKE ?
ORDER BY CAST(number AS UNSIGNED)
LIMIT 10",
[$term, $term]
);
}
}

88
models/MonthlyBill.php Executable file
View File

@@ -0,0 +1,88 @@
<?php
class MonthlyBill {
public static function getYear($year) {
$db = Database::getInstance();
$months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
$result = [];
foreach ($months as $month) {
$bill = $db->fetchOne(
"SELECT * FROM monthly_bills WHERE year = ? AND month = ?",
[$year, $month]
);
$result[$month] = $bill;
}
return $result;
}
public static function save($data) {
$db = Database::getInstance();
$activeHouses = House::countActive();
if (isset($data['amount_per_house']) && !empty($data['amount_per_house'])) {
$amountPerHouse = $data['amount_per_house'];
} else {
$amountPerHouse = round($data['total_amount'] / $activeHouses, 2);
}
$existing = $db->fetchOne(
"SELECT id FROM monthly_bills WHERE year = ? AND month = ?",
[$data['year'], $data['month']]
);
if ($existing) {
$db->execute(
"UPDATE monthly_bills SET total_amount = ?, amount_per_house = ?, due_date = ? WHERE id = ?",
[
$data['total_amount'],
$amountPerHouse,
$data['due_date'] ?? null,
$existing['id']
]
);
return $existing['id'];
} else {
$db->execute(
"INSERT INTO monthly_bills (year, month, total_amount, amount_per_house, due_date)
VALUES (?, ?, ?, ?, ?)",
[
$data['year'],
$data['month'],
$data['total_amount'],
$amountPerHouse,
$data['due_date'] ?? null
]
);
return $db->lastInsertId();
}
}
public static function updatePayments($year, $month) {
$db = Database::getInstance();
$bill = $db->fetchOne(
"SELECT * FROM monthly_bills WHERE year = ? AND month = ?",
[$year, $month]
);
if (!$bill) {
return false;
}
$houses = House::getActive();
foreach ($houses as $house) {
$monto_esperado = $bill['amount_per_house'];
if ($house['consumption_only'] && $year >= 2025) {
$monto_esperado = max(0, $monto_esperado - 100.00);
}
}
return true;
}
}

110
models/Payment.php Executable file
View File

@@ -0,0 +1,110 @@
<?php
class Payment {
public static function getMatrix($year) {
$db = Database::getInstance();
$houses = $db->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'];
$payments = [];
foreach ($months as $month) {
$monthPayments = $db->fetchAll(
"SELECT house_id, amount, payment_date
FROM payments
WHERE year = ? AND month = ?",
[$year, $month]
);
$payments[$month] = [];
foreach ($monthPayments as $p) {
$payments[$month][$p['house_id']] = $p;
}
}
return ['houses' => $houses, 'payments' => $payments, 'months' => $months];
}
public static function getExpectedAmount($house, $year, $month) {
$db = Database::getInstance();
$bill = $db->fetchOne(
"SELECT * FROM monthly_bills WHERE year = ? AND month = ?",
[$year, $month]
);
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);
}
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;
}
}

399
models/Report.php Executable file
View File

@@ -0,0 +1,399 @@
<?php
class Report {
public static function getGeneralBalance($startDate = null, $endDate = null) {
$db = Database::getInstance();
$whereClause = '';
$params = [];
if ($startDate && $endDate) {
$whereClause = " WHERE YEAR(cp.payment_date) BETWEEN ? AND ?";
$params = [$startDate, $endDate];
}
$accessibleHouseIds = Auth::getAccessibleHouseIds();
if (!empty($accessibleHouseIds) && !Auth::isAdmin()) {
$placeholders = str_repeat('?,', count($accessibleHouseIds) - 1) . '?';
if ($whereClause) {
$whereClause .= " AND cp.house_id IN ($placeholders)";
} else {
$whereClause = " WHERE cp.house_id IN ($placeholders)";
}
$params = array_merge($params, $accessibleHouseIds);
}
$totalConceptPayments = $db->fetchOne(
"SELECT COALESCE(SUM(cp.amount), 0) as total
FROM finance_collection_payments cp
LEFT JOIN finance_collection_concepts c ON cp.concept_id = c.id
$whereClause",
$params
);
$totalConceptPayments = $totalConceptPayments['total'] ?? 0;
$totalExpenses = 0;
$balance = $totalConceptPayments;
if (Auth::isAdmin() || Auth::isCapturist()) {
$expenseParams = [];
$expenseWhere = '';
if ($startDate && $endDate) {
$expenseWhere = " WHERE YEAR(expense_date) BETWEEN ? AND ?";
$expenseParams = [$startDate, $endDate];
}
$totalExpenses = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total
FROM expenses $expenseWhere",
$expenseParams
);
$totalExpenses = $totalExpenses['total'] ?? 0;
$balance = $totalConceptPayments - $totalExpenses;
}
return [
'total_incomes' => $totalConceptPayments,
'water_incomes' => 0,
'concept_incomes' => $totalConceptPayments,
'total_expenses' => $totalExpenses,
'balance' => $balance
];
}
public static function getHouseStatement($houseId, $year = null) {
$db = Database::getInstance();
$house = House::findById($houseId);
$whereClause = $year ? " AND year = ?" : "";
$params = $year ? [$houseId, $year] : [$houseId];
$waterPayments = $db->fetchAll(
"SELECT 'Agua' as type, month, amount, payment_date, notes
FROM payments
WHERE house_id = ? $whereClause
ORDER BY year DESC, FIELD(month, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC",
$params
);
$conceptPayments = $db->fetchAll(
"SELECT 'Concepto' as type, c.name as description, cp.amount, cp.payment_date, cp.notes
FROM finance_collection_payments cp
JOIN finance_collection_concepts c ON cp.concept_id = c.id
WHERE cp.house_id = ?" .
($year ? " AND YEAR(cp.payment_date) = ?" : "") . "
ORDER BY cp.payment_date DESC",
$year ? [$houseId, $year] : [$houseId]
);
return [
'house' => $house,
'water_payments' => $waterPayments,
'concept_payments' => $conceptPayments
];
}
public static function getPaymentsByYear($year) {
$db = Database::getInstance();
$months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
$data = [];
foreach ($months as $month) {
$result = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as count
FROM payments
WHERE year = ? AND month = ?",
[$year, $month]
);
$data[$month] = [
'total' => $result['total'] ?? 0,
'count' => $result['count'] ?? 0
];
}
return $data;
}
public static function getExpensesByCategory($startDate = null, $endDate = null) {
$db = Database::getInstance();
$sql = "SELECT category, COALESCE(SUM(amount), 0) as total
FROM expenses";
$params = [];
if ($startDate && $endDate) {
$sql .= " WHERE YEAR(expense_date) BETWEEN ? AND ?";
$params = [$startDate, $endDate];
}
$sql .= " GROUP BY category ORDER BY total DESC";
return $db->fetchAll($sql, $params);
}
public static function getCollectionReport($conceptId) {
$concept = CollectionConcept::findById($conceptId);
$status = CollectionConcept::getCollectionStatus($conceptId);
$payments = CollectionConcept::getPaymentsByConcept($conceptId);
return [
'concept' => $concept,
'status' => $status,
'payments' => $payments
];
}
public static function getDashboardStats($year = null, $accessibleHouseIds = []) {
$year = $year ?? date('Y');
$db = Database::getInstance();
if (!empty($accessibleHouseIds) && !Auth::isAdmin()) {
$placeholders = str_repeat('?,', count($accessibleHouseIds) - 1) . '?';
$totalHouses = count($accessibleHouseIds);
$activeHousesResult = $db->fetchOne(
"SELECT COUNT(*) as count
FROM houses
WHERE id IN ($placeholders) AND status = 'activa'",
$accessibleHouseIds
);
$activeHouses = $activeHousesResult['count'] ?? 0;
$conceptPayments = $db->fetchOne(
"SELECT COALESCE(SUM(cp.amount), 0) as total
FROM finance_collection_payments cp
WHERE YEAR(cp.payment_date) = ? AND cp.house_id IN ($placeholders)",
array_merge([$year], $accessibleHouseIds)
);
$conceptPayments = $conceptPayments['total'] ?? 0;
$totalExpenses = 0;
$balance = $conceptPayments;
} else {
$totalHouses = House::countAll();
$activeHouses = House::countActive();
$conceptPayments = $db->fetchOne(
"SELECT COALESCE(SUM(cp.amount), 0) as total
FROM finance_collection_payments cp
WHERE YEAR(cp.payment_date) = ?",
[$year]
);
$conceptPayments = $conceptPayments['total'] ?? 0;
$totalExpenses = $db->fetchOne(
"SELECT COALESCE(SUM(amount), 0) as total
FROM expenses
WHERE YEAR(expense_date) = ?",
[$year]
);
$totalExpenses = $totalExpenses['total'] ?? 0;
$balance = $conceptPayments - $totalExpenses;
}
$totalConcepts = $db->fetchOne(
"SELECT COUNT(*) as count
FROM finance_collection_concepts
WHERE is_active = 1"
);
$totalConcepts = $totalConcepts['count'] ?? 0;
return [
'total_houses' => $totalHouses,
'active_houses' => $activeHouses,
'total_payments' => $conceptPayments,
'total_expenses' => $totalExpenses,
'balance' => $balance,
'active_concepts' => $totalConcepts,
'year' => $year
];
}
public static function getWaterDebtors($filters = []) {
$db = Database::getInstance();
$allMonths = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
$year = $filters['year'] ?? null;
$months = $filters['months'] ?? $allMonths;
$houseId = $filters['house_id'] ?? null;
$accessibleHouseIds = $filters['accessible_house_ids'] ?? [];
$whereHouse = '';
$houseParams = [];
if ($houseId) {
$whereHouse = "AND h.id = ?";
$houseParams = [$houseId];
}
$sql = "SELECT h.id, h.number, h.owner_name, h.status, h.consumption_only
FROM houses h
WHERE h.status = 'activa' $whereHouse";
if (!empty($accessibleHouseIds) && !Auth::isAdmin()) {
$placeholders = str_repeat('?,', count($accessibleHouseIds) - 1) . '?';
$sql .= " AND h.id IN ($placeholders)";
$houseParams = array_merge($houseParams, $accessibleHouseIds);
}
$sql .= " ORDER BY CAST(h.number AS UNSIGNED)";
$houses = $db->fetchAll($sql, $houseParams);
if ($year) {
$yearsToCheck = [$year];
} else {
$years = $db->fetchAll("SELECT DISTINCT year FROM payments ORDER BY year");
$yearsToCheck = array_column($years, 'year');
}
$debtors = [];
$grandTotalExpected = 0;
$grandTotalPaid = 0;
foreach ($houses as $house) {
$totalExpected = 0;
$totalPaid = 0;
$monthDetails = [];
foreach ($yearsToCheck as $yr) {
foreach ($months as $month) {
$expected = Payment::getExpectedAmount($house, $yr, $month);
$payment = $db->fetchOne(
"SELECT amount FROM payments WHERE house_id = ? AND year = ? AND month = ?",
[$house['id'], $yr, $month]
);
$paid = $payment['amount'] ?? 0;
$due = $expected - $paid;
$totalExpected += $expected;
$totalPaid += $paid;
if ($due > 0) {
$monthDetails[] = [
'year' => $yr,
'month' => $month,
'expected' => $expected,
'paid' => $paid,
'due' => $due
];
}
}
}
$houseTotalDue = $totalExpected - $totalPaid;
if ($houseTotalDue > 0) {
$debtors[] = [
'house_id' => $house['id'],
'house_number' => $house['number'],
'owner_name' => $house['owner_name'],
'months_due' => $monthDetails,
'total_due' => $houseTotalDue
];
}
$grandTotalExpected += $totalExpected;
$grandTotalPaid += $totalPaid;
}
$grandTotalDue = $grandTotalExpected - $grandTotalPaid;
return [
'debtors' => $debtors,
'total_due' => $grandTotalDue,
'total_expected' => $grandTotalExpected,
'total_paid' => $grandTotalPaid,
'filters' => $filters
];
}
public static function getConceptDebtors($accessibleHouseIds = []) {
$db = Database::getInstance();
$concepts = $db->fetchAll(
"SELECT c.id, c.name, c.amount_per_house
FROM finance_collection_concepts c
WHERE c.is_active = 1
ORDER BY c.created_at DESC"
);
$debtors = [];
$grandTotal = 0;
foreach ($concepts as $concept) {
$sql = "SELECT h.id, h.number, h.owner_name, h.status
FROM houses h
WHERE h.status = 'activa'";
$params = [];
if (!empty($accessibleHouseIds) && !Auth::isAdmin()) {
$placeholders = str_repeat('?,', count($accessibleHouseIds) - 1) . '?';
$sql .= " AND h.id IN ($placeholders)";
$params = $accessibleHouseIds;
}
$sql .= " ORDER BY CAST(h.number AS UNSIGNED)";
$houses = $db->fetchAll($sql, $params);
$houseDebtors = [];
$totalCollected = 0;
$totalExpected = $concept['amount_per_house'] * count($houses);
foreach ($houses as $house) {
$payment = $db->fetchOne(
"SELECT amount FROM finance_collection_payments
WHERE concept_id = ? AND house_id = ?",
[$concept['id'], $house['id']]
);
$paid = $payment['amount'] ?? 0;
if ($paid < $concept['amount_per_house']) {
$due = $concept['amount_per_house'] - $paid;
if ($due > 0) {
$houseDebtors[] = [
'house_id' => $house['id'],
'house_number' => $house['number'],
'owner_name' => $house['owner_name'],
'expected' => $concept['amount_per_house'],
'paid' => $paid,
'due' => $due
];
}
}
$totalCollected += $paid;
}
$conceptTotalDue = $totalExpected - $totalCollected;
if ($conceptTotalDue > 0) {
$debtors[] = [
'concept_id' => $concept['id'],
'concept_name' => $concept['name'],
'amount_per_house' => $concept['amount_per_house'],
'total_expected' => $totalExpected,
'total_collected' => $totalCollected,
'total_due' => $conceptTotalDue,
'house_debtors' => $houseDebtors
];
$grandTotal += $conceptTotalDue;
}
}
return [
'debtors' => $debtors,
'total_due' => $grandTotal
];
}
}

113
models/User.php Executable file
View File

@@ -0,0 +1,113 @@
<?php
class User {
public static function findByUsername($username) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM users WHERE username = ? AND is_active = 1",
[$username]
);
}
public static function findById($id) {
$db = Database::getInstance();
return $db->fetchOne(
"SELECT * FROM users WHERE id = ?",
[$id]
);
}
public static function all() {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT id, username, email, first_name, last_name, role, is_active, last_login, created_at
FROM users ORDER BY id"
);
}
public static function create($data) {
$db = Database::getInstance();
$db->execute(
"INSERT INTO users (username, email, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?, ?)",
[
$data['username'],
$data['email'],
password_hash($data['password'], PASSWORD_DEFAULT),
$data['first_name'],
$data['last_name'],
$data['role']
]
);
return $db->lastInsertId();
}
public static function update($id, $data) {
$db = Database::getInstance();
$sql = "UPDATE users SET username = ?, email = ?, first_name = ?, last_name = ?, role = ?";
$params = [
$data['username'],
$data['email'],
$data['first_name'],
$data['last_name'],
$data['role']
];
if (!empty($data['password'])) {
$sql .= ", password = ?";
$params[] = password_hash($data['password'], PASSWORD_DEFAULT);
}
$sql .= " WHERE id = ?";
$params[] = $id;
// Database::execute() puede devolver el número de filas afectadas o un booleano.
// Si devuelve 0 (ninguna fila afectada) PHP lo interpreta como false.
// Queremos que sea true si la consulta se ejecuta sin errores.
$stmt = $db->execute($sql, $params);
return $stmt !== false; // Devuelve true si la ejecución fue exitosa, false si hubo un error.
}
public static function delete($id) {
$db = Database::getInstance();
return $db->execute(
"UPDATE users SET is_active = 0 WHERE id = ?",
[$id]
);
}
public static function updateProfile($id, $data) {
$db = Database::getInstance();
$stmt = $db->execute(
"UPDATE users SET email = ?, first_name = ?, last_name = ? WHERE id = ?",
[
$data['email'],
$data['first_name'],
$data['last_name'],
$id
]
);
return $stmt !== false; // Devuelve true si la ejecución fue exitosa, false si hubo un error.
}
public static function changePassword($id, $newPassword) {
$db = Database::getInstance();
$stmt = $db->execute(
"UPDATE users SET password = ? WHERE id = ?",
[
password_hash($newPassword, PASSWORD_DEFAULT),
$id
]
);
return $stmt !== false; // Devuelve true si la ejecución fue exitosa, false si hubo un error.
}
public static function verifyPassword($id, $password) {
$db = Database::getInstance();
$user = $db->fetchOne(
"SELECT password FROM users WHERE id = ?",
[$id]
);
return $user && password_verify($password, $user['password']);
}
}

42
models/UserPermission.php Executable file
View File

@@ -0,0 +1,42 @@
<?php
class UserPermission {
public static function getUserHouses($userId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT h.* FROM houses h
INNER JOIN user_house_permissions uhp ON h.id = uhp.house_id
WHERE uhp.user_id = ? ORDER BY CAST(h.number AS UNSIGNED)",
[$userId]
);
}
public static function getUserHouseIds($userId) {
$db = Database::getInstance();
$result = $db->fetchAll(
"SELECT house_id FROM user_house_permissions WHERE user_id = ?",
[$userId]
);
return array_column($result, 'house_id');
}
public static function assignHousesToUser($userId, $houseIds) {
$db = Database::getInstance();
$db->execute("DELETE FROM user_house_permissions WHERE user_id = ?", [$userId]);
foreach ($houseIds as $houseId) {
$db->execute(
"INSERT INTO user_house_permissions (user_id, house_id) VALUES (?, ?)",
[$userId, $houseId]
);
}
}
public static function removeUserFromHouse($userId, $houseId) {
$db = Database::getInstance();
return $db->execute(
"DELETE FROM user_house_permissions WHERE user_id = ? AND house_id = ?",
[$userId, $houseId]
);
}
}