diff --git a/app/Config/Routes.php b/app/Config/Routes.php index fad6d96..9a27ab3 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -535,6 +535,8 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate'); // Parent event participation $routes->get('administrator/event-charges', 'View\EventController::eventShow'); +$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1'); +$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1'); $routes->get('administrator/get-students-with-charges', 'View\EventController::getStudentsWithCharges'); $routes->get('parent/events', 'View\ParentController::parentEventPage'); diff --git a/app/Controllers/View/DiscountController.php b/app/Controllers/View/DiscountController.php index 85fbf27..e87c073 100644 --- a/app/Controllers/View/DiscountController.php +++ b/app/Controllers/View/DiscountController.php @@ -750,7 +750,9 @@ class DiscountController extends BaseController } } catch (\Throwable $e) {} - $newTotal = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2); + $discountableTotal = $tuitionSubtotal + $additionalSubtotal; + $nonDiscountableTotal = $eventSubtotal; + $newTotal = round($discountableTotal + $nonDiscountableTotal, 2); // ---- Payments / Discounts / Refunds ---- $db = $this->db; @@ -794,7 +796,8 @@ class DiscountController extends BaseController ->get()->getRowArray(); $totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0); - $newBalance = max(0.0, $newTotal - $totalDisc - $totalPaid - $totalRefundPaid); + $appliedDiscount = min($totalDisc, $discountableTotal); + $newBalance = max(0.0, $newTotal - $appliedDiscount - $totalPaid - $totalRefundPaid); $newStatus = ($newBalance <= 0.00001) ? 'Paid' : (($totalPaid > 0) ? 'Partially Paid' : 'Unpaid'); $updateData = [ diff --git a/app/Controllers/View/EventController.php b/app/Controllers/View/EventController.php index f61110d..ad195f8 100644 --- a/app/Controllers/View/EventController.php +++ b/app/Controllers/View/EventController.php @@ -6,8 +6,14 @@ use App\Models\EventModel; use App\Models\EventChargesModel; use App\Models\UserModel; use App\Models\StudentModel; +use App\Models\StudentClassModel; use App\Models\ConfigurationModel; use App\Models\EnrollmentModel; +use App\Models\InvoiceModel; +use App\Models\InvoiceEventModel; +use App\Models\ClassSectionModel; +use App\Models\PaymentModel; +#use ??? use App\Controllers\View\InvoiceController; use CodeIgniter\RESTful\ResourceController; @@ -19,6 +25,11 @@ class EventController extends ResourceController protected $configModel; protected $eventModel; protected $invoiceController; + protected $invoiceModel; + protected $invoiceEventModel; + protected $studentClassModel; + protected $classSectionModel; + protected $paymentModel; protected $schoolYear; protected $semester; protected $categories; @@ -32,6 +43,11 @@ class EventController extends ResourceController $this->userModel = new UserModel(); // Add this $this->eventModel = new EventModel(); $this->invoiceController = new InvoiceController(); + $this->invoiceModel = new InvoiceModel(); + $this->invoiceEventModel = new InvoiceEventModel(); + $this->studentClassModel = new StudentClassModel(); + $this->classSectionModel = new ClassSectionModel(); + $this->paymentModel = new PaymentModel(); $this->enrollmentModel = new EnrollmentModel(); $this->schoolYear = $this->configModel->getConfig('school_year'); @@ -166,6 +182,19 @@ class EventController extends ResourceController $parentIds[] = $charge['parent_id']; } + // Also remove any invoice_event rows tied to this event + $eventName = $event['event_name'] ?? null; + if ($eventName) { + $builder = $this->invoiceEventModel->where('event_name', $eventName); + if (!empty($event['school_year'])) { + $builder->where('school_year', $event['school_year']); + } + if (!empty($event['semester'])) { + $builder->where('semester', $event['semester']); + } + $builder->delete(); + } + // Delete event $this->eventModel->delete($id); @@ -189,6 +218,7 @@ class EventController extends ResourceController $parents = $this->userModel->getParents(); $events = $this->eventModel->getActiveEvents($this->schoolYear); $filterEventId = (int) ($this->request->getGet('event_id') ?? 0); + $filterParentId = (int) ($this->request->getGet('parent_id') ?? 0); $chargesBuilder = $this->eventChargesModel ->select('event_charges.*, @@ -209,6 +239,67 @@ class EventController extends ResourceController ->orderBy('event_charges.created_at', 'DESC') ->findAll(); + foreach ($charges as &$charge) { + if (empty($charge['class_section_id']) && !empty($charge['student_id'])) { + $sections = $this->studentClassModel->getClassSectionIdsByStudentId( + (int)$charge['student_id'], + $schoolYear + ); + if (!empty($sections)) { + $charge['class_section_id'] = $sections[0]; + } + } + } + unset($charge); + + $parentBalances = []; + try { + $balanceRows = $this->invoiceModel + ->select('parent_id, COALESCE(SUM(balance),0) AS total_balance') + ->where('school_year', $schoolYear) + ->where('semester', $semester) + ->groupBy('parent_id') + ->findAll(); + + foreach ($balanceRows as $row) { + $parentBalances[(int)($row['parent_id'] ?? 0)] = (float)($row['total_balance'] ?? 0.0); + } + } catch (\Throwable $e) { + log_message('error', 'Failed to load parent balances: ' . $e->getMessage()); + } + + $sectionIds = array_unique(array_filter(array_column($charges, 'class_section_id'))); + $classSectionNames = []; + if (!empty($sectionIds)) { + $sections = $this->classSectionModel + ->select('class_section_id, class_section_name') + ->whereIn('class_section_id', $sectionIds) + ->findAll(); + foreach ($sections as $section) { + $classSectionNames[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; + } + } + + $semesterOptions = (new EventChargesModel()) + ->select('semester') + ->distinct() + ->orderBy('semester', 'ASC') + ->findColumn('semester'); + $semesterOptions = array_values(array_filter(array_unique($semesterOptions ?? []))); + if (empty($semesterOptions)) { + $semesterOptions = [$this->semester]; + } + + $schoolYearOptions = (new EventChargesModel()) + ->select('school_year') + ->distinct() + ->orderBy('school_year', 'ASC') + ->findColumn('school_year'); + $schoolYearOptions = array_values(array_filter(array_unique($schoolYearOptions ?? []))); + if (empty($schoolYearOptions)) { + $schoolYearOptions = [$this->schoolYear]; + } + return view('administrator/events/event_charges', [ 'charges' => $charges, 'parents' => $parents, @@ -216,6 +307,11 @@ class EventController extends ResourceController 'school_year' => $schoolYear, 'semester' => $semester, 'filterEventId' => $filterEventId, + 'filterParentId' => $filterParentId, + 'parentBalances' => $parentBalances, + 'classSectionNames' => $classSectionNames, + 'semesterOptions' => $semesterOptions, + 'schoolYearOptions' => $schoolYearOptions, ]); } @@ -252,13 +348,20 @@ class EventController extends ResourceController continue; } - // value is 'yes' + $sectionIds = $this->studentClassModel->getClassSectionIdsByStudentId($studentId, $schoolYear); + $classSectionId = !empty($sectionIds) ? $sectionIds[0] : null; + if ($existing) { - $this->eventChargesModel->update($existing['id'], [ - 'participation' => 'yes', - 'charged' => $event['amount'], - 'updated_by' => $userId - ]); + $updateData = [ + 'participation' => 'yes', + 'updated_by' => $userId, + 'class_section_id'=> $classSectionId, + ]; + if (isset($event['amount']) && ((float)$event['amount'] !== (float)($existing['charged'] ?? 0))) { + $updateData['charged'] = $event['amount']; + } + $this->eventChargesModel->update($existing['id'], $updateData); + continue; } else { $this->eventChargesModel->insert([ 'parent_id' => $parentId, @@ -266,6 +369,8 @@ class EventController extends ResourceController 'event_id' => $eventId, 'participation' => 'yes', 'charged' => $event['amount'], + 'event_paid' => 0, + 'class_section_id' => $classSectionId, 'school_year' => $schoolYear, 'semester' => $semester, 'created_by' => $userId, @@ -276,9 +381,90 @@ class EventController extends ResourceController $this->invoiceController->generateInvoice($parentId); + $this->fixInvoiceStatusAfterCharge($parentId, $schoolYear); + return redirect()->back()->with('success', 'Event charges updated successfully.'); } + public function removeCharge($chargeId = null) + { + if (!$chargeId) { + return redirect()->back()->with('error', 'Invalid charge.'); + } + + $charge = $this->eventChargesModel->find($chargeId); + if (!$charge) { + return redirect()->back()->with('error', 'Charge not found.'); + } + + $paymentId = (int)($charge['event_payment_id'] ?? 0); + if ($paymentId > 0) { + $this->paymentModel->delete($paymentId); + } + + $this->eventChargesModel->delete($chargeId); + $this->invoiceController->generateInvoice($charge['parent_id']); + + return redirect()->back()->with('success', 'Participation removed, invoices updated.'); + } + + public function toggleEventPayment($chargeId = null) + { + if (!$chargeId) { + return redirect()->back()->with('error', 'Invalid charge.'); + } + + $charge = $this->eventChargesModel->find($chargeId); + if (!$charge) { + return redirect()->back()->with('error', 'Charge not found.'); + } + + $isPaid = $this->request->getPost('paid') === '1'; + $event = $this->eventModel->find($charge['event_id']); + $eventAmount = max(0.0, (float)($event['amount'] ?? 0)); + $paymentId = (int)($charge['event_payment_id'] ?? 0); + + $parentId = $charge['parent_id']; + $schoolYear = $charge['school_year'] ?? $this->schoolYear; + + if (!empty($eventAmount)) { + $invoice = $this->invoiceModel + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->orderBy('id', 'DESC') + ->first(); + + if (!$invoice) { + $this->invoiceController->generateInvoice($parentId); + $invoice = $this->invoiceModel + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->orderBy('id', 'DESC') + ->first(); + } + + if ($invoice) { + if ($isPaid && !$paymentId) { + $paymentId = $this->createEventPayment($parentId, (int)$invoice['id'], $eventAmount, $schoolYear, $charge['semester'] ?? $this->semester); + } elseif (!$isPaid && $paymentId > 0) { + $this->paymentModel->delete($paymentId); + $paymentId = 0; + } + } + } + + $this->eventChargesModel->update($chargeId, [ + 'event_paid' => $isPaid ? 1 : 0, + 'charged' => $eventAmount, + 'event_payment_id' => $paymentId > 0 ? $paymentId : null, + ]); + + $this->invoiceController->generateInvoice($parentId); + $this->fixInvoiceStatusAfterCharge($parentId, $schoolYear); + + return redirect()->back()->with('success', 'Event payment status updated.'); + } + @@ -292,10 +478,18 @@ class EventController extends ResourceController $students = $this->studentModel->where('parent_id', $parentId)->findAll(); // Get student_ids that already have charges - $chargedStudentIds = $this->eventChargesModel + $eventId = $this->request->getGet('event_id'); + + $chargesBuilder = $this->eventChargesModel ->where('parent_id', $parentId) ->where('semester', $semester) - ->where('school_year', $schoolYear) + ->where('school_year', $schoolYear); + + if (!empty($eventId)) { + $chargesBuilder->where('event_id', $eventId); + } + + $chargedStudentIds = $chargesBuilder ->groupBy('student_id') ->select('student_id') ->findColumn('student_id'); @@ -312,5 +506,58 @@ class EventController extends ResourceController return $this->response->setJSON($data); } + private function createEventPayment(int $parentId, int $invoiceId, float $amount, string $schoolYear, ?string $semester): ?int + { + if ($amount <= 0 || $invoiceId <= 0 || $parentId <= 0) { + return null; + } + + $monthSemester = $semester ?: $this->semester; + $data = [ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'total_amount' => $amount, + 'paid_amount' => $amount, + 'balance' => 0.0, + 'number_of_installments' => 1, + 'payment_method' => 'Event charge', + 'payment_date' => utc_now(), + 'school_year' => $schoolYear, + 'semester' => $monthSemester, + 'status' => 'Paid', + 'transaction_id' => $this->paymentModel->generateNewTransactionId(), + 'updated_by' => session()->get('user_id'), + ]; + + if (!$this->paymentModel->insert($data)) { + log_message('error', 'Failed to insert event payment: ' . json_encode($this->paymentModel->errors())); + return null; + } + + return (int)$this->paymentModel->getInsertID(); + } + + private function fixInvoiceStatusAfterCharge($parentId, $schoolYear) + { + if (!$parentId || !$schoolYear) { + return; + } + + $invoices = $this->invoiceModel + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->findAll(); + + foreach ($invoices as $invoice) { + $status = strtolower(trim($invoice['status'] ?? '')); + $balance = (float)($invoice['balance'] ?? 0.0); + + if ($balance <= 0.00001 && $status !== 'paid') { + $this->invoiceModel->update($invoice['id'], ['status' => 'Paid']); + } elseif ($status === 'paid' && $balance > 0) { + $this->invoiceModel->update($invoice['id'], ['status' => 'Unpaid']); + } + } + } } diff --git a/app/Controllers/View/InvoiceController.php b/app/Controllers/View/InvoiceController.php index a3c6829..04232d4 100644 --- a/app/Controllers/View/InvoiceController.php +++ b/app/Controllers/View/InvoiceController.php @@ -678,7 +678,7 @@ class InvoiceController extends ResourceController log_message('error', 'additional_charges sum failed for discount recalculation invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage()); } - $baseTotal = round($tuitionFee + $eventchargeTotal + $extrasSum, 2); + $baseTotal = round($tuitionFee + $extrasSum, 2); // Recalculate discount if ($discountUsage['discount_type'] === 'percent') { @@ -1612,12 +1612,24 @@ private function getGradeLevel($grade): array $invoice['last_payment_date'] = $lastPayments[$invoice['id']]['last_payment_date'] ?? null; } + $invoiceEventCharges = []; + foreach ($invoices as $invoice) { + $year = $invoice['school_year'] ?? $this->schoolYear; + $sem = $invoice['semester'] ?? $this->semester; + $invoiceEventCharges[(int)$invoice['id']] = $this->chargesModel->getChargesWithEventInfo( + $invoice['parent_id'], + $year, + $sem + ); + } + return view('/parent/invoice_payment', [ 'invoices' => $invoices, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'currentSchoolYear' => $currentSchoolYear, - 'dueDate' => $this->dueDate + 'dueDate' => $this->dueDate, + 'invoiceEventCharges' => $invoiceEventCharges, ]); } diff --git a/app/Database/Migrations/2026-02-16-000001_AddEventPaidFlagToCharges.php b/app/Database/Migrations/2026-02-16-000001_AddEventPaidFlagToCharges.php new file mode 100644 index 0000000..b957885 --- /dev/null +++ b/app/Database/Migrations/2026-02-16-000001_AddEventPaidFlagToCharges.php @@ -0,0 +1,27 @@ + [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 0, + 'after' => 'charged', + ], + ]; + + $this->forge->addColumn('event_charges', $fields); + } + + public function down() + { + $this->forge->dropColumn('event_charges', 'event_paid'); + } +} diff --git a/app/Database/Migrations/2026-04-13-000002_AddClassSectionToEventCharges.php b/app/Database/Migrations/2026-04-13-000002_AddClassSectionToEventCharges.php new file mode 100644 index 0000000..b93d2f5 --- /dev/null +++ b/app/Database/Migrations/2026-04-13-000002_AddClassSectionToEventCharges.php @@ -0,0 +1,32 @@ + [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'null' => true, + 'after' => 'event_paid', + ], + ]; + + if (! $this->db->fieldExists('class_section_id', 'event_charges')) { + $this->forge->addColumn('event_charges', $fields); + } + } + + public function down() + { + if ($this->db->fieldExists('class_section_id', 'event_charges')) { + $this->forge->dropColumn('event_charges', 'class_section_id'); + } + } +} diff --git a/app/Database/Migrations/2026-04-13-000003_AddEventPaymentRefToEventCharges.php b/app/Database/Migrations/2026-04-13-000003_AddEventPaymentRefToEventCharges.php new file mode 100644 index 0000000..8853af3 --- /dev/null +++ b/app/Database/Migrations/2026-04-13-000003_AddEventPaymentRefToEventCharges.php @@ -0,0 +1,32 @@ + [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'null' => true, + 'after' => 'class_section_id', + ], + ]; + + if (! $this->db->fieldExists('event_payment_id', 'event_charges')) { + $this->forge->addColumn('event_charges', $fields); + } + } + + public function down() + { + if ($this->db->fieldExists('event_payment_id', 'event_charges')) { + $this->forge->dropColumn('event_charges', 'event_payment_id'); + } + } +} diff --git a/app/Models/EventChargesModel.php b/app/Models/EventChargesModel.php index 3c5dde5..aadd00d 100644 --- a/app/Models/EventChargesModel.php +++ b/app/Models/EventChargesModel.php @@ -15,6 +15,9 @@ class EventChargesModel extends Model 'student_id', 'participation', 'charged', + 'event_paid', + 'event_payment_id', + 'class_section_id', 'semester', 'school_year', 'updated_by', diff --git a/app/Views/administrator/events/event_charges.php b/app/Views/administrator/events/event_charges.php index fca7e83..c28c292 100644 --- a/app/Views/administrator/events/event_charges.php +++ b/app/Views/administrator/events/event_charges.php @@ -12,6 +12,14 @@ + + +
+
+
+ 0): ?> + + + 0): ?> + + +
+ + +
+
+ + +
+
+ +
+
+
+
+
-
+
@@ -60,16 +106,13 @@
- -
- -
+
+ + onchange="this.form.paid.value = this.checked ? 1 : 0; this.form.submit();"> + + + + +
+ + Edit + +
+ + +
+
+ + + + + Totals + $ + + + participating + + + +
@@ -214,6 +308,10 @@ $(function() { } window.location.href = url.toString(); }); + + if ($('#event_id').val() && $('#parent_id').val()) { + loadStudentsWithCharges(); + } }); endSection() ?> diff --git a/app/Views/parent/invoice_payment.php b/app/Views/parent/invoice_payment.php index 7cc2563..64a1e29 100644 --- a/app/Views/parent/invoice_payment.php +++ b/app/Views/parent/invoice_payment.php @@ -143,12 +143,61 @@ $deadline = parseDbDateTime($dueDate, 'UTC', $displayTz); // format either DATE
+ + + + + + +
+
+ Event charges for Invoice # +
+
+
+ + + + + + + + + + + + + + + + + + + + +
EventStudentFeeStatus
$ + + + +
+
+
+
+ +
No invoice found for the selected school year.
+ + endSection() ?> section('scripts') ?> diff --git a/public/uploads/event_flyers/1775954297_1397d04bd23a3687a1a2.png b/public/uploads/event_flyers/1775954297_1397d04bd23a3687a1a2.png new file mode 100644 index 0000000..e97a976 Binary files /dev/null and b/public/uploads/event_flyers/1775954297_1397d04bd23a3687a1a2.png differ