From 654453222fba4fac9c2a3f6f097e46788aa31a69 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Jun 2026 23:30:41 -0400 Subject: [PATCH] financial fix --- app/Config/Routes.php | 2 + app/Controllers/View/FinancialController.php | 936 +++++++++++++++++- .../payment/financial_report_summary.php | 145 ++- 3 files changed, 1042 insertions(+), 41 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1b51f9e..65b47f2 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -645,6 +645,8 @@ $routes->post('admin/broadcast-email/upload-image', 'View\BroadcastEmailControll $routes->get('payment/financial_report', 'View\FinancialController::financialReport', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); $routes->get('financial-report/financialReportSummary', 'View\FinancialController::financialReportSummary', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); +$routes->get('financial-report/downloadSummaryCsv', 'View\FinancialController::downloadSummaryCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); +$routes->get('financial-report/downloadSummaryAllDetailsCsv', 'View\FinancialController::downloadSummaryAllDetailsCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); $routes->get('payment/download_csv', 'View\FinancialController::downloadCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); $routes->get('administrator/tuition-forecast', 'View\TuitionForecastController::index', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); $routes->post('administrator/tuition-forecast/calculate', 'View\TuitionForecastController::calculate', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']); diff --git a/app/Controllers/View/FinancialController.php b/app/Controllers/View/FinancialController.php index 8391cef..3733fd4 100644 --- a/app/Controllers/View/FinancialController.php +++ b/app/Controllers/View/FinancialController.php @@ -341,6 +341,859 @@ public function financialReport() return $this->financialReportSummary(); } + public function downloadSummaryCsv() + { + $dateFrom = $this->request->getGet('date_from'); + $dateTo = $this->request->getGet('date_to'); + $schoolYear = $this->request->getGet('school_year'); + $data = $this->getFinancialSummary($dateFrom, $dateTo, $schoolYear); + + $safeYear = preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)($data['schoolYear'] ?? 'summary')); + $filename = 'financial_report_summary_' . trim($safeYear, '_') . '_' . date('Ymd_His') . '.csv'; + + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + + $out = fopen('php://output', 'w'); + + fputcsv($out, ['Financial Report Summary']); + fputcsv($out, ['School Year', (string)($data['schoolYear'] ?? '')]); + fputcsv($out, ['Date From', (string)($data['dateFrom'] ?? '')]); + fputcsv($out, ['Date To', (string)($data['dateTo'] ?? '')]); + fputcsv($out, []); + fputcsv($out, ['Description', 'Amount']); + + $rows = [ + ['Tuition Charges', (float)($data['tuitionCharges'] ?? 0)], + ['Event Fees', (float)($data['totalEventFees'] ?? 0)], + ['Prior-Year Tuition Carryover', (float)($data['totalExtraCharges'] ?? 0)], + ['Gross Charges', (float)($data['grossCharges'] ?? $data['totalCharges'] ?? 0)], + ['Total Discounts', (float)($data['totalDiscounts'] ?? 0)], + ['Total Refunds', (float)($data['totalRefunds'] ?? 0)], + ['Total Expenses', (float)($data['totalExpenses'] ?? 0)], + ['Total Reimbursements', (float)($data['totalReimbursements'] ?? 0)], + ['Donation to School (included in expenses)', (float)($data['donationToSchool'] ?? 0)], + ['Net Charges After Discounts/Refunds', (float)($data['netAmount'] ?? 0)], + ['Amount Collected (Paid)', (float)($data['amountCollected'] ?? 0)], + ['Overpayment Credits', (float)($data['totalOverpaid'] ?? 0)], + ['Gross Outstanding Before Credits', (float)($data['totalUnpaid'] ?? 0)], + ['Amount Unpaid (Net of Credits)', (float)($data['netReceivable'] ?? 0)], + ]; + + foreach ($rows as $row) { + fputcsv($out, $row); + } + + fputcsv($out, []); + fputcsv($out, ['Reconciliation']); + fputcsv($out, [ + 'Collected + Outstanding - Overpayment Credits = Net Charges After Discounts/Refunds', + round( + (float)($data['amountCollected'] ?? 0) + + (float)($data['totalUnpaid'] ?? 0) + - (float)($data['totalOverpaid'] ?? 0), + 2 + ), + ]); + + if (!empty($data['overpaymentDetails']) && is_array($data['overpaymentDetails'])) { + fputcsv($out, []); + fputcsv($out, ['Overpayment Credit Details']); + fputcsv($out, ['Parent', 'Invoice #', 'Gross Charges', 'Discounts', 'Refunds', 'Paid', 'Credit Amount', 'Note']); + foreach ($data['overpaymentDetails'] as $detail) { + fputcsv($out, [ + (string)($detail['parent_name'] ?? ''), + (string)($detail['invoice_number'] ?? ''), + (float)($detail['gross_amount'] ?? 0), + (float)($detail['discount_amount'] ?? 0), + (float)($detail['refund_amount'] ?? 0), + (float)($detail['paid_amount'] ?? 0), + (float)($detail['credit_amount'] ?? 0), + (string)($detail['note'] ?? ''), + ]); + } + } + + fclose($out); + exit; + } + + public function downloadSummaryAllDetailsCsv() + { + $dateFrom = $this->request->getGet('date_from'); + $dateTo = $this->request->getGet('date_to'); + $schoolYear = $this->request->getGet('school_year'); + + $summary = $this->getFinancialSummary($dateFrom, $dateTo, $schoolYear); + $sections = $this->getFinancialSummaryDetailSections($dateFrom, $dateTo, $schoolYear); + + $safeYear = preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)($summary['schoolYear'] ?? 'summary')); + $filename = 'financial_report_all_details_' . trim($safeYear, '_') . '_' . date('Ymd_His') . '.csv'; + + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + + $out = fopen('php://output', 'w'); + + fputcsv($out, ['Financial Report All Details']); + fputcsv($out, ['School Year', (string)($summary['schoolYear'] ?? '')]); + fputcsv($out, ['Date From', (string)($summary['dateFrom'] ?? '')]); + fputcsv($out, ['Date To', (string)($summary['dateTo'] ?? '')]); + + $overview = [ + ['Tuition Charges' => (float)($summary['tuitionCharges'] ?? 0)], + ['Event Fee Charges' => (float)($summary['totalEventFees'] ?? 0)], + ['Prior-Year Tuition Carryover' => (float)($summary['totalExtraCharges'] ?? 0)], + ['Gross Charges' => (float)($summary['grossCharges'] ?? $summary['totalCharges'] ?? 0)], + ['Total Discounts' => (float)($summary['totalDiscounts'] ?? 0)], + ['Total Refunds' => (float)($summary['totalRefunds'] ?? 0)], + ['Total Expenses' => (float)($summary['totalExpenses'] ?? 0)], + ['Total Reimbursements' => (float)($summary['totalReimbursements'] ?? 0)], + ['Donation to School (included in expenses)' => (float)($summary['donationToSchool'] ?? 0)], + ['Net Charges After Discounts/Refunds' => (float)($summary['netAmount'] ?? 0)], + ['Amount Collected (Paid)' => (float)($summary['amountCollected'] ?? 0)], + ['Overpayment Credits' => (float)($summary['totalOverpaid'] ?? 0)], + ['Gross Outstanding Before Credits' => (float)($summary['totalUnpaid'] ?? 0)], + ['Amount Unpaid (Net of Credits)' => (float)($summary['netReceivable'] ?? 0)], + ]; + + fputcsv($out, []); + fputcsv($out, ['Summary Overview']); + fputcsv($out, ['Description', 'Amount']); + foreach ($overview as $row) { + $label = array_key_first($row); + fputcsv($out, [$label, $row[$label]]); + } + + fputcsv($out, []); + fputcsv($out, ['Reconciliation']); + fputcsv($out, [ + 'Collected + Gross Outstanding - Overpayment Credits = Net Charges After Discounts/Refunds', + round( + (float)($summary['amountCollected'] ?? 0) + + (float)($summary['totalUnpaid'] ?? 0) + - (float)($summary['totalOverpaid'] ?? 0), + 2 + ), + ]); + + $orderedSections = [ + 'Invoices' => $sections['invoices'] ?? [], + 'Discount Details' => $sections['discounts'] ?? [], + 'Payment Details' => $sections['payments'] ?? [], + 'Refund Details' => $sections['refunds'] ?? [], + 'Event Fee Charge Details' => $sections['eventCharges'] ?? [], + 'Prior-Year Tuition Carryover Details' => $sections['carryovers'] ?? [], + 'Parents With Outstanding Balance Details' => $sections['unpaidParents'] ?? [], + 'Expense Details' => $sections['expenses'] ?? [], + 'Reimbursement Details' => $sections['reimbursements'] ?? [], + 'Donation Expense Details' => $sections['donationExpenses'] ?? [], + 'Donation Reimbursement Details' => $sections['donationReimbursements'] ?? [], + 'Reimbursement Batch Fallback Details' => $sections['reimbursementBatchFallbacks'] ?? [], + 'Overpayment Credit Details' => $summary['overpaymentDetails'] ?? [], + ]; + + foreach ($orderedSections as $title => $rows) { + $this->writeCsvSection($out, $title, $rows); + } + + fclose($out); + exit; + } + + private function writeCsvSection($out, string $title, array $rows): void + { + fputcsv($out, []); + fputcsv($out, [$title]); + if (empty($rows)) { + fputcsv($out, ['No rows found']); + return; + } + + $headers = array_keys((array)reset($rows)); + fputcsv($out, $headers); + foreach ($rows as $row) { + $line = []; + foreach ($headers as $header) { + $line[] = $row[$header] ?? ''; + } + fputcsv($out, $line); + } + } + + private function getFinancialSummaryDetailSections(?string $dateFrom = null, ?string $dateTo = null, ?string $schoolYear = null): array + { + $configModel = new \App\Models\ConfigurationModel(); + $schoolYear = $schoolYear ?: $configModel->getConfig('school_year'); + + $derivedDateFrom = null; + $derivedDateTo = null; + if (!empty($schoolYear) && empty($dateFrom) && empty($dateTo)) { + if (preg_match('/^(\\d{4})[^\\d]?(\\d{4})$/', $schoolYear, $m)) { + $derivedDateFrom = $m[1] . '-01-01'; + $derivedDateTo = $m[2] . '-12-31'; + } + } + + $invoiceDateFrom = $dateFrom ?: $derivedDateFrom; + $invoiceDateTo = $dateTo ?: $derivedDateTo; + $db = \Config\Database::connect(); + + $normalizeYear = static function (?string $year): string { + return preg_replace('/[^0-9]/', '', (string)$year); + }; + $normalizedYear = $normalizeYear($schoolYear); + $paymentExclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled']; + $hasEventPaid = $db->fieldExists('event_paid', 'event_charges'); + $hasEventPaymentId = $db->fieldExists('event_payment_id', 'event_charges'); + $specialRecipientIds = array_map('intval', array_keys(ReimbursementController::SPECIAL_RECIPIENTS)); + + $invoiceRows = $db->table('invoices') + ->select("invoices.id, invoices.invoice_number, invoices.parent_id, CONCAT(COALESCE(users.firstname, ''), ' ', COALESCE(users.lastname, '')) AS parent_name, invoices.issue_date, invoices.due_date, invoices.total_amount, invoices.status") + ->join('users', 'users.id = invoices.parent_id', 'left') + ->where('invoices.school_year', $schoolYear); + if (!empty($invoiceDateFrom)) { + $invoiceRows->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $invoiceRows->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $invoiceDateTo); + } + $invoiceRows = $invoiceRows->orderBy('invoices.id', 'ASC')->get()->getResultArray(); + + $invoiceIds = array_values(array_filter(array_map(static fn($row) => (int)($row['id'] ?? 0), $invoiceRows))); + + $paidByInvoice = []; + $discountByInvoice = []; + $refundByInvoice = []; + if (!empty($invoiceIds)) { + $paidRows = $db->table('payments') + ->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid') + ->whereIn('invoice_id', $invoiceIds) + ->groupStart() + ->whereNotIn('status', $paymentExclude) + ->orWhere('status IS NULL', null, false) + ->groupEnd(); + if (!empty($schoolYear)) { + $paidRows->where('school_year', $schoolYear); + } + if (!empty($invoiceDateFrom)) { + $paidRows->where('DATE(payment_date) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $paidRows->where('DATE(payment_date) <=', $invoiceDateTo); + } + foreach ($paidRows->groupBy('invoice_id')->get()->getResultArray() as $row) { + $paidByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_paid'] ?? 0); + } + + $discountRows = $db->table('discount_usages') + ->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_discount') + ->whereIn('invoice_id', $invoiceIds); + if (!empty($dateFrom)) { + $discountRows->where('DATE(COALESCE(used_at, created_at)) >=', $dateFrom); + } + if (!empty($dateTo)) { + $discountRows->where('DATE(COALESCE(used_at, created_at)) <=', $dateTo); + } + foreach ($discountRows->groupBy('invoice_id')->get()->getResultArray() as $row) { + $discountByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_discount'] ?? 0); + } + + $refundRows = $db->table('refunds') + ->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_refund') + ->whereIn('invoice_id', $invoiceIds) + ->whereIn('status', ['Partial', 'Paid']); + if (!empty($dateFrom)) { + $refundRows->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom); + } + if (!empty($dateTo)) { + $refundRows->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo); + } + foreach ($refundRows->groupBy('invoice_id')->get()->getResultArray() as $row) { + $refundByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_refund'] ?? 0); + } + } + + $invoices = []; + foreach ($invoiceRows as $row) { + $iid = (int)($row['id'] ?? 0); + $paid = (float)($paidByInvoice[$iid] ?? 0); + $disc = (float)($discountByInvoice[$iid] ?? 0); + $refund = (float)($refundByInvoice[$iid] ?? 0); + $balance = round((float)($row['total_amount'] ?? 0) - $disc - $refund - $paid, 2); + $invoices[] = [ + 'Invoice #' => (string)($row['invoice_number'] ?? ''), + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Issue Date' => (string)($row['issue_date'] ?? ''), + 'Due Date' => (string)($row['due_date'] ?? ''), + 'Gross Charges' => (float)($row['total_amount'] ?? 0), + 'Discounts' => $disc, + 'Refunds' => $refund, + 'Paid' => $paid, + 'Balance' => $balance, + 'Status' => (string)($row['status'] ?? ''), + ]; + } + + $discountDetailsBuilder = $db->table('discount_usages du') + ->select("du.id, du.invoice_id, du.voucher_id, du.discount_amount, du.used_at, du.created_at, i.invoice_number, CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS parent_name") + ->join('invoices i', 'i.id = du.invoice_id') + ->join('users u', 'u.id = i.parent_id', 'left') + ->where('i.school_year', $schoolYear); + if (!empty($dateFrom)) { + $discountDetailsBuilder->where('DATE(COALESCE(du.used_at, du.created_at)) >=', $dateFrom); + } + if (!empty($dateTo)) { + $discountDetailsBuilder->where('DATE(COALESCE(du.used_at, du.created_at)) <=', $dateTo); + } + $discounts = array_map(static function ($row) { + return [ + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Invoice #' => (string)($row['invoice_number'] ?? ''), + 'Voucher ID' => (string)($row['voucher_id'] ?? ''), + 'Discount Amount' => (float)($row['discount_amount'] ?? 0), + 'Used At' => (string)($row['used_at'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $discountDetailsBuilder->orderBy('du.id', 'ASC')->get()->getResultArray()); + + $paymentDetailsBuilder = $db->table('payments p') + ->select("p.id, p.invoice_id, p.parent_id, p.paid_amount, p.payment_method, p.payment_date, p.status, p.transaction_id, p.check_number, i.invoice_number, CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS parent_name") + ->join('invoices i', 'i.id = p.invoice_id', 'left') + ->join('users u', 'u.id = p.parent_id', 'left') + ->where('p.school_year', $schoolYear) + ->groupStart() + ->whereNotIn('p.status', $paymentExclude) + ->orWhere('p.status IS NULL', null, false) + ->groupEnd(); + if (!empty($invoiceDateFrom)) { + $paymentDetailsBuilder->where('DATE(p.payment_date) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $paymentDetailsBuilder->where('DATE(p.payment_date) <=', $invoiceDateTo); + } + $payments = array_map(static function ($row) { + return [ + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Invoice #' => (string)($row['invoice_number'] ?? ''), + 'Paid Amount' => (float)($row['paid_amount'] ?? 0), + 'Payment Method' => (string)($row['payment_method'] ?? ''), + 'Payment Date' => (string)($row['payment_date'] ?? ''), + 'Status' => (string)($row['status'] ?? ''), + 'Transaction ID' => (string)($row['transaction_id'] ?? ''), + 'Check Number' => (string)($row['check_number'] ?? ''), + ]; + }, $paymentDetailsBuilder->orderBy('p.id', 'ASC')->get()->getResultArray()); + + $refundDetailsBuilder = $db->table('refunds r') + ->select("r.id, r.invoice_id, r.parent_id, r.refund_paid_amount, r.status, r.request, r.reason, r.refunded_at, r.created_at, i.invoice_number, CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS parent_name") + ->join('invoices i', 'i.id = r.invoice_id', 'left') + ->join('users u', 'u.id = r.parent_id', 'left') + ->where('r.school_year', $schoolYear) + ->whereIn('r.status', ['Partial', 'Paid']); + if (!empty($dateFrom)) { + $refundDetailsBuilder->where('DATE(COALESCE(r.refunded_at, r.created_at)) >=', $dateFrom); + } + if (!empty($dateTo)) { + $refundDetailsBuilder->where('DATE(COALESCE(r.refunded_at, r.created_at)) <=', $dateTo); + } + $refunds = array_map(static function ($row) { + return [ + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Invoice #' => (string)($row['invoice_number'] ?? ''), + 'Refund Amount' => (float)($row['refund_paid_amount'] ?? 0), + 'Status' => (string)($row['status'] ?? ''), + 'Request Type' => (string)($row['request'] ?? ''), + 'Reason' => (string)($row['reason'] ?? ''), + 'Refunded At' => (string)($row['refunded_at'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $refundDetailsBuilder->orderBy('r.id', 'ASC')->get()->getResultArray()); + + $eventSelect = "ec.id, ec.parent_id, ec.student_id, ec.charged, ec.participation, ec.created_at, e.event_name, CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS parent_name, CONCAT(COALESCE(s.firstname, ''), ' ', COALESCE(s.lastname, '')) AS student_name"; + if ($hasEventPaid) { + $eventSelect .= ", ec.event_paid"; + } + if ($hasEventPaymentId) { + $eventSelect .= ", ec.event_payment_id"; + } + $eventBuilder = $db->table('event_charges ec') + ->select($eventSelect, false) + ->join('events e', 'e.id = ec.event_id', 'left') + ->join('users u', 'u.id = ec.parent_id', 'left') + ->join('students s', 's.id = ec.student_id', 'left') + ->where('ec.school_year', $schoolYear); + if (!empty($invoiceDateFrom)) { + $eventBuilder->where('DATE(ec.created_at) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $eventBuilder->where('DATE(ec.created_at) <=', $invoiceDateTo); + } + $eventCharges = array_map(static function ($row) use ($hasEventPaid, $hasEventPaymentId) { + return [ + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Student' => trim((string)($row['student_name'] ?? '')), + 'Event' => (string)($row['event_name'] ?? ''), + 'Charged Amount' => (float)($row['charged'] ?? 0), + 'Participation' => (string)($row['participation'] ?? ''), + 'Event Paid' => $hasEventPaid ? (string)($row['event_paid'] ?? '') : '', + 'Event Payment ID' => $hasEventPaymentId ? (string)($row['event_payment_id'] ?? '') : '', + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $eventBuilder->orderBy('ec.id', 'ASC')->get()->getResultArray()); + + $carryoverBuilder = $db->table('additional_charges ac') + ->select("ac.id, ac.parent_id, ac.invoice_id, ac.title, ac.description, ac.amount, ac.status, ac.charge_type, ac.due_date, ac.created_at, i.invoice_number, CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS parent_name") + ->join('users u', 'u.id = ac.parent_id', 'left') + ->join('invoices i', 'i.id = ac.invoice_id', 'left') + ->where('ac.school_year', $schoolYear) + ->whereNotIn('ac.status', ['void', 'voided']); + if (!empty($dateFrom)) { + $carryoverBuilder->where('DATE(ac.due_date) >=', $dateFrom); + } + if (!empty($dateTo)) { + $carryoverBuilder->where('DATE(ac.due_date) <=', $dateTo); + } + $carryovers = array_map(static function ($row) { + return [ + 'Parent' => trim((string)($row['parent_name'] ?? '')), + 'Invoice #' => (string)($row['invoice_number'] ?? ''), + 'Title' => (string)($row['title'] ?? ''), + 'Description' => (string)($row['description'] ?? ''), + 'Amount' => (float)($row['amount'] ?? 0), + 'Charge Type' => (string)($row['charge_type'] ?? ''), + 'Status' => (string)($row['status'] ?? ''), + 'Due Date' => (string)($row['due_date'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $carryoverBuilder->orderBy('ac.id', 'ASC')->get()->getResultArray()); + + $expenseBuilder = $db->table('expenses e') + ->select('e.id, e.category, e.amount, e.description, e.retailor, e.date_of_purchase, e.status, e.school_year, e.semester, e.created_at') + ->where('e.school_year', $schoolYear); + if (!empty($invoiceDateFrom)) { + $expenseBuilder->where('DATE(e.created_at) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $expenseBuilder->where('DATE(e.created_at) <=', $invoiceDateTo); + } + $expenseRows = $expenseBuilder->orderBy('e.id', 'ASC')->get()->getResultArray(); + $expenses = array_map(static function ($row) { + return [ + 'Expense ID' => (string)($row['id'] ?? ''), + 'Category' => (string)($row['category'] ?? ''), + 'Amount' => (float)($row['amount'] ?? 0), + 'Description' => (string)($row['description'] ?? ''), + 'Vendor' => (string)($row['retailor'] ?? ''), + 'Date of Purchase' => (string)($row['date_of_purchase'] ?? ''), + 'Status' => (string)($row['status'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $expenseRows); + + $reimbursementBuilder = $db->table('reimbursements r') + ->select("r.id, r.expense_id, r.amount, r.status, r.description, r.reimbursement_method, r.check_number, r.batch_number, r.created_at, e.category AS expense_category, e.amount AS expense_amount, e.description AS expense_description, CONCAT(COALESCE(rec.firstname, ''), ' ', COALESCE(rec.lastname, '')) AS reimbursed_to_name") + ->join('expenses e', 'e.id = r.expense_id', 'left') + ->join('users rec', 'rec.id = r.reimbursed_to', 'left'); + if (!empty($schoolYear)) { + $reimbursementBuilder + ->groupStart() + ->groupStart() + ->where('r.school_year', $schoolYear) + ->orWhere( + "REPLACE(REPLACE(REPLACE(REPLACE(r.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '')", + $normalizedYear + ) + ->groupEnd() + ->orGroupStart() + ->where('r.school_year IS NULL', null, false) + ->groupStart() + ->where('e.school_year', $schoolYear) + ->orWhere( + "REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '')", + $normalizedYear + ) + ->groupEnd() + ->groupEnd() + ->groupEnd(); + } + if (!empty($invoiceDateFrom)) { + $reimbursementBuilder->where('DATE(COALESCE(r.created_at, e.created_at)) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $reimbursementBuilder->where('DATE(COALESCE(r.created_at, e.created_at)) <=', $invoiceDateTo); + } + $reimbursementRows = $reimbursementBuilder->orderBy('r.id', 'ASC')->get()->getResultArray(); + $reimbursements = array_map(static function ($row) { + return [ + 'Reimbursement ID' => (string)($row['id'] ?? ''), + 'Expense ID' => (string)($row['expense_id'] ?? ''), + 'Expense Category' => (string)($row['expense_category'] ?? ''), + 'Expense Amount' => (float)($row['expense_amount'] ?? 0), + 'Reimbursed Amount' => (float)($row['amount'] ?? 0), + 'Reimbursed To' => trim((string)($row['reimbursed_to_name'] ?? '')), + 'Status' => (string)($row['status'] ?? ''), + 'Method' => (string)($row['reimbursement_method'] ?? ''), + 'Check Number' => (string)($row['check_number'] ?? ''), + 'Batch Number' => (string)($row['batch_number'] ?? ''), + 'Description' => (string)($row['description'] ?? ''), + 'Expense Description' => (string)($row['expense_description'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $reimbursementRows); + + $donationExpenses = array_values(array_filter($expenses, static function ($row) { + return strtolower(trim((string)($row['Category'] ?? ''))) === 'donation'; + })); + + $donationReimbursements = []; + if (!empty($specialRecipientIds)) { + $donationBuilder = $db->table('reimbursements r') + ->select("r.id, r.expense_id, r.amount, r.created_at, r.description, CONCAT(COALESCE(rec.firstname, ''), ' ', COALESCE(rec.lastname, '')) AS reimbursed_to_name, e.category AS expense_category, e.description AS expense_description") + ->join('expenses e', 'e.id = r.expense_id', 'left') + ->join('users rec', 'rec.id = r.reimbursed_to', 'left') + ->whereIn('r.reimbursed_to', $specialRecipientIds); + if (!empty($schoolYear)) { + $donationBuilder + ->groupStart() + ->groupStart() + ->where('r.school_year', $schoolYear) + ->orWhere( + "REPLACE(REPLACE(REPLACE(REPLACE(r.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '')", + $normalizedYear + ) + ->groupEnd() + ->orGroupStart() + ->where('r.school_year IS NULL', null, false) + ->groupStart() + ->where('e.school_year', $schoolYear) + ->orWhere( + "REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '')", + $normalizedYear + ) + ->groupEnd() + ->groupEnd() + ->groupEnd(); + } + if (!empty($invoiceDateFrom)) { + $donationBuilder->where('DATE(COALESCE(r.created_at, e.created_at)) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $donationBuilder->where('DATE(COALESCE(r.created_at, e.created_at)) <=', $invoiceDateTo); + } + $donationReimbursements = array_map(static function ($row) { + return [ + 'Reimbursement ID' => (string)($row['id'] ?? ''), + 'Expense ID' => (string)($row['expense_id'] ?? ''), + 'Reimbursed To' => trim((string)($row['reimbursed_to_name'] ?? '')), + 'Amount' => (float)($row['amount'] ?? 0), + 'Expense Category' => (string)($row['expense_category'] ?? ''), + 'Expense Description' => (string)($row['expense_description'] ?? ''), + 'Description' => (string)($row['description'] ?? ''), + 'Created At' => (string)($row['created_at'] ?? ''), + ]; + }, $donationBuilder->orderBy('r.id', 'ASC')->get()->getResultArray()); + } + + $batchFallbackBuilder = $db->table('reimbursement_batch_items bi') + ->select("bi.batch_id, b.title AS batch_title, b.yearly_batch_number, e.id AS expense_id, e.category, e.amount, e.description, b.closed_at") + ->join('reimbursement_batches b', 'b.id = bi.batch_id', 'inner') + ->join('expenses e', 'e.id = bi.expense_id', 'inner') + ->join('reimbursements r', 'r.expense_id = e.id', 'left') + ->where('b.status', 'closed') + ->where('bi.unassigned_at IS NULL', null, false) + ->where('r.id IS NULL'); + if (!empty($schoolYear)) { + $batchFallbackBuilder + ->groupStart() + ->where('e.school_year', $schoolYear) + ->orWhere( + "REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '')", + $normalizedYear + ) + ->groupEnd(); + } + if (!empty($invoiceDateFrom)) { + $batchFallbackBuilder->where('DATE(COALESCE(e.created_at, b.closed_at)) >=', $invoiceDateFrom); + } + if (!empty($invoiceDateTo)) { + $batchFallbackBuilder->where('DATE(COALESCE(e.created_at, b.closed_at)) <=', $invoiceDateTo); + } + $reimbursementBatchFallbacks = array_map(static function ($row) { + $batchNumber = (string)($row['yearly_batch_number'] ?? ''); + if ($batchNumber === '') { + $batchNumber = (string)($row['batch_title'] ?? ''); + } + return [ + 'Batch ID' => (string)($row['batch_id'] ?? ''), + 'Batch Number' => $batchNumber, + 'Expense ID' => (string)($row['expense_id'] ?? ''), + 'Category' => (string)($row['category'] ?? ''), + 'Amount' => (float)($row['amount'] ?? 0), + 'Description' => (string)($row['description'] ?? ''), + 'Closed At' => (string)($row['closed_at'] ?? ''), + ]; + }, $batchFallbackBuilder->orderBy('bi.batch_id', 'ASC')->get()->getResultArray()); + + return [ + 'invoices' => $invoices, + 'discounts' => $discounts, + 'payments' => $payments, + 'refunds' => $refunds, + 'eventCharges' => $eventCharges, + 'carryovers' => $carryovers, + 'unpaidParents' => $this->getUnpaidParentsDetails($schoolYear), + 'expenses' => $expenses, + 'reimbursements' => $reimbursements, + 'donationExpenses' => $donationExpenses, + 'donationReimbursements' => $donationReimbursements, + 'reimbursementBatchFallbacks' => $reimbursementBatchFallbacks, + ]; + } + + private function getUnpaidParentsDetails(string $schoolYear): array + { + if ($schoolYear === '') { + return []; + } + + $db = \Config\Database::connect(); + $invRows = $db->table('invoices i') + ->select('i.id, i.parent_id, i.total_amount, u.firstname, u.lastname, u.email') + ->join('users u', 'u.id = i.parent_id', 'inner') + ->where('i.school_year', $schoolYear) + ->orderBy('i.parent_id', 'ASC') + ->orderBy('i.id', 'ASC') + ->get()->getResultArray(); + + $byParent = []; + $paymentsTbl = 'payments'; + $hasStatus = $db->fieldExists('status', $paymentsTbl); + $hasVoid = $db->fieldExists('is_void', $paymentsTbl); + + foreach ($invRows as $r) { + $iid = (int)($r['id'] ?? 0); + $pid = (int)($r['parent_id'] ?? 0); + if ($iid <= 0 || $pid <= 0) { + continue; + } + + $qb = $db->table('payments')->select('COALESCE(SUM(paid_amount),0) AS tot')->where('invoice_id', $iid); + if ($hasStatus) { + $qb->groupStart() + ->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled']) + ->orWhere('status IS NULL', null, false) + ->groupEnd(); + } + if ($hasVoid) { + $qb->groupStart() + ->where('is_void', 0) + ->orWhere('is_void IS NULL', null, false) + ->groupEnd(); + } + $paidRow = $qb->get()->getRowArray(); + $paidSum = (float)($paidRow['tot'] ?? 0); + + $discRow = $db->table('discount_usages') + ->select('COALESCE(SUM(discount_amount),0) AS tot') + ->where('invoice_id', $iid) + ->get()->getRowArray(); + $discSum = (float)($discRow['tot'] ?? 0); + + $refRow = $db->table('refunds') + ->select('COALESCE(SUM(refund_paid_amount),0) AS tot') + ->where('invoice_id', $iid) + ->whereIn('status', ['Partial','Paid']) + ->get()->getRowArray(); + $refSum = (float)($refRow['tot'] ?? 0); + + $total = (float)($r['total_amount'] ?? 0); + $balance = max(0.0, round($total - $discSum - $paidSum - $refSum, 2)); + + if (!isset($byParent[$pid])) { + $byParent[$pid] = [ + 'parent_id' => $pid, + 'firstname' => (string)($r['firstname'] ?? ''), + 'lastname' => (string)($r['lastname'] ?? ''), + 'email' => (string)($r['email'] ?? ''), + 'total_invoice' => 0.0, + 'total_balance' => 0.0, + 'total_paid' => 0.0, + 'total_discount' => 0.0, + ]; + } + $byParent[$pid]['total_invoice'] += $total; + $byParent[$pid]['total_balance'] += $balance; + $byParent[$pid]['total_paid'] += $paidSum; + $byParent[$pid]['total_discount']+= $discSum; + } + + $extraRows = $db->table('additional_charges ac') + ->select( + "ac.parent_id, + u.firstname, + u.lastname, + u.email, + COALESCE(SUM(ac.amount), 0) AS total_extra", + false + ) + ->join('users u', 'u.id = ac.parent_id', 'left') + ->where('ac.school_year', $schoolYear) + ->where('ac.status !=', 'void') + ->groupStart() + ->where('ac.invoice_id IS NULL', null, false) + ->orWhere('ac.invoice_id', 0) + ->orWhere('ac.status !=', 'applied') + ->groupEnd() + ->groupBy('ac.parent_id') + ->get() + ->getResultArray(); + + foreach ($extraRows as $er) { + $pid = (int)($er['parent_id'] ?? 0); + if ($pid <= 0) { + continue; + } + $extra = (float)($er['total_extra'] ?? 0); + if (abs($extra) < 0.00001) { + continue; + } + if (!isset($byParent[$pid])) { + $byParent[$pid] = [ + 'parent_id' => $pid, + 'firstname' => (string)($er['firstname'] ?? ''), + 'lastname' => (string)($er['lastname'] ?? ''), + 'email' => (string)($er['email'] ?? ''), + 'total_invoice' => 0.0, + 'total_balance' => 0.0, + 'total_paid' => 0.0, + 'total_discount' => 0.0, + ]; + } + $byParent[$pid]['total_invoice'] += $extra; + $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) { + } + + $installmentEndRaw = (string) (new \App\Models\ConfigurationModel())->getConfig('installment_date'); + $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); + $tzNY = new \DateTimeZone($tzName); + $todayNY = new \DateTimeImmutable('today', $tzNY); + $endDate = null; + if ($installmentEndRaw) { + try { + $endDate = new \DateTimeImmutable($installmentEndRaw, $tzNY); + } catch (\Throwable $e) { + $endDate = null; + } + } + $monthsUntil = function (?\DateTimeImmutable $end) use ($todayNY): int { + if (!$end) { + return 0; + } + $y = (int)$end->format('Y') - (int)$todayNY->format('Y'); + $m = (int)$end->format('n') - (int)$todayNY->format('n'); + $months = $y * 12 + $m; + if ((int)$end->format('j') > (int)$todayNY->format('j')) { + $months += 1; + } + return max(0, $months); + }; + + $rows = []; + foreach ($byParent as $pid => $agg) { + $balance = (float)($agg['total_balance'] ?? 0); + if ($balance > 0.00001) { + $remMonths = $monthsUntil($endDate); + $remainingInstallments = max(1, $remMonths); + $instAmount = round($balance / $remainingInstallments, 2); + + $rows[] = [ + 'parent_id' => $pid, + 'firstname' => $agg['firstname'], + 'lastname' => $agg['lastname'], + 'email' => $agg['email'], + 'total_invoice' => (float)$agg['total_invoice'], + 'total_balance' => $balance, + 'total_discount' => (float)($agg['total_discount'] ?? 0), + 'remaining_installments' => $remainingInstallments, + 'installment_amount' => $instAmount, + ]; + } + } + + usort($rows, static function ($a, $b) { + return ($b['total_balance'] <=> $a['total_balance']); + }); + + $tz = new \DateTimeZone($tzName); + $now = new \DateTime('now', $tz); + $nextInstallmentDt = (clone $now)->modify('first day of next month'); + $nextInstallmentYmd = $nextInstallmentDt->format('Y-m-d'); + + $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, COUNT(*) AS payment_count') + ->whereIn('parent_id', $parentIds) + ->where('school_year', $schoolYear) + ->groupBy('parent_id') + ->get()->getResultArray(); + foreach ($pRows as $pr) { + $pid = (int)($pr['parent_id'] ?? 0); + if ($pid > 0) { + $hasPayments[$pid] = true; + $paidTotals[$pid] = (float)($pr['total_paid'] ?? 0); + $paymentCounts[$pid] = (int)($pr['payment_count'] ?? 0); + } + } + } catch (\Throwable $e) { + } + } + + return 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 [ + 'Parent' => $name, + 'Parent ID' => $pid, + 'Email' => (string)($r['email'] ?? ''), + 'Type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment', + 'Event Fees' => (float)($eventFeesPerParent[$pid] ?? 0), + 'Invoice Amount' => (float)($r['total_invoice'] ?? 0), + 'Applied Discount' => (float)($r['total_discount'] ?? 0), + 'Paid Amount' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0, + 'Nbr of Installments' => isset($paymentCounts[$pid]) ? (int)$paymentCounts[$pid] : 0, + 'Remaining Installments' => (int)($r['remaining_installments'] ?? 0), + 'Installment Amount' => (float)($r['installment_amount'] ?? 0), + 'Has Installment' => isset($hasPayments[$pid]) ? 1 : 0, + 'Next Installment' => $nextInstallmentYmd, + 'Outstanding Balance' => (float)($r['total_balance'] ?? 0), + ]; + }, $rows); + } + public function downloadCsv() { @@ -637,7 +1490,7 @@ public function financialReport() $rows = [ 'Total Charges' => $data['totalCharges'], - 'Total Extra Charges' => $data['totalExtraCharges'] ?? 0, + 'Prior-Year Tuition Carryover' => $data['totalExtraCharges'] ?? 0, 'Total Discounts' => $data['totalDiscounts'], 'Total Refunds' => $data['totalRefunds'], 'Total Expenses' => $data['totalExpenses'], @@ -769,7 +1622,10 @@ public function financialReport() $db = \Config\Database::connect(); // === Invoices: Total Charges === - $invoiceBuilder = $invoiceModel->where('school_year', $schoolYear); + $invoiceBuilder = $invoiceModel + ->select("invoices.*, CONCAT(COALESCE(users.firstname, ''), ' ', COALESCE(users.lastname, '')) AS parent_name") + ->join('users', 'users.id = invoices.parent_id', 'left') + ->where('invoices.school_year', $schoolYear); $invoiceDateFrom = $dateFrom ?: $derivedDateFrom; $invoiceDateTo = $dateTo ?: $derivedDateTo; if (!empty($invoiceDateFrom)) { @@ -787,12 +1643,16 @@ public function financialReport() // === Additional Charges === $hasExplicitDates = !empty($dateFrom) || !empty($dateTo); - $sumExpr = "COALESCE(SUM(ac.amount), 0) AS amount"; + $signedAdditionalChargeExpr = "CASE + WHEN LOWER(COALESCE(ac.charge_type, '')) = 'deduct' THEN -ABS(ac.amount) + ELSE ABS(ac.amount) + END"; + $sumExpr = "COALESCE(SUM({$signedAdditionalChargeExpr}), 0) AS amount"; $acAllBuilder = $db->table('additional_charges ac') ->select($sumExpr, false) ->where('ac.school_year', $schoolYear) - ->where('ac.status !=', 'void'); + ->whereNotIn('ac.status', ['void', 'voided']); if ($hasExplicitDates) { if (!empty($dateFrom)) $acAllBuilder->where('DATE(ac.due_date) >=', $dateFrom); if (!empty($dateTo)) $acAllBuilder->where('DATE(ac.due_date) <=', $dateTo); @@ -806,7 +1666,7 @@ public function financialReport() $acExtraBuilder = $db->table('additional_charges ac') ->select('ac.parent_id, ' . $sumExpr, false) ->where('ac.school_year', $schoolYear) - ->where('ac.status !=', 'void') + ->whereNotIn('ac.status', ['void', 'voided']) ->groupStart() ->where('ac.invoice_id IS NULL', null, false) ->orWhere('ac.invoice_id', 0) @@ -1091,9 +1951,10 @@ public function financialReport() $discountResult = $discountBuilder->selectSum('discount_amount')->get()->getRowArray(); $totalDiscounts = isset($discountResult['discount_amount']) ? (float) $discountResult['discount_amount'] : 0.00; - // === Net & Unpaid === - // Sum positive per-parent balances (per-invoice balance + unapplied extra charges) - $parentBalances = []; + // === Net, Outstanding & Overpayments === + $overpaymentDetails = []; + $totalUnpaid = 0.0; + $totalOverpaid = 0.0; foreach ($invoices as $inv) { $iid = (int)($inv['id'] ?? 0); $pid = (int)($inv['parent_id'] ?? 0); @@ -1102,26 +1963,62 @@ public function financialReport() $paid = (float)($paidByInvoice[$iid] ?? 0); $disc = (float)($discountByInvoice[$iid] ?? 0); $ref = (float)($refundByInvoice[$iid] ?? 0); - $bal = max(0.0, round($total - $disc - $paid - $ref, 2)); - if (!isset($parentBalances[$pid])) $parentBalances[$pid] = 0.0; - $parentBalances[$pid] += $bal; + $rawBal = round($total - $disc - $paid - $ref, 2); + if ($rawBal > 0.00001) { + $totalUnpaid += $rawBal; + } elseif ($rawBal < -0.00001) { + $credit = abs($rawBal); + $totalOverpaid += $credit; + $overpaymentDetails[] = [ + 'type' => 'invoice', + 'parent_id' => $pid, + 'parent_name' => trim((string)($inv['parent_name'] ?? '')), + 'invoice_id' => $iid, + 'invoice_number' => (string)($inv['invoice_number'] ?? ''), + 'credit_amount' => $credit, + 'gross_amount' => $total, + 'discount_amount' => $disc, + 'refund_amount' => $ref, + 'paid_amount' => $paid, + 'note' => 'Invoice payments/discounts exceed net invoice charges.', + ]; + } } foreach ($extraByParent as $pid => $extra) { - if (!isset($parentBalances[$pid])) $parentBalances[$pid] = 0.0; - $parentBalances[$pid] += (float)$extra; - } - $totalUnpaid = 0.0; - foreach ($parentBalances as $bal) { - if ($bal > 0.00001) $totalUnpaid += $bal; + $extra = round((float)$extra, 2); + if ($extra > 0.00001) { + $totalUnpaid += $extra; + } elseif ($extra < -0.00001) { + $credit = abs($extra); + $totalOverpaid += $credit; + $overpaymentDetails[] = [ + 'type' => 'adjustment', + 'parent_id' => (int)$pid, + 'parent_name' => '', + 'invoice_id' => null, + 'invoice_number' => '', + 'credit_amount' => $credit, + 'gross_amount' => 0.0, + 'discount_amount' => 0.0, + 'refund_amount' => 0.0, + 'paid_amount' => 0.0, + 'note' => 'Negative unapplied carryover adjustment.', + ]; + } } $amountCollected = $totalPaid; - $netAmount = ($totalCharges - $totalDiscounts - $totalRefunds); + $grossCharges = round((float)$totalCharges, 2); + $netAmount = round($grossCharges - (float)$totalDiscounts - (float)$totalRefunds, 2); $totalEventFees = $this->getEventFeesTotal($schoolYear, $invoiceDateFrom, $invoiceDateTo); + $tuitionCharges = round($grossCharges - (float)$totalEventFees - (float)$totalExtraCharges, 2); + $netReceivable = max(0.0, round((float)$totalUnpaid - (float)$totalOverpaid, 2)); return [ 'schoolYear' => $schoolYear, 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, + 'grossCharges' => $grossCharges, + 'tuitionCharges' => $tuitionCharges, 'totalCharges' => $totalCharges, 'totalExtraCharges' => $totalExtraCharges, 'totalDiscounts' => $totalDiscounts, @@ -1132,6 +2029,9 @@ public function financialReport() 'totalPaid' => $totalPaid, 'amountCollected' => $amountCollected, 'totalUnpaid' => $totalUnpaid, + 'totalOverpaid' => $totalOverpaid, + 'overpaymentDetails' => $overpaymentDetails, + 'netReceivable' => $netReceivable, 'netAmount' => $netAmount, 'totalEventFees' => $totalEventFees, ]; diff --git a/app/Views/payment/financial_report_summary.php b/app/Views/payment/financial_report_summary.php index 6ff0f2b..24a8d7e 100644 --- a/app/Views/payment/financial_report_summary.php +++ b/app/Views/payment/financial_report_summary.php @@ -7,6 +7,8 @@
+ +