589 lines
24 KiB
PHP
Executable File
589 lines
24 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
|
||
use App\Controllers\BaseController;
|
||
use App\Models\AdditionalChargeModel;
|
||
use CodeIgniter\Controller;
|
||
use App\Models\UserModel;
|
||
use App\Models\ConfigurationModel;
|
||
use CodeIgniter\Events\Events;
|
||
use App\Models\InvoiceModel;
|
||
use CodeIgniter\I18n\Time;
|
||
|
||
class ExtraChargesController extends BaseController
|
||
{
|
||
protected $additionalChargeModel;
|
||
protected $db;
|
||
protected $configModel;
|
||
protected $semester;
|
||
protected $schoolYear;
|
||
protected $attendanceDataModel;
|
||
protected $studentModel;
|
||
protected $emergencyContactModel;
|
||
protected $userModel;
|
||
protected $teacherClassSection;
|
||
protected $invoiceModel;
|
||
protected $classSectionModel;
|
||
protected $studentClassModel;
|
||
protected $enableAttendance;
|
||
protected $attendanceDayModel;
|
||
|
||
public function __construct()
|
||
{
|
||
// Initialize the database connection in the constructor
|
||
$this->db = \Config\Database::connect();
|
||
$this->configModel = new ConfigurationModel();
|
||
$this->additionalChargeModel = new AdditionalChargeModel();
|
||
$this->userModel = new UserModel();
|
||
$this->invoiceModel = new InvoiceModel();
|
||
$this->semester = $this->configModel->getConfig('semester');
|
||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
// API-first: delegate to JSON list endpoint
|
||
return $this->apiList();
|
||
}
|
||
|
||
/** Render HTML management page */
|
||
public function page()
|
||
{
|
||
$parentId = (int)($this->request->getGet('parent_id') ?? 0);
|
||
$status = $this->request->getGet('status') ?: null;
|
||
$yearSelect = (string)($this->request->getGet('school_year') ?? $this->schoolYear);
|
||
|
||
$rows = [];
|
||
if ($parentId > 0) {
|
||
$rows = $this->additionalChargeModel
|
||
->byParentTerm($parentId, $this->schoolYear, $this->semester, $status);
|
||
}
|
||
|
||
// Load ALL parents for current view
|
||
$parents = $this->userModel->select('users.id, users.firstname, users.lastname')
|
||
->join('user_roles', 'user_roles.user_id = users.id')
|
||
->join('roles', 'roles.id = user_roles.role_id')
|
||
->where('LOWER(roles.name) =', 'parent')
|
||
->where('user_roles.deleted_at IS NULL', null, false)
|
||
->orderBy('users.lastname', 'ASC')
|
||
->orderBy('users.firstname', 'ASC')
|
||
->findAll();
|
||
|
||
// Map parent id -> "Lastname, Firstname" (no email/ID shown)
|
||
$parentMap = [];
|
||
foreach ($parents as $p) {
|
||
$label = trim(($p['lastname'] ?? '') . ', ' . ($p['firstname'] ?? ''));
|
||
$parentMap[(int)$p['id']] = $label !== '' && $label !== ',' ? $label : ('User #' . (int)$p['id']);
|
||
}
|
||
|
||
// 🔹 Get ALL invoices for ALL parents this year, group by parent_id
|
||
$parentIds = array_map(fn($r) => (int)$r['id'], $parents);
|
||
$invoicesByParent = [];
|
||
if (!empty($parentIds)) {
|
||
$all = $this->invoiceModel->getAllInvoicesByUserIds($parentIds, $yearSelect);
|
||
foreach ($all as $inv) {
|
||
$pid = (int)$inv['parent_id'];
|
||
$invoicesByParent[$pid][] = [
|
||
'id' => (int)$inv['id'],
|
||
'invoice_number' => (string)$inv['invoice_number'],
|
||
'status' => (string)($inv['status'] ?? ''),
|
||
'balance' => (float)($inv['balance'] ?? 0),
|
||
'issue_date' => $inv['issue_date'] ?? null,
|
||
];
|
||
}
|
||
}
|
||
|
||
// Label for preselect (if filtering by a parent)
|
||
$selectedParentLabel = ($parentId > 0 && isset($parentMap[$parentId])) ? $parentMap[$parentId] : '';
|
||
|
||
// Optional filters
|
||
$status = $this->request->getGet('status') ?: null;
|
||
$q = trim((string)($this->request->getGet('q') ?? ''));
|
||
|
||
// ✅ Always pull all charges for the selected year & current semester (all parents)
|
||
$rows = $this->additionalChargeModel->listAllForTerm(
|
||
$yearSelect,
|
||
$this->semester,
|
||
$status,
|
||
$q,
|
||
50 // per-page
|
||
);
|
||
$pager = $this->additionalChargeModel->pager;
|
||
|
||
// Build school year options from data (additional_charges + invoices)
|
||
$schoolYears = [];
|
||
try {
|
||
$q1 = $this->db->table('additional_charges')->select('DISTINCT school_year', false)
|
||
->where('school_year IS NOT NULL', null, false)
|
||
->orderBy('school_year', 'DESC')->get()->getResultArray();
|
||
foreach ($q1 as $r) {
|
||
$val = (string)($r['school_year'] ?? '');
|
||
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
|
||
}
|
||
} catch (\Throwable $e) {}
|
||
try {
|
||
$q2 = $this->db->table('invoices')->select('DISTINCT school_year', false)
|
||
->where('school_year IS NOT NULL', null, false)
|
||
->orderBy('school_year', 'DESC')->get()->getResultArray();
|
||
foreach ($q2 as $r) {
|
||
$val = (string)($r['school_year'] ?? '');
|
||
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
|
||
}
|
||
} catch (\Throwable $e) {}
|
||
if (empty($schoolYears) && is_string($this->schoolYear) && $this->schoolYear !== '') {
|
||
// fallback: generate recent years around configured schoolYear
|
||
$schoolYears[] = $this->schoolYear;
|
||
// Optionally add previous/next
|
||
[$start, $end] = explode('-', $this->schoolYear) + [0 => date('Y'), 1 => date('Y')+1];
|
||
$start = (int)$start;
|
||
for ($i = 1; $i <= 3; $i++) {
|
||
$schoolYears[] = ($start - $i) . '-' . (($start - $i) + 1);
|
||
}
|
||
}
|
||
rsort($schoolYears);
|
||
|
||
return view('payment/extra_charges', [
|
||
'q' => $q,
|
||
'pager' => $pager,
|
||
'rows' => $rows,
|
||
'parents' => $parents,
|
||
'parentMap' => $parentMap,
|
||
'invoicesByParent' => $invoicesByParent, // 🔹 pass the grouped map
|
||
'parentId' => $parentId,
|
||
'selectedParentLabel' => $selectedParentLabel,
|
||
'status' => $status,
|
||
'schoolYear' => $yearSelect,
|
||
'schoolYears' => $schoolYears,
|
||
'semester' => $this->semester,
|
||
]);
|
||
}
|
||
|
||
private function wantsJson(): bool
|
||
{
|
||
$accept = strtolower((string)($this->request->getHeaderLine('Accept') ?? ''));
|
||
return $this->request->isAJAX() || str_contains($accept, 'application/json');
|
||
}
|
||
|
||
/** Used by Select2 AJAX: returns {results:[{id,text}]} */
|
||
public function parentOptions()
|
||
{
|
||
$q = trim((string)($this->request->getGet('q') ?? ''));
|
||
$limit = 20;
|
||
|
||
$builder = $this->userModel
|
||
->select('users.id, users.firstname, users.lastname, users.email')
|
||
->join('user_roles', 'user_roles.user_id = users.id')
|
||
->join('roles', 'roles.id = user_roles.role_id')
|
||
// If your role name may have different case, use LOWER(...):
|
||
// ->where('LOWER(roles.name) =', 'parent')
|
||
->where('roles.name', 'parent')
|
||
// Make sure this produces IS NULL; if not, use the raw condition line below
|
||
->where('user_roles.deleted_at', null);
|
||
|
||
// Alternative guaranteed-IS-NULL:
|
||
// $builder->where('user_roles.deleted_at IS NULL', null, false);
|
||
|
||
if ($q !== '') {
|
||
$builder->groupStart()
|
||
->like('users.firstname', $q)
|
||
->orLike('users.lastname', $q)
|
||
->orLike('users.email', $q)
|
||
->groupEnd();
|
||
}
|
||
|
||
$rows = $builder
|
||
->orderBy('users.lastname', 'ASC')
|
||
->orderBy('users.firstname', 'ASC')
|
||
->limit($limit)
|
||
->find();
|
||
|
||
$results = array_map(function ($r) {
|
||
$name = trim(($r['lastname'] ?? '') . ', ' . ($r['firstname'] ?? ''));
|
||
$email = $r['email'] ?? '';
|
||
$label = $name !== ',' ? $name : ('User #' . $r['id']);
|
||
if ($email) $label .= ' — ' . $email;
|
||
$label .= ' (ID: ' . $r['id'] . ')';
|
||
return ['id' => (int)$r['id'], 'text' => $label];
|
||
}, $rows);
|
||
|
||
return $this->response->setJSON(['results' => $results]);
|
||
}
|
||
|
||
/** Helper for preselect label in the modal */
|
||
private function getParentLabel(int $id): string
|
||
{
|
||
$p = $this->userModel->select('firstname, lastname, email')->find($id);
|
||
if (!$p) return 'User #' . $id;
|
||
$name = trim(($p['lastname'] ?? '') . ', ' . ($p['firstname'] ?? ''));
|
||
$email = $p['email'] ?? '';
|
||
$label = $name !== ',' ? $name : ('User #' . $id);
|
||
if ($email) $label .= ' — ' . $email;
|
||
return $label . ' (ID: ' . $id . ')';
|
||
}
|
||
|
||
public function update($id)
|
||
{
|
||
$row = $this->additionalChargeModel->find($id);
|
||
if (!$row) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Charge not found', 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash()]);
|
||
return redirect()->back()->with('error', 'Charge not found.');
|
||
}
|
||
|
||
$data = $this->request->getPost();
|
||
$newAmount = isset($data['amount']) ? (float)$data['amount'] : (float)$row['amount'];
|
||
$delta = $newAmount - (float)$row['amount'];
|
||
|
||
$db = \Config\Database::connect();
|
||
$db->transStart();
|
||
|
||
// Update the charge first
|
||
$this->additionalChargeModel->update($id, [
|
||
'title' => trim($data['title'] ?? $row['title']),
|
||
'description' => trim($data['description'] ?? $row['description']),
|
||
'amount' => $newAmount,
|
||
'due_date' => $data['due_date'] ?? $row['due_date'],
|
||
'charge_type' => $data['charge_type'] ?? $row['charge_type'],
|
||
// keep status as-is
|
||
]);
|
||
|
||
// If it’s already applied on an invoice and the amount changed, reflect the delta
|
||
if ($row['status'] === 'applied' && !empty($row['invoice_id']) && abs($delta) > 0.00001) {
|
||
if ($delta > 0) {
|
||
$this->invoiceModel->applyAdditionalCharge((int)$row['invoice_id'], $delta);
|
||
} else {
|
||
$this->invoiceModel->reverseAdditionalCharge((int)$row['invoice_id'], -$delta);
|
||
}
|
||
}
|
||
|
||
$db->transComplete();
|
||
|
||
if (!$db->transStatus()) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Failed to update charge', 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash()]);
|
||
return redirect()->back()->with('error', 'Failed to update charge.');
|
||
}
|
||
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => true, 'id' => (int)$id, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash()]);
|
||
return redirect()->back()->with('status', 'Charge updated.');
|
||
}
|
||
|
||
// (removed duplicate model-like listAllForTerm method; use AdditionalChargeModel::listAllForTerm)
|
||
|
||
// ExtraChargesController.php
|
||
public function invoicesForParent()
|
||
{
|
||
$parentId = (int)($this->request->getGet('parent_id') ?? 0);
|
||
$schoolYear = (string)($this->request->getGet('school_year') ?? $this->schoolYear);
|
||
|
||
$rows = ($parentId > 0)
|
||
? ($this->invoiceModel->getInvoicesByUserId($parentId, $schoolYear) ?? [])
|
||
: [];
|
||
|
||
$results = array_map(function ($inv) {
|
||
$id = (int)($inv['id'] ?? 0);
|
||
$num = (string)($inv['invoice_number'] ?? ('INV-' . $id));
|
||
$status = strtolower((string)($inv['status'] ?? ''));
|
||
$balance = (float)($inv['balance'] ?? 0);
|
||
$issue = $inv['issue_date'] ?? null;
|
||
|
||
return [
|
||
'id' => $id,
|
||
'invoice_number' => $num,
|
||
'status' => $status,
|
||
'balance' => $balance,
|
||
'issue_date' => $issue,
|
||
'text' => $num
|
||
. ($status ? ' — ' . ucfirst($status) : '')
|
||
. ' (Bal: $' . number_format($balance, 2) . ')',
|
||
];
|
||
}, $rows);
|
||
|
||
return $this->response->setJSON(['results' => $results]);
|
||
}
|
||
|
||
|
||
public function store()
|
||
{
|
||
$data = $this->request->getPost();
|
||
|
||
$rules = [
|
||
'parent_id' => 'required|integer', // this is actually the user_id of the parent
|
||
'title' => 'required|string|min_length[2]',
|
||
'amount' => 'required|decimal',
|
||
'charge_type' => 'required|in_list[add,deduct]',
|
||
'due_date' => 'permit_empty|valid_date',
|
||
'invoice_id' => 'permit_empty|integer',
|
||
];
|
||
if (!$this->validate($rules)) {
|
||
$msg = implode(' ', $this->validator->getErrors());
|
||
if ($this->wantsJson()) {
|
||
return $this->response->setJSON([
|
||
'ok' => false,
|
||
'error' => $msg,
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
return redirect()->back()->withInput()->with('error', $msg);
|
||
}
|
||
|
||
$invoiceId = !empty($data['invoice_id']) ? (int)$data['invoice_id'] : null;
|
||
$chargeType = (string)$data['charge_type'];
|
||
|
||
$amountAbs = round(abs((float)$data['amount']), 2);
|
||
$signedAmount = ($chargeType === 'add') ? $amountAbs : -$amountAbs;
|
||
|
||
$payload = [
|
||
'parent_id' => (int)$data['parent_id'], // ← users.id of the parent
|
||
'invoice_id' => $invoiceId,
|
||
'school_year' => $data['school_year'] ?? $this->schoolYear,
|
||
'semester' => $data['semester'] ?? $this->semester,
|
||
'charge_type' => $chargeType,
|
||
'title' => trim($data['title']),
|
||
'description' => trim($data['description'] ?? ''),
|
||
'amount' => $signedAmount,
|
||
'due_date' => !empty($data['due_date']) ? $data['due_date'] : null,
|
||
'status' => $invoiceId ? 'applied' : 'pending',
|
||
'created_by' => (int)(session()->get('user_id') ?? 0),
|
||
'created_at' => \CodeIgniter\I18n\Time::now('UTC')->toDateTimeString(), // store UTC
|
||
];
|
||
|
||
$this->db->transStart();
|
||
|
||
// BEFORE
|
||
$invoiceBefore = $this->invoiceModel->getInvoicesByParentId($data['parent_id'], $this->schoolYear);
|
||
|
||
// Insert charge
|
||
$this->additionalChargeModel->insert($payload);
|
||
$chargeId = (int)$this->additionalChargeModel->getInsertID();
|
||
|
||
// Apply to invoice if present
|
||
if ($invoiceId) {
|
||
try {
|
||
if ($chargeType === 'add') {
|
||
$this->invoiceModel->applyAdditionalCharge($invoiceId, $amountAbs);
|
||
} else {
|
||
$this->invoiceModel->deductAdditionalCharge($invoiceId, $amountAbs);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'apply/deductAdditionalCharge failed: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// AFTER
|
||
$invoiceAfter = $this->invoiceModel->getInvoicesByParentId($data['parent_id'], $this->schoolYear);
|
||
|
||
// Parent USER (not parent table)
|
||
$parentUser = $this->userModel->getUserInfoById($data['parent_id']);
|
||
|
||
$this->db->transComplete();
|
||
|
||
if (!$this->db->transStatus()) {
|
||
$err = $this->db->error();
|
||
log_message('error', 'Transaction failed in ExtraChargesController::store: ' . json_encode($err));
|
||
if ($this->wantsJson()) {
|
||
return $this->response->setJSON(['ok' => false, 'error' => 'Failed to save charge', 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash()]);
|
||
}
|
||
return redirect()->back()->withInput()->with('error', 'Failed to save charge.');
|
||
}
|
||
|
||
// Build event payload (same style as your payment handler)
|
||
// allow either first_name/last_name OR firstname/lastname from users table
|
||
$first = $parentUser['first_name'] ?? $parentUser['firstname'] ?? '';
|
||
$last = $parentUser['last_name'] ?? $parentUser['lastname'] ?? '';
|
||
$parentName = trim($first . ' ' . $last);
|
||
|
||
$invoiceBefore = $this->normalizeRow($invoiceBefore);
|
||
$invoiceAfter = $this->normalizeRow($invoiceAfter);
|
||
|
||
$invoiceTotal = $invoiceAfter ? (float)($invoiceAfter['total_amount'] ?? 0) : 0.0;
|
||
$preBalance = $invoiceBefore ? (float)($invoiceBefore['balance'] ?? 0) : 0.0;
|
||
$postBalance = $invoiceAfter ? (float)($invoiceAfter['balance'] ?? 0) : 0.0;
|
||
|
||
$eventData = [
|
||
// user identity (parent user)
|
||
'user_id' => (int)($parentUser['id'] ?? 0),
|
||
'email' => $parentUser['email'] ?? null,
|
||
'firstname' => $first,
|
||
'lastname' => $last,
|
||
'parentName' => $parentName,
|
||
|
||
// context
|
||
'school_year' => $payload['school_year'],
|
||
'semester' => $payload['semester'],
|
||
|
||
// invoice
|
||
'invoice_id' => $invoiceId,
|
||
'invoice_number' => $invoiceAfter['invoice_number'] ?? ($invoiceId ? 'INV-' . $invoiceId : null),
|
||
|
||
// charge details
|
||
'charge_id' => $chargeId,
|
||
'charge_title' => $payload['title'],
|
||
'charge_desc' => $payload['description'],
|
||
'charge_type' => $payload['charge_type'], // add|deduct
|
||
'amount_signed' => $signedAmount,
|
||
'amount_abs' => $amountAbs,
|
||
'due_date' => $payload['due_date'],
|
||
'created_at' => $payload['created_at'],
|
||
|
||
// money snapshot
|
||
'pre_balance' => $preBalance,
|
||
'post_balance' => $postBalance,
|
||
'invoice_total' => $invoiceTotal,
|
||
|
||
// links
|
||
'portal_link' => base_url('/login'),
|
||
'invoice_link' => $invoiceId ? site_url('parent/invoices/view/' . $invoiceId) : site_url('parent/invoices'),
|
||
];
|
||
|
||
$students = []; // keep [] unless you want to include children under this user
|
||
Events::trigger('extraCharge', $eventData, $students);
|
||
|
||
if ($this->wantsJson()) {
|
||
return $this->response->setJSON([
|
||
'ok' => true,
|
||
'id' => $chargeId,
|
||
'invoice_id' => $invoiceId,
|
||
'parent_id' => (int)$data['parent_id'],
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
return redirect()->to(site_url('admin/charges'))->with('status', 'Charge recorded.');
|
||
}
|
||
|
||
/**
|
||
* Mark a charge as void and roll back its impact on the invoice if applied.
|
||
*/
|
||
public function void($id)
|
||
{
|
||
$row = $this->additionalChargeModel->find((int)$id);
|
||
if (!$row) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Charge not found']);
|
||
return redirect()->back()->with('error', 'Charge not found.');
|
||
}
|
||
|
||
$invoiceId = (int)($row['invoice_id'] ?? 0);
|
||
$amountAbs = round(abs((float)($row['amount'] ?? 0)), 2);
|
||
$chargeType = (string)($row['charge_type'] ?? 'add');
|
||
$status = (string)($row['status'] ?? 'pending');
|
||
|
||
$this->db->transStart();
|
||
|
||
if ($status === 'applied' && $invoiceId > 0 && $amountAbs > 0) {
|
||
try {
|
||
if ($chargeType === 'add') {
|
||
$this->invoiceModel->reverseAdditionalCharge($invoiceId, $amountAbs);
|
||
} else {
|
||
// voiding a deduction -> add back
|
||
$this->invoiceModel->applyAdditionalCharge($invoiceId, $amountAbs);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'void(): invoice adjust failed: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
$this->additionalChargeModel->update((int)$id, [
|
||
'status' => 'void',
|
||
]);
|
||
|
||
$this->db->transComplete();
|
||
|
||
if (!$this->db->transStatus()) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Failed to void charge']);
|
||
return redirect()->back()->with('error', 'Failed to void charge.');
|
||
}
|
||
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => true]);
|
||
return redirect()->back()->with('status', 'Charge voided.');
|
||
}
|
||
|
||
/**
|
||
* Reverse a previously applied charge: undo invoice impact and return to pending state.
|
||
*/
|
||
public function reverse($id)
|
||
{
|
||
$row = $this->additionalChargeModel->find((int)$id);
|
||
if (!$row) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Charge not found']);
|
||
return redirect()->back()->with('error', 'Charge not found.');
|
||
}
|
||
|
||
$invoiceId = (int)($row['invoice_id'] ?? 0);
|
||
$amountAbs = round(abs((float)($row['amount'] ?? 0)), 2);
|
||
$chargeType = (string)($row['charge_type'] ?? 'add');
|
||
$status = (string)($row['status'] ?? 'pending');
|
||
|
||
if ($status !== 'applied' || $invoiceId <= 0 || $amountAbs <= 0) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Nothing to reverse']);
|
||
return redirect()->back()->with('error', 'Nothing to reverse.');
|
||
}
|
||
|
||
$this->db->transStart();
|
||
try {
|
||
if ($chargeType === 'add') {
|
||
$this->invoiceModel->reverseAdditionalCharge($invoiceId, $amountAbs);
|
||
} else {
|
||
$this->invoiceModel->applyAdditionalCharge($invoiceId, $amountAbs);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'reverse(): invoice adjust failed: ' . $e->getMessage());
|
||
}
|
||
|
||
$this->additionalChargeModel->update((int)$id, [
|
||
'status' => 'pending',
|
||
'invoice_id' => null,
|
||
]);
|
||
|
||
$this->db->transComplete();
|
||
|
||
if (!$this->db->transStatus()) {
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => false, 'error' => 'Failed to reverse charge']);
|
||
return redirect()->back()->with('error', 'Failed to reverse charge.');
|
||
}
|
||
|
||
if ($this->wantsJson()) return $this->response->setJSON(['ok' => true]);
|
||
return redirect()->back()->with('status', 'Charge reversed to pending.');
|
||
}
|
||
|
||
/** JSON: list charges for the current term (with optional filters). */
|
||
public function apiList()
|
||
{
|
||
$year = (string)($this->request->getGet('school_year') ?? $this->schoolYear);
|
||
$sem = (string)($this->request->getGet('semester') ?? $this->semester);
|
||
$status = $this->request->getGet('status') ?: null;
|
||
$q = trim((string)($this->request->getGet('q') ?? '')) ?: null;
|
||
$per = (int)($this->request->getGet('per_page') ?? 50);
|
||
|
||
$rows = $this->additionalChargeModel->listAllForTerm($year, $sem, $status, $q, $per);
|
||
$pager = $this->additionalChargeModel->pager;
|
||
|
||
$meta = [
|
||
'perPage' => method_exists($pager, 'getPerPage') ? $pager->getPerPage() : $per,
|
||
'page' => method_exists($pager, 'getCurrentPage') ? $pager->getCurrentPage() : null,
|
||
'total' => method_exists($pager, 'getTotal') ? $pager->getTotal() : null,
|
||
'pageCount' => method_exists($pager, 'getPageCount') ? $pager->getPageCount() : null,
|
||
];
|
||
|
||
return $this->response->setJSON([
|
||
'ok' => true,
|
||
'school_year' => $year,
|
||
'semester' => $sem,
|
||
'rows' => $rows,
|
||
'pager' => $meta,
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
|
||
// Normalize query result into a single associative row
|
||
private function normalizeRow($row): ?array
|
||
{
|
||
if (!$row) return null;
|
||
if (is_array($row) && isset($row[0]) && is_array($row[0])) {
|
||
return $row[0]; // unwrap first row from a result set
|
||
}
|
||
return is_array($row) ? $row : null;
|
||
}
|
||
}
|