262 lines
9.8 KiB
PHP
262 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Administrator;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\SchoolYearModel;
|
|
use App\Support\SchoolYear\SchoolYearStatus;
|
|
use Throwable;
|
|
|
|
class SchoolYearClosingController extends BaseController
|
|
{
|
|
public function preview(int $id)
|
|
{
|
|
try {
|
|
$targetId = $this->normalizeInt($this->request->getGet('target_school_year_id'));
|
|
$source = (new SchoolYearModel())->find($id);
|
|
if ($targetId === null && (string) ($source['status'] ?? '') === SchoolYearStatus::ACTIVE) {
|
|
$target = service('schoolYearManagement')->ensureNextDraftForClosing($id, $this->userId());
|
|
$targetId = (int) ($target['id'] ?? 0) ?: null;
|
|
}
|
|
$preview = service('schoolYearClosing')->preview($id, $targetId);
|
|
$promotionTable = $this->promotionTablePayload($preview['promotion']['rows'] ?? []);
|
|
$carryForwardTable = $this->carryForwardTablePayload($preview['carry_forward'] ?? []);
|
|
$latestBatch = service('schoolYearClosing')->latestBatch($id);
|
|
|
|
return view('school_years/closing_preview', [
|
|
'preview' => $preview,
|
|
'promotionTable' => $promotionTable,
|
|
'carryForwardTable' => $carryForwardTable,
|
|
'latestBatch' => $latestBatch,
|
|
'missingCarryForwardInvoices' => $this->missingCarryForwardInvoiceCount($latestBatch),
|
|
'schoolYears' => (new SchoolYearModel())->orderBy('name', 'DESC')->findAll(),
|
|
]);
|
|
} catch (Throwable $e) {
|
|
return redirect()->to('/administrator/school-years')->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function previewByName(string $name)
|
|
{
|
|
try {
|
|
$year = (new SchoolYearModel())
|
|
->where('name', rawurldecode($name))
|
|
->first();
|
|
|
|
if ($year === null) {
|
|
return redirect()->to('/administrator/school-years')->with('error', 'School year was not found.');
|
|
}
|
|
|
|
return $this->preview((int) $year['id']);
|
|
} catch (Throwable $e) {
|
|
return redirect()->to('/administrator/school-years')->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function start(int $id)
|
|
{
|
|
try {
|
|
$targetId = $this->normalizeInt($this->request->getPost('target_school_year_id'));
|
|
if ($targetId === null) {
|
|
return redirect()->back()->with('error', 'Select a target school year.');
|
|
}
|
|
|
|
service('schoolYearClosing')->start($id, $targetId, $this->userId());
|
|
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'End-year process started.');
|
|
} catch (Throwable $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function execute(int $id)
|
|
{
|
|
try {
|
|
service('schoolYearClosing')->execute($id, $this->userId());
|
|
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'Carry-forward confirmed.');
|
|
} catch (Throwable $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function complete(int $id)
|
|
{
|
|
try {
|
|
service('schoolYearClosing')->complete($id, $this->userId());
|
|
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'School year ended and closed.');
|
|
} catch (Throwable $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function cancel(int $id)
|
|
{
|
|
try {
|
|
service('schoolYearClosing')->cancel($id, $this->userId());
|
|
return redirect()->to('/administrator/school-years')->with('success', 'End-year process cancelled.');
|
|
} catch (Throwable $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function normalizeInt(mixed $value): ?int
|
|
{
|
|
return is_numeric($value) && (int) $value > 0 ? (int) $value : null;
|
|
}
|
|
|
|
private function missingCarryForwardInvoiceCount(?array $batch): int
|
|
{
|
|
if ($batch === null || ! in_array((string) ($batch['status'] ?? ''), ['executed', 'completed'], true)) {
|
|
return 0;
|
|
}
|
|
|
|
$db = \Config\Database::connect();
|
|
if (! $db->tableExists('school_year_closing_items')) {
|
|
return 0;
|
|
}
|
|
|
|
return $db->table('school_year_closing_items')
|
|
->where('closing_batch_id', (int) $batch['id'])
|
|
->groupStart()
|
|
->where('target_invoice_id', null)
|
|
->orWhere('target_invoice_id', 0)
|
|
->groupEnd()
|
|
->countAllResults();
|
|
}
|
|
|
|
private function promotionTablePayload(array $rows): array
|
|
{
|
|
$allowedSorts = ['student', 'school_id', 'class', 'year_score', 'decision', 'source', 'queue', 'target', 'status'];
|
|
$sort = 'class';
|
|
$order = 'asc';
|
|
$perPage = 25;
|
|
$allowedPerPage = [10, 25, 50, 100];
|
|
|
|
usort($rows, function (array $a, array $b) use ($sort, $order): int {
|
|
$comparison = $this->comparePromotionRows($a, $b, $sort);
|
|
if ($comparison === 0 && $sort !== 'class') {
|
|
$comparison = $this->comparePromotionRows($a, $b, 'class');
|
|
}
|
|
if ($comparison === 0 && $sort !== 'student') {
|
|
$comparison = $this->comparePromotionRows($a, $b, 'student');
|
|
}
|
|
if ($comparison === 0) {
|
|
$comparison = ((int) ($a['student_id'] ?? 0)) <=> ((int) ($b['student_id'] ?? 0));
|
|
}
|
|
|
|
return $order === 'desc' ? -$comparison : $comparison;
|
|
});
|
|
|
|
$total = count($rows);
|
|
|
|
return [
|
|
'rows' => $rows,
|
|
'sort' => $sort,
|
|
'order' => $order,
|
|
'page' => 1,
|
|
'perPage' => $perPage,
|
|
'total' => $total,
|
|
'pageCount' => 1,
|
|
'allowedPerPage' => $allowedPerPage,
|
|
'from' => $total === 0 ? 0 : 1,
|
|
'to' => $total,
|
|
];
|
|
}
|
|
|
|
private function comparePromotionRows(array $a, array $b, string $sort): int
|
|
{
|
|
$aValue = $this->promotionSortValue($a, $sort);
|
|
$bValue = $this->promotionSortValue($b, $sort);
|
|
|
|
if ($sort === 'year_score') {
|
|
if ($aValue === null && $bValue === null) {
|
|
return 0;
|
|
}
|
|
if ($aValue === null) {
|
|
return 1;
|
|
}
|
|
if ($bValue === null) {
|
|
return -1;
|
|
}
|
|
|
|
return $aValue <=> $bValue;
|
|
}
|
|
|
|
return strnatcasecmp((string) $aValue, (string) $bValue);
|
|
}
|
|
|
|
private function promotionSortValue(array $row, string $sort): mixed
|
|
{
|
|
return match ($sort) {
|
|
'student' => (string) ($row['student_name'] ?? ''),
|
|
'school_id' => (string) ($row['school_id'] ?? ''),
|
|
'class' => (string) ($row['class_section_name'] ?? ''),
|
|
'year_score' => is_numeric($row['year_score'] ?? null) ? (float) $row['year_score'] : null,
|
|
'decision' => (string) ($row['decision'] ?? ''),
|
|
'source' => (string) ($row['source'] ?? ''),
|
|
'queue' => (string) ($row['queue_status'] ?? ''),
|
|
'target' => trim((string) ($row['target_section'] ?? '') . ' ' . (string) ($row['target_class'] ?? '')),
|
|
'status' => (string) ($row['status'] ?? ''),
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function carryForwardTablePayload(array $rows): array
|
|
{
|
|
$allowedSorts = ['family', 'parent', 'source_balance', 'credits', 'adjustments', 'net'];
|
|
$sort = (string) ($this->request->getGet('cf_sort') ?? 'family');
|
|
$sort = in_array($sort, $allowedSorts, true) ? $sort : 'family';
|
|
$order = strtolower((string) ($this->request->getGet('cf_order') ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
|
|
|
usort($rows, function (array $a, array $b) use ($sort, $order): int {
|
|
$comparison = $this->compareCarryForwardRows($a, $b, $sort);
|
|
if ($comparison === 0 && $sort !== 'family') {
|
|
$comparison = $this->compareCarryForwardRows($a, $b, 'family');
|
|
}
|
|
if ($comparison === 0) {
|
|
$comparison = ((int) ($a['family_id'] ?? 0)) <=> ((int) ($b['family_id'] ?? 0));
|
|
}
|
|
|
|
return $order === 'desc' ? -$comparison : $comparison;
|
|
});
|
|
|
|
return [
|
|
'rows' => $rows,
|
|
'sort' => $sort,
|
|
'order' => $order,
|
|
];
|
|
}
|
|
|
|
private function compareCarryForwardRows(array $a, array $b, string $sort): int
|
|
{
|
|
$numericSorts = ['source_balance', 'credits', 'adjustments', 'net'];
|
|
$aValue = $this->carryForwardSortValue($a, $sort);
|
|
$bValue = $this->carryForwardSortValue($b, $sort);
|
|
|
|
if (in_array($sort, $numericSorts, true)) {
|
|
return (float) $aValue <=> (float) $bValue;
|
|
}
|
|
|
|
return strnatcasecmp((string) $aValue, (string) $bValue);
|
|
}
|
|
|
|
private function carryForwardSortValue(array $row, string $sort): mixed
|
|
{
|
|
return match ($sort) {
|
|
'family' => (string) ($row['family'] ?? ''),
|
|
'parent' => trim((string) ($row['parent'] ?? '') . ' ' . (string) ($row['parent_email'] ?? '')),
|
|
'source_balance' => (float) ($row['source_balance'] ?? 0),
|
|
'credits' => (float) ($row['credit_amount'] ?? 0),
|
|
'adjustments' => (float) ($row['adjustment_amount'] ?? 0),
|
|
'net' => (float) ($row['carry_forward_amount'] ?? 0),
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function userId(): ?int
|
|
{
|
|
$id = session('user_id') ?? session('id');
|
|
|
|
return is_numeric($id) ? (int) $id : null;
|
|
}
|
|
}
|