merge prod to main

This commit is contained in:
root
2026-05-16 13:44:12 -04:00
parent 663c0cdbda
commit b32fb853f6
901 changed files with 11241 additions and 1340 deletions
+103 -22
View File
@@ -68,17 +68,45 @@ public function financialReport()
}
// === Invoices ===
$invoices = $invoiceModel
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
->join('users', 'users.id = invoices.parent_id')
->findAll();
// Business rule: one active invoice per parent per school year.
// If legacy data has multiple invoices, show only the latest one to avoid reporting older invoices as "Unpaid"
// after new charges/payments update the current invoice.
$db = \Config\Database::connect();
if (!empty($schoolYear)) {
$latestSub = $db->table('invoices')
->select('parent_id, MAX(id) AS max_id')
->where('school_year', $schoolYear)
->groupBy('parent_id')
->getCompiledSelect();
$invBuilder = $db->table('invoices')
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name", false)
->join("($latestSub) latest", 'latest.max_id = invoices.id', 'inner', false)
->join('users', 'users.id = invoices.parent_id', 'left');
if ($hasFrom) {
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $dateFrom);
}
if ($hasTo) {
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $dateTo);
}
$invoices = $invBuilder->get()->getResultArray();
} else {
$invoices = $invoiceModel
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
->join('users', 'users.id = invoices.parent_id')
->findAll();
}
$invoiceIds = array_values(array_unique(array_filter(array_map(static function ($inv) {
$id = (int)($inv['id'] ?? 0);
return $id > 0 ? $id : null;
}, $invoices))));
// Helper to build a fresh, filtered PaymentModel each time (so filters don't get lost between queries)
$buildPaymentModel = function () use ($schoolYear, $dateFrom, $dateTo) {
$pm = new PaymentModel();
if ($schoolYear) {
$pm->where('school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$pm->where('DATE(payment_date) >=', $dateFrom);
}
@@ -114,7 +142,13 @@ public function financialReport()
};
// === Payments aggregated by invoice_id (for the "Paid" column) ===
$payments = $applyPaymentFilters($buildPaymentModel())
$paymentsQuery = $applyPaymentFilters($buildPaymentModel());
if (!empty($invoiceIds)) {
$paymentsQuery->whereIn('invoice_id', $invoiceIds);
} else {
$paymentsQuery->where('invoice_id', -1);
}
$payments = $paymentsQuery
->select('invoice_id, SUM(paid_amount) AS paid_amount')
->where('invoice_id IS NOT NULL')
->groupBy('invoice_id')
@@ -138,6 +172,7 @@ public function financialReport()
SUM(paid_amount) AS amount
")
->where('invoice_id IS NOT NULL')
->whereIn('invoice_id', $invoiceIds ?: [-1])
->groupBy('invoice_id, method')
->findAll();
@@ -161,6 +196,7 @@ public function financialReport()
SUM(CASE WHEN LOWER(TRIM(payment_method)) IN ('credit','card','credit card','debit','debit card','visa','mastercard') THEN paid_amount ELSE 0 END) AS total_credit
")
->where('invoice_id IS NOT NULL')
->whereIn('invoice_id', $invoiceIds ?: [-1])
->first() ?? [];
$paymentTotals = [
@@ -216,25 +252,28 @@ public function financialReport()
$schoolYears[] = (string)$schoolYear;
}
$eventFeesTotal = $this->getEventFeesTotal($schoolYear, $dateFrom, $dateTo);
// JSON API support
if ($this->wantsJson() || strtolower((string)($this->request->getGet('format') ?? '')) === 'json') {
return $this->response->setJSON([
'ok' => true,
'selectedYear' => $schoolYear,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
return $this->response->setJSON([
'ok' => true,
'selectedYear' => $schoolYear,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'schoolYears' => $schoolYears,
'invoices' => $invoices,
'payments' => $payments,
'paymentBreakdown' => $paymentBreakdown,
'paymentTotals' => $paymentTotals,
'refunds' => $refunds,
'expenses' => $expenses,
'reimbursements' => $reimbursements,
'discounts' => $discounts,
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_hash(),
]);
'refunds' => $refunds,
'expenses' => $expenses,
'reimbursements' => $reimbursements,
'discounts' => $discounts,
'eventFeesTotal' => $eventFeesTotal,
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_hash(),
]);
}
return view('payment/financial_report', [
@@ -246,6 +285,7 @@ public function financialReport()
'expenses' => $expenses,
'reimbursements' => $reimbursements,
'discounts' => $discounts,
'eventFeesTotal' => $eventFeesTotal,
'selectedYear' => $schoolYear,
'schoolYears' => $schoolYears,
'dateFrom' => $dateFrom,
@@ -1058,6 +1098,7 @@ public function financialReport()
$amountCollected = $totalPaid;
$netAmount = ($totalCharges - $totalDiscounts - $totalRefunds);
$totalEventFees = $this->getEventFeesTotal($schoolYear, $invoiceDateFrom, $invoiceDateTo);
return [
'schoolYear' => $schoolYear,
'dateFrom' => $dateFrom,
@@ -1073,9 +1114,28 @@ public function financialReport()
'amountCollected' => $amountCollected,
'totalUnpaid' => $totalUnpaid,
'netAmount' => $netAmount,
'totalEventFees' => $totalEventFees,
];
}
private function getEventFeesTotal(?string $schoolYear, ?string $dateFrom, ?string $dateTo): float
{
$db = \Config\Database::connect();
$builder = $db->table('event_charges ec')
->select('COALESCE(SUM(ec.charged),0) AS amount', false);
if (!empty($schoolYear)) {
$builder->where('ec.school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$builder->where('DATE(ec.created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$builder->where('DATE(ec.created_at) <=', $dateTo);
}
$row = $builder->get()->getRowArray();
return $row ? (float)($row['amount'] ?? 0) : 0.0;
}
/**
* Management page: list parents with outstanding balances (> 0) for a school year.
@@ -1226,6 +1286,23 @@ public function financialReport()
$byParent[$pid]['total_balance'] += $extra;
}
$eventFeesPerParent = [];
try {
$eventFeesRows = $db->table('event_charges ec')
->select('ec.parent_id, COALESCE(SUM(ec.charged),0) AS event_fees', false)
->where('ec.school_year', $schoolYear)
->groupBy('ec.parent_id')
->get()
->getResultArray();
foreach ($eventFeesRows as $row) {
$pid = (int)($row['parent_id'] ?? 0);
if ($pid <= 0) continue;
$eventFeesPerParent[$pid] = (float)($row['event_fees'] ?? 0);
}
} catch (\Throwable $e) {
// ignore, fallback to no event fees data
}
// Reduce into rows list; only parents with positive balance
// Also compute remaining installments and suggested monthly amount
$rows = [];
@@ -1283,10 +1360,11 @@ public function financialReport()
$parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows)));
$hasPayments = [];
$paidTotals = [];
$paymentCounts = [];
if (!empty($parentIds)) {
try {
$pRows = $db->table('payments')
->select('parent_id, SUM(paid_amount) AS total_paid')
->select('parent_id, SUM(paid_amount) AS total_paid, COUNT(*) AS payment_count')
->whereIn('parent_id', $parentIds)
->where('school_year', $schoolYear)
->groupBy('parent_id')
@@ -1296,6 +1374,7 @@ public function financialReport()
if ($pid > 0) {
$hasPayments[$pid] = true;
$paidTotals[$pid] = (float)($pr['total_paid'] ?? 0);
$paymentCounts[$pid] = (int)($pr['payment_count'] ?? 0);
}
}
} catch (\Throwable $e) {
@@ -1303,7 +1382,7 @@ public function financialReport()
}
}
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $nextInstallmentYmd) {
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallmentYmd, $eventFeesPerParent) {
$pid = (int)($r['parent_id'] ?? 0);
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
return [
@@ -1317,8 +1396,10 @@ public function financialReport()
'installment_amount' => (float)($r['installment_amount'] ?? 0),
'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment',
'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0,
'payment_count' => isset($paymentCounts[$pid]) ? (int)$paymentCounts[$pid] : 0,
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
'next_installment' => $nextInstallmentYmd,
'event_fees' => (float)($eventFeesPerParent[$pid] ?? 0),
];
}, $rows);