942 lines
36 KiB
PHP
942 lines
36 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
|
|
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 App\Models\CalendarModel;
|
|
use Config\Database;
|
|
#use ???
|
|
use App\Controllers\View\InvoiceController;
|
|
use CodeIgniter\RESTful\ResourceController;
|
|
|
|
class EventController extends ResourceController
|
|
{
|
|
protected $eventChargesModel;
|
|
protected $studentModel;
|
|
protected $userModel;
|
|
protected $configModel;
|
|
protected $eventModel;
|
|
protected $invoiceController;
|
|
protected $invoiceModel;
|
|
protected $invoiceEventModel;
|
|
protected $studentClassModel;
|
|
protected $classSectionModel;
|
|
protected $paymentModel;
|
|
protected $schoolYear;
|
|
protected $semester;
|
|
protected $categories;
|
|
protected $enrollmentModel;
|
|
private ?bool $eventChargesHasCreatedBy = null;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->eventChargesModel = new EventChargesModel(); //eventChargesModel
|
|
$this->studentModel = new StudentModel();
|
|
$this->configModel = new ConfigurationModel();
|
|
$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');
|
|
$this->semester = $this->configModel->getConfig('semester');
|
|
$this->categories = [
|
|
'workshops',
|
|
'orientations',
|
|
'field trips',
|
|
'Ramadan programs',
|
|
];
|
|
}
|
|
|
|
private function eventChargesSupportsCreatedBy(): bool
|
|
{
|
|
if ($this->eventChargesHasCreatedBy !== null) {
|
|
return $this->eventChargesHasCreatedBy;
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
$this->eventChargesHasCreatedBy = $db->fieldExists('created_by', 'event_charges');
|
|
} catch (\Throwable $e) {
|
|
$this->eventChargesHasCreatedBy = false;
|
|
}
|
|
|
|
return $this->eventChargesHasCreatedBy;
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$eventModel = new EventModel();
|
|
|
|
$today = local_date(utc_now(), 'Y-m-d');
|
|
|
|
// Fetch all events
|
|
$events = $eventModel
|
|
->orderBy('created_at', 'DESC')
|
|
->findAll();
|
|
|
|
// Fetch active events (not expired)
|
|
$activeEventCount = $eventModel
|
|
->where('expiration_date >=', $today)
|
|
->countAllResults();
|
|
|
|
return view('administrator/events/event_list', [
|
|
'events' => $events,
|
|
'activeEventCount' => $activeEventCount
|
|
]);
|
|
}
|
|
|
|
|
|
public function create()
|
|
{
|
|
helper(['form']);
|
|
if (strtolower($this->request->getMethod()) === 'post') {
|
|
$file = $this->request->getFile('flyer');
|
|
$flyerPath = null;
|
|
|
|
if ($file && $file->isValid() && !$file->hasMoved()) {
|
|
// Move to public/uploads/event_flyers
|
|
$newName = $file->getRandomName();
|
|
$file->move(FCPATH . 'uploads/event_flyers', $newName);
|
|
$flyerPath = 'event_flyers/' . $newName; // store relative path
|
|
}
|
|
|
|
$eventId = $this->eventModel->insert([
|
|
'event_name' => $this->request->getPost('event_name'),
|
|
'event_category' => $this->request->getPost('event_category'),
|
|
'description' => $this->request->getPost('description'),
|
|
'amount' => $this->request->getPost('amount'),
|
|
'flyer' => $flyerPath,
|
|
'expiration_date' => $this->request->getPost('expiration_date'),
|
|
'semester' => $this->request->getPost('semester'),
|
|
'school_year' => $this->request->getPost('school_year'),
|
|
'created_by' => session()->get('user_id'),
|
|
]);
|
|
|
|
if ($eventId && $this->request->getPost('add_to_calendar')) {
|
|
$calendarModel = new CalendarModel();
|
|
|
|
$title = trim((string)$this->request->getPost('event_name'));
|
|
$date = (string)$this->request->getPost('expiration_date');
|
|
$schoolYear = (string)$this->request->getPost('school_year');
|
|
$semester = (string)$this->request->getPost('semester');
|
|
|
|
if ($title !== '' && $date !== '' && $schoolYear !== '') {
|
|
$data = [
|
|
'title' => $title,
|
|
'description' => (string)$this->request->getPost('description'),
|
|
'event_type' => 'Event',
|
|
'date' => $date,
|
|
'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0,
|
|
'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0,
|
|
'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0,
|
|
'no_school' => 0,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester ?: $this->semester,
|
|
];
|
|
if (!$calendarModel->supportsEventType()) {
|
|
unset($data['event_type']);
|
|
}
|
|
|
|
$dupQuery = $calendarModel
|
|
->where('school_year', $data['school_year'])
|
|
->where('date', $data['date'])
|
|
->where('title', $data['title']);
|
|
if ($calendarModel->supportsEventType() && isset($data['event_type'])) {
|
|
$dupQuery = $dupQuery->where('event_type', $data['event_type']);
|
|
}
|
|
$existing = $dupQuery->first();
|
|
if (!$existing) {
|
|
$calendarModel->save($data);
|
|
}
|
|
}
|
|
}
|
|
|
|
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
|
}
|
|
|
|
return view('administrator/events/create_event', [
|
|
'categories' => $this->categories,
|
|
]);
|
|
}
|
|
|
|
|
|
public function edit($id = null)
|
|
{
|
|
helper(['form']);
|
|
$event = $this->eventModel->find($id);
|
|
|
|
if (!$event) {
|
|
return redirect()->to('/administrator/events')->with('error', 'Event not found');
|
|
}
|
|
|
|
if (strtolower($this->request->getMethod()) === 'post') {
|
|
log_message('debug', 'POST detected');
|
|
$file = $this->request->getFile('flyer');
|
|
$flyerPath = $event['flyer']; // Default: keep old flyer
|
|
|
|
if ($file && $file->isValid() && !$file->hasMoved()) {
|
|
$newName = $file->getRandomName();
|
|
$file->move(FCPATH . 'uploads/event_flyers', $newName);
|
|
$flyerPath = 'event_flyers/' . $newName; // store relative path
|
|
}
|
|
|
|
$updated = $this->eventModel->update($id, [
|
|
'event_name' => $this->request->getPost('event_name'),
|
|
'event_category' => $this->request->getPost('event_category'),
|
|
'description' => $this->request->getPost('description'),
|
|
'amount' => $this->request->getPost('amount'),
|
|
'flyer' => $flyerPath,
|
|
'expiration_date' => $this->request->getPost('expiration_date'),
|
|
'semester' => $this->request->getPost('semester'),
|
|
'school_year' => $this->request->getPost('school_year'),
|
|
]);
|
|
|
|
if ($updated) {
|
|
return redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
|
|
} else {
|
|
log_message('debug', 'GET detected');
|
|
return redirect()->back()->with('error', 'Failed to update event');
|
|
}
|
|
}
|
|
|
|
return view('administrator/events/edit_event', [
|
|
'event' => $event,
|
|
'categories' => $this->categories,
|
|
]);
|
|
}
|
|
|
|
|
|
public function delete($id = null)
|
|
{
|
|
|
|
$event = $this->eventModel->find($id);
|
|
|
|
if (!$event) {
|
|
return redirect()->to('/administrator/events')->with('error', 'Event not found');
|
|
}
|
|
|
|
// Delete related charges and collect parent IDs
|
|
$charges = $this->eventChargesModel->where('event_id', $id)->findAll();
|
|
$parentIds = [];
|
|
|
|
foreach ($charges as $charge) {
|
|
$this->eventChargesModel->delete($charge['id']);
|
|
$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);
|
|
|
|
$parentIds = array_unique($parentIds);
|
|
|
|
foreach ($parentIds as $parentId) {
|
|
$this->invoiceController->generateInvoice($parentId);
|
|
}
|
|
|
|
return redirect()->to('/administrator/events')->with('success', 'Event, charges, and invoices updated.');
|
|
}
|
|
|
|
|
|
// Optionally keep your eventShow / eventUpdate for legacy administrator event charges
|
|
public function eventShow()
|
|
{
|
|
|
|
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
|
$semester = $this->request->getGet('semester') ?? $this->semester;
|
|
|
|
$parents = $this->userModel->getParents();
|
|
$events = $this->eventModel->getActiveEvents($schoolYear);
|
|
$filterEventId = (int) ($this->request->getGet('event_id') ?? 0);
|
|
$filterParentId = (int) ($this->request->getGet('parent_id') ?? 0);
|
|
|
|
$chargesBuilder = $this->eventChargesModel
|
|
->select('event_charges.*,
|
|
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
|
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
|
events.event_name, events.description AS event_description, events.amount AS event_amount')
|
|
->join('users', 'users.id = event_charges.parent_id', 'left')
|
|
->join('students', 'students.id = event_charges.student_id', 'left')
|
|
->join('events', 'events.id = event_charges.event_id', 'left')
|
|
->where('event_charges.school_year', $schoolYear)
|
|
->where('event_charges.semester', $semester);
|
|
|
|
if ($filterEventId > 0) {
|
|
$chargesBuilder->where('event_charges.event_id', $filterEventId);
|
|
}
|
|
|
|
$charges = $chargesBuilder
|
|
->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,
|
|
'events' => $events,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'filterEventId' => $filterEventId,
|
|
'filterParentId' => $filterParentId,
|
|
'parentBalances' => $parentBalances,
|
|
'classSectionNames' => $classSectionNames,
|
|
'semesterOptions' => $semesterOptions,
|
|
'schoolYearOptions' => $schoolYearOptions,
|
|
]);
|
|
}
|
|
|
|
|
|
public function eventUpdate()
|
|
{
|
|
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
|
$semester = $this->request->getPost('semester') ?? $this->semester;
|
|
|
|
$parentId = $this->request->getPost('parent_id');
|
|
$eventId = $this->request->getPost('event_id');
|
|
$participations = $this->request->getPost('participation') ?? [];
|
|
$externalParticipants = $this->request->getPost('external_participants') ?? [];
|
|
|
|
// Allow "external only" submissions (no enrolled students selected).
|
|
if (!$eventId || (empty($participations) && empty($externalParticipants))) {
|
|
return redirect()->back()->with('error', 'Missing required information.');
|
|
}
|
|
if (!empty($participations) && !$parentId) {
|
|
return redirect()->back()->with('error', 'Select a parent when adding enrolled students.');
|
|
}
|
|
|
|
$userId = session()->get('user_id');
|
|
$event = $this->eventModel->getEvent($eventId, $schoolYear);
|
|
if (!$event) {
|
|
return redirect()->back()->with('error', 'Selected event not found for the chosen school year.');
|
|
}
|
|
|
|
$parentsForInvoice = [];
|
|
$supportsCreatedBy = $this->eventChargesSupportsCreatedBy();
|
|
|
|
foreach ($participations as $studentId => $value) {
|
|
$existing = $this->eventChargesModel->where([
|
|
'parent_id' => $parentId,
|
|
'student_id' => $studentId,
|
|
'event_id' => $eventId,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester
|
|
])->first();
|
|
|
|
if ($value === 'no') {
|
|
if ($existing) {
|
|
$this->eventChargesModel->delete($existing['id']);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$sectionIds = $this->studentClassModel->getClassSectionIdsByStudentId($studentId, $schoolYear);
|
|
$classSectionId = !empty($sectionIds) ? $sectionIds[0] : null;
|
|
|
|
if ($existing) {
|
|
$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 {
|
|
$insertData = [
|
|
'parent_id' => $parentId,
|
|
'student_id' => $studentId,
|
|
'event_id' => $eventId,
|
|
'participation' => 'yes',
|
|
'charged' => $event['amount'],
|
|
'event_paid' => 0,
|
|
'class_section_id' => $classSectionId,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'updated_by' => $userId
|
|
];
|
|
if ($supportsCreatedBy) {
|
|
$insertData['created_by'] = $userId;
|
|
}
|
|
$this->eventChargesModel->insert($insertData);
|
|
}
|
|
}
|
|
|
|
foreach ($externalParticipants as $pieces) {
|
|
$firstname = $this->normalizeExternalName($pieces['firstname'] ?? '');
|
|
$lastname = $this->normalizeExternalName($pieces['lastname'] ?? '');
|
|
$note = trim($pieces['note'] ?? '');
|
|
$parentFirst = $this->normalizeExternalName($pieces['parent_firstname'] ?? '');
|
|
$parentLast = $this->normalizeExternalName($pieces['parent_lastname'] ?? '');
|
|
$parentPhone = trim($pieces['parent_phone'] ?? '');
|
|
$parentEmail = trim((string)($pieces['parent_email'] ?? ''));
|
|
$markPaid = (string)($pieces['paid'] ?? '0') === '1';
|
|
|
|
if (!$firstname && !$lastname) {
|
|
continue;
|
|
}
|
|
|
|
$matchedParentId = $this->findExistingParentIdByExternalContact(
|
|
$parentFirst,
|
|
$parentLast,
|
|
$parentEmail,
|
|
$parentPhone
|
|
);
|
|
|
|
// Match external participants by kid name + event context.
|
|
// Parent contact info can change (or be fixed later) and should be updated, not used for matching.
|
|
$matchConditions = [
|
|
'event_id' => $eventId,
|
|
'external_firstname' => $firstname,
|
|
'external_lastname' => $lastname,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
];
|
|
if ($matchedParentId) {
|
|
$matchConditions['parent_id'] = $matchedParentId;
|
|
} else {
|
|
// External-only parent: match by external parent contact to avoid duplicates.
|
|
$matchConditions['parent_id'] = null;
|
|
$matchConditions['external_parent_firstname'] = $parentFirst ?: null;
|
|
$matchConditions['external_parent_lastname'] = $parentLast ?: null;
|
|
$matchConditions['external_parent_phone'] = $parentPhone ?: null;
|
|
$matchConditions['external_parent_email'] = $parentEmail ?: null;
|
|
}
|
|
$existingExternal = $this->eventChargesModel->where($matchConditions)->first();
|
|
|
|
if ($existingExternal) {
|
|
$this->eventChargesModel->update($existingExternal['id'], [
|
|
'participation' => 'yes',
|
|
'charged' => $event['amount'],
|
|
'updated_by' => $userId,
|
|
// Preserve existing payment status; only update contact fields when provided.
|
|
'external_note' => $note !== '' ? $note : ($existingExternal['external_note'] ?? null),
|
|
'external_parent_firstname' => $parentFirst !== '' ? $parentFirst : ($existingExternal['external_parent_firstname'] ?? null),
|
|
'external_parent_lastname' => $parentLast !== '' ? $parentLast : ($existingExternal['external_parent_lastname'] ?? null),
|
|
'external_parent_phone' => $parentPhone !== '' ? $parentPhone : ($existingExternal['external_parent_phone'] ?? null),
|
|
'external_parent_email' => $parentEmail !== '' ? $parentEmail : ($existingExternal['external_parent_email'] ?? null),
|
|
]);
|
|
continue;
|
|
}
|
|
|
|
$insertData = [
|
|
'parent_id' => $matchedParentId ?: null,
|
|
'student_id' => null,
|
|
'event_id' => $eventId,
|
|
'participation' => 'yes',
|
|
'charged' => $event['amount'],
|
|
'event_paid' => $markPaid ? 1 : 0,
|
|
'external_firstname' => $firstname,
|
|
'external_lastname' => $lastname,
|
|
'external_note' => $note,
|
|
'external_parent_firstname' => $parentFirst,
|
|
'external_parent_lastname' => $parentLast,
|
|
'external_parent_phone' => $parentPhone,
|
|
'external_parent_email' => $parentEmail,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'updated_by' => $userId,
|
|
];
|
|
if ($supportsCreatedBy) {
|
|
$insertData['created_by'] = $userId;
|
|
}
|
|
$this->eventChargesModel->insert($insertData);
|
|
|
|
$newChargeId = (int)$this->eventChargesModel->getInsertID();
|
|
if ($markPaid && $newChargeId > 0) {
|
|
$this->applyEventPaymentStatus($newChargeId, true);
|
|
}
|
|
|
|
if ($matchedParentId && $this->parentHasEnrollment($matchedParentId, (string)$schoolYear)) {
|
|
$parentsForInvoice[$matchedParentId] = true;
|
|
}
|
|
}
|
|
|
|
if ($parentId && $this->parentHasEnrollment((int)$parentId, (string)$schoolYear)) {
|
|
$parentsForInvoice[(int)$parentId] = true;
|
|
}
|
|
|
|
foreach (array_keys($parentsForInvoice) as $pid) {
|
|
$this->invoiceController->generateInvoice((string)$pid, (string)$schoolYear, (string)$semester, false);
|
|
$this->fixInvoiceStatusAfterCharge((int)$pid, (string)$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);
|
|
if (!empty($charge['parent_id']) && $this->parentHasEnrollment((int)$charge['parent_id'], (string)($charge['school_year'] ?? ''))) {
|
|
$this->invoiceController->generateInvoice((string)$charge['parent_id'], (string)($charge['school_year'] ?? $this->schoolYear), (string)($charge['semester'] ?? $this->semester), false);
|
|
}
|
|
|
|
return redirect()->back()->with('success', 'Participation removed, invoices updated.');
|
|
}
|
|
|
|
public function toggleEventPayment($chargeId = null)
|
|
{
|
|
if (!$chargeId) {
|
|
return redirect()->back()->with('error', 'Invalid charge.');
|
|
}
|
|
|
|
$isPaid = $this->request->getPost('paid') === '1';
|
|
$meta = $this->applyEventPaymentStatus((int)$chargeId, $isPaid);
|
|
if (!$meta) {
|
|
return redirect()->back()->with('error', 'Charge not found.');
|
|
}
|
|
|
|
if (!empty($meta['invoice_id'])) {
|
|
$this->invoiceController->generateInvoice((string)$meta['parent_id'], (string)$meta['school_year'], (string)($meta['semester'] ?? $this->semester), false);
|
|
$this->fixInvoiceStatusAfterCharge((int)$meta['parent_id'], (string)$meta['school_year']);
|
|
}
|
|
|
|
return redirect()->back()->with('success', 'Event payment status updated.');
|
|
}
|
|
|
|
private function parentHasEnrollment(int $parentId, string $schoolYear): bool
|
|
{
|
|
if ($parentId <= 0 || $schoolYear === '') {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return $this->enrollmentModel
|
|
->where('parent_id', $parentId)
|
|
->where('school_year', $schoolYear)
|
|
->countAllResults() > 0;
|
|
} catch (\Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function findExistingParentIdByExternalContact(string $first, string $last, string $email, string $phone): ?int
|
|
{
|
|
$first = trim($first);
|
|
$last = trim($last);
|
|
$email = trim($email);
|
|
$phoneDigits = preg_replace('/\D+/', '', (string)$phone);
|
|
|
|
if ($first === '' || $last === '' || $email === '' || $phoneDigits === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
$expr = "REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') =";
|
|
|
|
$row = $this->userModel
|
|
->select('id')
|
|
->where('LOWER(firstname)', strtolower($first))
|
|
->where('LOWER(lastname)', strtolower($last))
|
|
->where('LOWER(email)', strtolower($email))
|
|
->where($expr, $db->escapeString($phoneDigits), false)
|
|
->first();
|
|
|
|
$id = (int)($row['id'] ?? 0);
|
|
return $id > 0 ? $id : null;
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Failed to match external parent to users: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function syncInvoicePaymentSummary(int $invoiceId): void
|
|
{
|
|
if ($invoiceId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$invoice = $this->invoiceModel->find($invoiceId);
|
|
if (!$invoice) {
|
|
return;
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
|
|
|
$paidSum = 0.0;
|
|
try {
|
|
$qb = $db->table('payments')
|
|
->select('COALESCE(SUM(paid_amount),0) AS tot')
|
|
->where('invoice_id', $invoiceId)
|
|
->where('paid_amount >', 0);
|
|
|
|
if ($db->fieldExists('status', 'payments')) {
|
|
$qb->groupStart()
|
|
->whereNotIn('status', $exclude)
|
|
->orWhere('status IS NULL', null, false)
|
|
->groupEnd();
|
|
}
|
|
if ($db->fieldExists('is_void', 'payments')) {
|
|
$qb->groupStart()
|
|
->where('is_void', 0)
|
|
->orWhere('is_void IS NULL', null, false)
|
|
->groupEnd();
|
|
}
|
|
|
|
$row = $qb->get()->getRowArray();
|
|
$paidSum = (float)($row['tot'] ?? 0.0);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Failed to sum payments for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
|
}
|
|
|
|
$discountSum = 0.0;
|
|
try {
|
|
$row = $db->table('discount_usages')
|
|
->select('COALESCE(SUM(discount_amount),0) AS tot')
|
|
->where('invoice_id', $invoiceId)
|
|
->get()
|
|
->getRowArray();
|
|
$discountSum = (float)($row['tot'] ?? 0.0);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Failed to sum discounts for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
|
}
|
|
|
|
$refundSum = 0.0;
|
|
try {
|
|
$row = $db->table('refunds')
|
|
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
|
->where('invoice_id', $invoiceId)
|
|
->whereIn('status', ['Partial', 'Paid'])
|
|
->get()
|
|
->getRowArray();
|
|
$refundSum = (float)($row['tot'] ?? 0.0);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Failed to sum refunds for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
|
}
|
|
|
|
$total = (float)($invoice['total_amount'] ?? 0.0);
|
|
$newBalance = round($total - $discountSum - $refundSum - $paidSum, 2);
|
|
$newStatus = ($newBalance <= 0.00001)
|
|
? 'Paid'
|
|
: (($paidSum > 0) ? 'Partially Paid' : 'Unpaid');
|
|
|
|
$this->invoiceModel->update($invoiceId, [
|
|
'paid_amount' => $paidSum,
|
|
'balance' => $newBalance,
|
|
'status' => $newStatus,
|
|
'updated_at' => utc_now(),
|
|
]);
|
|
}
|
|
|
|
private function applyEventPaymentStatus(int $chargeId, bool $isPaid): ?array
|
|
{
|
|
if ($chargeId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$charge = $this->eventChargesModel->find($chargeId);
|
|
if (!$charge) {
|
|
return null;
|
|
}
|
|
|
|
$event = $this->eventModel->find($charge['event_id']);
|
|
$eventAmount = max(0.0, (float)($event['amount'] ?? 0));
|
|
$paymentId = (int)($charge['event_payment_id'] ?? 0);
|
|
|
|
$parentId = (int)($charge['parent_id'] ?? 0);
|
|
$schoolYear = (string)($charge['school_year'] ?? $this->schoolYear);
|
|
$semester = (string)($charge['semester'] ?? $this->semester);
|
|
|
|
if ($parentId > 0 && $eventAmount > 0) {
|
|
$hasEnrollment = $this->parentHasEnrollment($parentId, $schoolYear);
|
|
$invoiceQuery = $this->invoiceModel
|
|
->where('parent_id', $parentId)
|
|
->where('school_year', $schoolYear)
|
|
->orderBy('id', 'DESC');
|
|
$invoice = $invoiceQuery->first();
|
|
|
|
if (!$invoice && $hasEnrollment) {
|
|
$this->invoiceController->generateInvoice((string)$parentId, $schoolYear, $semester, false);
|
|
$invoice = $invoiceQuery->first();
|
|
}
|
|
|
|
if ($invoice) {
|
|
$invoiceId = (int)($invoice['id'] ?? 0);
|
|
if ($invoiceId > 0) {
|
|
$this->syncInvoicePaymentSummary($invoiceId);
|
|
$invoice = $this->invoiceModel->find($invoiceId) ?? $invoice;
|
|
}
|
|
|
|
if ($isPaid && !$paymentId) {
|
|
$paymentSchoolYear = (string)($invoice['school_year'] ?? $schoolYear);
|
|
$paymentSemester = (string)($invoice['semester'] ?? $semester);
|
|
$paymentId = (int)($this->createEventPayment(
|
|
$parentId,
|
|
(int)$invoice['id'],
|
|
$eventAmount,
|
|
$paymentSchoolYear,
|
|
$paymentSemester,
|
|
(float)($invoice['total_amount'] ?? 0.0),
|
|
(float)($invoice['paid_amount'] ?? 0.0),
|
|
(float)($invoice['balance'] ?? 0.0)
|
|
) ?? 0);
|
|
} elseif (!$isPaid && $paymentId > 0) {
|
|
$this->paymentModel->delete($paymentId);
|
|
$paymentId = 0;
|
|
}
|
|
|
|
if (!empty($invoiceId)) {
|
|
$this->syncInvoicePaymentSummary($invoiceId);
|
|
}
|
|
} elseif (!$hasEnrollment) {
|
|
// External-only parent: do not create an invoice/payment row; just persist the paid flag on the charge.
|
|
$paymentId = 0;
|
|
}
|
|
}
|
|
|
|
$this->eventChargesModel->update($chargeId, [
|
|
'event_paid' => $isPaid ? 1 : 0,
|
|
'charged' => $eventAmount,
|
|
'event_payment_id' => $paymentId > 0 ? $paymentId : null,
|
|
]);
|
|
|
|
return [
|
|
'parent_id' => $parentId,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'invoice_id' => isset($invoiceId) && $invoiceId > 0 ? $invoiceId : null,
|
|
];
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getStudentsWithCharges()
|
|
{
|
|
$parentId = $this->request->getGet('parent_id');
|
|
$semester = $this->request->getGet('semester');
|
|
$schoolYear = $this->request->getGet('school_year');
|
|
|
|
// Get students for parent
|
|
$students = $this->studentModel->where('parent_id', $parentId)->findAll();
|
|
|
|
// Get student_ids that already have charges
|
|
$eventId = $this->request->getGet('event_id');
|
|
|
|
$chargesBuilder = $this->eventChargesModel
|
|
->where('parent_id', $parentId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear);
|
|
|
|
if (!empty($eventId)) {
|
|
$chargesBuilder->where('event_id', $eventId);
|
|
}
|
|
|
|
$chargedStudentIds = $chargesBuilder
|
|
->groupBy('student_id')
|
|
->select('student_id')
|
|
->findColumn('student_id');
|
|
|
|
$data = [];
|
|
foreach ($students as $student) {
|
|
$data[] = [
|
|
'id' => $student['id'],
|
|
'name' => $student['firstname'] . ' ' . $student['lastname'],
|
|
'charged' => in_array($student['id'], $chargedStudentIds ?? []),
|
|
];
|
|
}
|
|
|
|
return $this->response->setJSON($data);
|
|
}
|
|
|
|
private function createEventPayment(
|
|
int $parentId,
|
|
int $invoiceId,
|
|
float $amount,
|
|
string $schoolYear,
|
|
?string $semester,
|
|
float $invoiceTotal = 0.0,
|
|
float $invoicePaid = 0.0,
|
|
float $invoiceBalance = 0.0
|
|
): ?int
|
|
{
|
|
if ($amount <= 0 || $invoiceId <= 0 || $parentId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$monthSemester = $semester ?: $this->semester;
|
|
$newPaid = (float)$invoicePaid + $amount;
|
|
$newBalance = (float)$invoiceBalance - $amount;
|
|
$paymentStatus = ($newBalance <= 0.00001)
|
|
? 'Paid'
|
|
: (($newPaid > 0) ? 'Partially Paid' : 'Unpaid');
|
|
|
|
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
|
$priorCount = $this->paymentModel
|
|
->where('invoice_id', $invoiceId)
|
|
->where('paid_amount >', 0)
|
|
->whereNotIn('status', $exclude)
|
|
->countAllResults();
|
|
$installmentSeq = $priorCount + 1;
|
|
|
|
$data = [
|
|
'parent_id' => $parentId,
|
|
'invoice_id' => $invoiceId,
|
|
'total_amount' => $invoiceTotal > 0 ? $invoiceTotal : $amount,
|
|
'paid_amount' => $amount,
|
|
'balance' => $newBalance,
|
|
'number_of_installments' => $installmentSeq,
|
|
'payment_method' => 'cash',
|
|
'payment_date' => utc_now(),
|
|
'school_year' => $schoolYear,
|
|
'semester' => $monthSemester,
|
|
'status' => $paymentStatus,
|
|
'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 normalizeExternalName(?string $value): string
|
|
{
|
|
$raw = trim((string)$value);
|
|
if ($raw === '') {
|
|
return '';
|
|
}
|
|
$parts = preg_split('/\s+/', $raw);
|
|
$formatted = array_map(function ($part) {
|
|
$part = trim($part);
|
|
if ($part === '') {
|
|
return '';
|
|
}
|
|
return mb_strtoupper(mb_substr($part, 0, 1)) . mb_strtolower(mb_substr($part, 1));
|
|
}, $parts);
|
|
return implode(' ', array_filter($formatted, fn($p) => $p !== ''));
|
|
}
|
|
|
|
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']);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|