add projet
This commit is contained in:
+844
@@ -0,0 +1,844 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\LoginActivity;
|
||||
use App\Models\Refund;
|
||||
use App\Models\StaffAttendance;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use App\Services\FeeCalculationService;
|
||||
|
||||
class AdministratorController extends BaseApiController
|
||||
{
|
||||
protected $user;
|
||||
protected $student;
|
||||
protected $config;
|
||||
protected $loginActivity;
|
||||
protected $enrollment;
|
||||
protected $refund;
|
||||
protected $invoice;
|
||||
protected $classSection;
|
||||
protected $userRole;
|
||||
protected $studentClass;
|
||||
protected $staffAttendance;
|
||||
protected $db;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// Use CI4's model() to allow tests to inject mocks
|
||||
$this->user = model(User::class);
|
||||
$this->student = model(Student::class);
|
||||
$this->config = model(Configuration::class);
|
||||
$this->loginActivity = model(LoginActivity::class);
|
||||
$this->enrollment = model(Enrollment::class);
|
||||
$this->refund = model(Refund::class);
|
||||
$this->invoice = model(Invoice::class);
|
||||
$this->classSection = model(ClassSection::class);
|
||||
$this->userRole = model(UserRole::class);
|
||||
$this->studentClass = model(StudentClass::class);
|
||||
$this->staffAttendance= model(StaffAttendance::class);
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->schoolYear = $this->config->getConfig('school_year');
|
||||
$this->semester = $this->config->getConfig('semester');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard metrics
|
||||
* GET /api/v1/administrator/dashboard
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
try {
|
||||
$recentActivities = $this->loginActivity->getLastActivities(4);
|
||||
if (!is_array($recentActivities)) {
|
||||
$recentActivities = [];
|
||||
}
|
||||
|
||||
$totalAdmins = (int) ($this->user->countAdminsBySchoolYear($this->schoolYear) ?? 0);
|
||||
|
||||
$teachers = $this->user->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear);
|
||||
$totalTeachers = count(array_unique(array_column($teachers, 'id')));
|
||||
|
||||
$teacherAssistants = $this->user->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear);
|
||||
$totalTeacherAssistants = count(array_unique(array_column($teacherAssistants, 'id')));
|
||||
|
||||
$parents = $this->user->getUsersByRoleAndSchoolYear('parent', $this->schoolYear);
|
||||
$totalParents = count(array_unique(array_column($parents, 'id')));
|
||||
|
||||
$totalStudents = (int) (
|
||||
$this->db->table('student_class')
|
||||
->select('COUNT(DISTINCT student_class.student_id) AS cnt')
|
||||
->where('student_class.school_year', $this->schoolYear)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getRow('cnt')
|
||||
?? 0
|
||||
);
|
||||
|
||||
// Align shape with View\AdministratorController::buildDashboardMetrics()
|
||||
$data = [
|
||||
'counts' => [
|
||||
'students' => $totalStudents,
|
||||
'teachers' => $totalTeachers,
|
||||
'teacherAssistants' => $totalTeacherAssistants,
|
||||
'admins' => $totalAdmins,
|
||||
'parents' => $totalParents,
|
||||
],
|
||||
'recentActivities' => array_map(static function ($activity) {
|
||||
if (!is_array($activity)) { return []; }
|
||||
return [
|
||||
'login_time' => $activity['login_time'] ?? null,
|
||||
'email' => $activity['email'] ?? null,
|
||||
];
|
||||
}, $recentActivities),
|
||||
'meta' => [
|
||||
'schoolYear' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->success($data, 'Dashboard metrics retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Dashboard metrics error: ' . $e->getMessage());
|
||||
return $this->error('Failed to retrieve dashboard metrics', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute allowed absence dates (future Sundays within Sep..May for configured school year)
|
||||
*/
|
||||
private function allowedAbsenceDates(): array
|
||||
{
|
||||
$todayStr = local_date(utc_now(), 'Y-m-d');
|
||||
try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); }
|
||||
|
||||
$sy = (string)($this->schoolYear ?? '');
|
||||
$startYear = null; $endYear = null;
|
||||
if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) {
|
||||
$startYear = (int)$m[1];
|
||||
$endYear = (int)$m[2];
|
||||
} else {
|
||||
$cy = (int)date('Y');
|
||||
$cm = (int)date('n');
|
||||
if ($cm >= 9) { // Sep-Dec
|
||||
$startYear = $cy;
|
||||
$endYear = $cy + 1;
|
||||
} else {
|
||||
$startYear = $cy - 1;
|
||||
$endYear = $cy;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear));
|
||||
$end = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear));
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($start < $today) {
|
||||
$start = $today;
|
||||
}
|
||||
|
||||
$dates = [];
|
||||
for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) {
|
||||
if ((int)$cursor->format('w') === 0) { // Sunday
|
||||
$dates[] = $cursor->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/absence
|
||||
* Return current admin absence context and allowed dates
|
||||
*/
|
||||
public function absenceInfo()
|
||||
{
|
||||
$userId = $this->getCurrentUserId();
|
||||
if (!$userId) {
|
||||
return $this->error('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$admin = $this->user->find($userId);
|
||||
$displayName = $admin ? trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')) : 'Administrator';
|
||||
|
||||
$semester = (string)($this->semester ?? '');
|
||||
$schoolYear = (string)($this->schoolYear ?? '');
|
||||
|
||||
$existing = $this->staffAttendance
|
||||
->where('user_id', $userId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('date', 'DESC')
|
||||
->findAll();
|
||||
|
||||
return $this->success([
|
||||
'admin_name' => $displayName,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'existing' => $existing,
|
||||
'available_dates'=> $this->allowedAbsenceDates(),
|
||||
], 'Absence info retrieved');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/administrator/absence
|
||||
* Submit absence entries for current admin
|
||||
* Payload: { dates: ["YYYY-MM-DD",...], reason_type?: string, reason: string }
|
||||
*/
|
||||
public function submitAbsence()
|
||||
{
|
||||
$userId = $this->getCurrentUserId();
|
||||
if (!$userId) {
|
||||
return $this->error('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$payload = $this->request->getJSON(true) ?? [];
|
||||
$dates = (array)($payload['dates'] ?? []);
|
||||
$reasonType = trim((string)($payload['reason_type'] ?? ''));
|
||||
$reasonText = trim((string)($payload['reason'] ?? ''));
|
||||
|
||||
$semester = (string)($this->semester ?? '');
|
||||
$schoolYear = (string)($this->schoolYear ?? '');
|
||||
|
||||
if ($semester === '' || $schoolYear === '') {
|
||||
return $this->error('Semester or school year not configured', 500);
|
||||
}
|
||||
if ($reasonText === '') {
|
||||
return $this->error('Reason is required', 422);
|
||||
}
|
||||
|
||||
$reasonBase = ($reasonType !== '') ? strtolower($reasonType) : '';
|
||||
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
|
||||
|
||||
$allowedDates = $this->allowedAbsenceDates();
|
||||
$allowedSet = array_fill_keys($allowedDates, true);
|
||||
|
||||
$roleName = $this->user->getUserRole($userId) ?: null;
|
||||
|
||||
$saved = 0; $invalid = []; $savedDates = [];
|
||||
$dates = array_values(array_unique(array_map('strval', $dates)));
|
||||
foreach ($dates as $d) {
|
||||
$d = trim((string)$d);
|
||||
if ($d === '') continue;
|
||||
$dt = date_create_from_format('Y-m-d', $d);
|
||||
if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
|
||||
$invalid[] = $d;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ok = $this->staffAttendance->upsertOne(
|
||||
userId: $userId,
|
||||
roleName: $roleName,
|
||||
date: $d,
|
||||
semester: $semester,
|
||||
schoolYear: $schoolYear,
|
||||
status: StaffAttendance::STATUS_ABSENT,
|
||||
reason: $reason,
|
||||
editorId: $userId
|
||||
);
|
||||
if ($ok) { $saved++; $savedDates[] = $d; }
|
||||
}
|
||||
|
||||
// Best-effort principal email
|
||||
try {
|
||||
$user = $this->user->find($userId) ?: [];
|
||||
$fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Administrator';
|
||||
$userEmail = $user['email'] ?? '';
|
||||
$role = $roleName ?: 'admin';
|
||||
$dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
|
||||
|
||||
$subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates');
|
||||
|
||||
$body = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
|
||||
. '<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
|
||||
. '<p style="margin:0 0 12px;">A staff time-off request was submitted from the administrator portal.</p>'
|
||||
. '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
|
||||
. '<tr><td><strong>Name</strong></td><td>' . esc($fullName) . '</td></tr>'
|
||||
. '<tr><td><strong>Email</strong></td><td>' . esc($userEmail) . '</td></tr>'
|
||||
. '<tr><td><strong>Role</strong></td><td>' . esc((string)$role) . '</td></tr>'
|
||||
. '<tr><td><strong>Semester</strong></td><td>' . esc($semester) . '</td></tr>'
|
||||
. '<tr><td><strong>School Year</strong></td><td>' . esc($schoolYear) . '</td></tr>'
|
||||
. '<tr><td><strong>Reason Type</strong></td><td>' . esc($reasonType ?: '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Reason</strong></td><td>' . esc($reasonText) . '</td></tr>'
|
||||
. '<tr><td><strong>Dates</strong></td><td>' . esc($dateList ?: '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Submitted At</strong></td><td>' . esc(utc_now()) . '</td></tr>'
|
||||
. '</table>'
|
||||
. '</div>';
|
||||
|
||||
$mailer = \Config\Services::emailService();
|
||||
$principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org';
|
||||
$mailer->send($principalEmail, $subject, $body, 'notifications');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to send TimeOff email (api admin): ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'saved' => $saved,
|
||||
'invalid' => $invalid,
|
||||
'saved_dates' => $savedDates,
|
||||
], $saved . ' day(s) saved as absent.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users, students, parents, staff, emergency contacts
|
||||
* GET /api/v1/administrator/search
|
||||
*/
|
||||
public function search()
|
||||
{
|
||||
$q = trim((string)($this->request->getGet('q') ?? ''));
|
||||
if ($q === '') {
|
||||
return $this->error('Search query is required', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = $this->db;
|
||||
|
||||
// 1) Tokenize
|
||||
$rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$tokens = array_values(array_filter(array_map('trim', $rawTokens)));
|
||||
|
||||
// 2) Phone variants per token
|
||||
$phoneMap = [];
|
||||
foreach ($tokens as $t) {
|
||||
$digits = preg_replace('/\D+/', '', $t);
|
||||
if ($digits === '') continue;
|
||||
$v = [];
|
||||
if (strlen($digits) >= 7) {
|
||||
$v[] = $digits;
|
||||
if (strlen($digits) === 10) {
|
||||
$v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = '1' . $digits;
|
||||
$v[] = '+1' . $digits;
|
||||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
} elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) {
|
||||
$ten = substr($digits, 1);
|
||||
$v[] = $ten;
|
||||
$v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = '+1' . $ten;
|
||||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
} else {
|
||||
$v[] = $digits; // partials
|
||||
}
|
||||
}
|
||||
if (!empty($v)) $phoneMap[$t] = array_values(array_unique($v));
|
||||
}
|
||||
|
||||
// Helper
|
||||
$applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) {
|
||||
foreach ($tokens as $t) {
|
||||
$qb->groupStart();
|
||||
foreach ($columns as $i => $col) {
|
||||
if ($i === 0) $qb->like($col, $t); else $qb->orLike($col, $t);
|
||||
}
|
||||
if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
|
||||
foreach ($phoneMap[$t] as $pv) {
|
||||
foreach ($phoneCols as $pcol) { $qb->orLike($pcol, $pv); }
|
||||
}
|
||||
}
|
||||
$qb->groupEnd();
|
||||
}
|
||||
return $qb;
|
||||
};
|
||||
|
||||
// USERS (phone: cellphone)
|
||||
$uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
|
||||
$uQB = $db->table('users')->select('id, firstname, lastname, email, cellphone, school_id, city, state, school_year, semester');
|
||||
$applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
|
||||
$users = $uQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// STUDENTS (no phone field)
|
||||
$sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
|
||||
$sQB = $db->table('students')->select('id, parent_id, school_id, firstname, lastname, dob, gender, school_year, semester, rfid_tag');
|
||||
$applyMultiTokenLike($sQB, $sCols, $tokens, []);
|
||||
$students = $sQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// PARENTS (phone: secondparent_phone)
|
||||
$pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
|
||||
$pQB = $db->table('parents')->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone, school_year, semester');
|
||||
$applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
|
||||
foreach ($tokens as $t) { if (ctype_digit($t)) { $pQB->orWhere('firstparent_id', (int)$t)->orWhere('id', (int)$t); } }
|
||||
$parents = $pQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// STAFF (phone: phone)
|
||||
$stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
|
||||
$stQB = $db->table('staff')->select('id, user_id, firstname, lastname, email, phone, role_name, school_year, active_role');
|
||||
$applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
|
||||
$staff = $stQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// EMERGENCY CONTACTS (phone: cellphone)
|
||||
$ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
|
||||
$ecQB = $db->table('emergency_contacts')->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester');
|
||||
$applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
|
||||
$emergency = $ecQB->limit(150)->get()->getResultArray();
|
||||
|
||||
$results = [
|
||||
'users' => $users,
|
||||
'students' => $students,
|
||||
'parents' => $parents,
|
||||
'staff' => $staff,
|
||||
'emergency_contacts' => $emergency,
|
||||
'total' => count($users) + count($students) + count($parents) + count($staff) + count($emergency),
|
||||
];
|
||||
|
||||
return $this->success($results, 'Search completed successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Search error: ' . $e->getMessage());
|
||||
return $this->error('Failed to perform search', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enrollment/withdrawal data
|
||||
* GET /api/v1/administrator/enrollment-withdrawal
|
||||
*/
|
||||
public function enrollmentWithdrawal()
|
||||
{
|
||||
$page = (int) ($this->request->getGet('page') ?? 1);
|
||||
$perPage = min(100, (int) ($this->request->getGet('per_page') ?? 20));
|
||||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
|
||||
try {
|
||||
$query = $this->enrollment
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC');
|
||||
|
||||
$result = $this->paginate($query, $page, $perPage);
|
||||
return $this->success($result, 'Enrollment/withdrawal data retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage());
|
||||
return $this->error('Failed to retrieve enrollment/withdrawal data', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/enrollment-withdrawal/data
|
||||
* Rich data used by admin UI: students, classes, school years
|
||||
*/
|
||||
public function enrollmentWithdrawalData()
|
||||
{
|
||||
try {
|
||||
$selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
|
||||
if ($selectedYear === '') { $selectedYear = (string)$this->schoolYear; }
|
||||
|
||||
// Distinct school years
|
||||
$yearsRows = $this->db->table('enrollments')->distinct()->select('school_year')->orderBy('school_year', 'DESC')->get()->getResultArray();
|
||||
$schoolYears = array_values(array_filter(array_map(static fn($r) => isset($r['school_year']) ? (string)$r['school_year'] : null, $yearsRows)));
|
||||
|
||||
$students = $this->student->getStudentsWithClassAndEnrollment();
|
||||
|
||||
foreach ($students as &$s) {
|
||||
$s['student_id'] = (int)($s['id'] ?? 0);
|
||||
if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
|
||||
$s['parent_id'] = (int)$s['secondparent_user_id'];
|
||||
} else {
|
||||
$s['parent_id'] = (int)($s['parent_id'] ?? 0);
|
||||
}
|
||||
|
||||
$pf = trim((string)($s['parent_firstname'] ?? ''));
|
||||
$pl = trim((string)($s['parent_lastname'] ?? ''));
|
||||
if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
|
||||
$parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2);
|
||||
$pf = $parts[0] ?? '';
|
||||
$pl = $parts[1] ?? '';
|
||||
}
|
||||
$s['parent_label'] = trim($pf . ' ' . $pl) ?: 'Unknown Parent';
|
||||
$s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent';
|
||||
|
||||
$s['is_new'] = (int) ($s['is_new'] ?? 0);
|
||||
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
|
||||
|
||||
$statusForYear = $this->enrollment->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
|
||||
if (!empty($statusForYear)) {
|
||||
$s['enrollment_status'] = $statusForYear;
|
||||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||||
$s['enrollment_status'] = 'denied';
|
||||
}
|
||||
|
||||
$className = $this->studentClass->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
|
||||
$s['class_section'] = $className ?: 'Class not Assigned';
|
||||
|
||||
$s['registration_date_order'] = !empty($s['registration_date']) ? date('Y-m-d', strtotime($s['registration_date'])) : '';
|
||||
}
|
||||
unset($s);
|
||||
|
||||
usort($students, function (array $a, array $b) {
|
||||
$pa = $a['parent_sort'] ?? '';
|
||||
$pb = $b['parent_sort'] ?? '';
|
||||
if (strcasecmp($pa, $pb) === 0) {
|
||||
$la = $a['lastname'] ?? '';
|
||||
$lb = $b['lastname'] ?? '';
|
||||
$cmp = strcasecmp($la, $lb);
|
||||
if ($cmp !== 0) return $cmp;
|
||||
return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
|
||||
}
|
||||
return strcasecmp($pa, $pb);
|
||||
});
|
||||
|
||||
$classes = $this->classSection
|
||||
->select('id, class_section_id, class_section_name, school_year, semester')
|
||||
->where('school_year', (string)$selectedYear)
|
||||
->where('semester', (string)$this->semester)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
if (empty($classes)) {
|
||||
$classes = $this->classSection
|
||||
->select('id, class_section_id, class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'students' => $students,
|
||||
'classes' => $classes,
|
||||
'semester' => (string)$this->semester,
|
||||
'school_year' => (string)$selectedYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
], 'Enrollment/withdrawal data');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'enrollmentWithdrawalData error: {msg}', ['msg' => $e->getMessage()]);
|
||||
return $this->error('Server error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/administrator/enrollment-withdrawal/update
|
||||
* Update enrollment statuses in batch and trigger notifications/refunds
|
||||
* Payload: { enrollment_status: { student_id: status, ... } }
|
||||
*/
|
||||
public function updateEnrollmentStatuses()
|
||||
{
|
||||
$payload = $this->request->getJSON(true) ?? [];
|
||||
$enrollmentStatuses = $payload['enrollment_status'] ?? null;
|
||||
if (empty($enrollmentStatuses) || !is_array($enrollmentStatuses)) {
|
||||
return $this->error('No enrollment statuses were submitted.', 422);
|
||||
}
|
||||
|
||||
$refundService = new FeeCalculationService();
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
$errors = [];
|
||||
$groupsByParentStatus = [];
|
||||
$parentInfo = [];
|
||||
$refundParents = [];
|
||||
$refundAmountByParent = [];
|
||||
|
||||
$validStatuses = [
|
||||
'admission under review', 'payment pending', 'enrolled',
|
||||
'withdraw under review', 'refund pending', 'withdrawn', 'denied', 'waitlist',
|
||||
];
|
||||
|
||||
foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
|
||||
if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
|
||||
$errors[] = "Invalid enrollment status '$newEnrollmentStatus' for student ID $studentId.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($newEnrollmentStatus === 'denied') { $admissionStatus = 'denied'; }
|
||||
elseif (in_array($newEnrollmentStatus, ['enrolled', 'payment pending'], true)) { $admissionStatus = 'accepted'; }
|
||||
else { $admissionStatus = 'pending'; }
|
||||
|
||||
$enrollmentRow = $this->db->table('enrollments')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$enrollmentRow) {
|
||||
$stu = $this->student->find((int)$studentId) ?? [];
|
||||
$parentId = (int)($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0));
|
||||
if (!$parentId) { $errors[] = "No parent ID found for student ID $studentId."; continue; }
|
||||
|
||||
$isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;
|
||||
|
||||
$ok = $this->db->table('enrollments')->insert([
|
||||
'student_id' => (int)$studentId,
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => (string)$this->schoolYear,
|
||||
'semester' => (string)$this->semester,
|
||||
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
|
||||
'is_withdrawn' => $isWithdrawn,
|
||||
'enrollment_status' => $newEnrollmentStatus,
|
||||
'admission_status' => $admissionStatus,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
|
||||
if (!$ok) { $errors[] = "Failed to create enrollment for student ID $studentId."; continue; }
|
||||
|
||||
$studentRow = $this->student->find($studentId) ?? [];
|
||||
$studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";
|
||||
|
||||
if (!isset($parentInfo[$parentId])) {
|
||||
$p = $this->user->find($parentId) ?? [];
|
||||
$parentInfo[$parentId] = [
|
||||
'user_id' => $p['id'] ?? $parentId,
|
||||
'email' => $p['email'] ?? null,
|
||||
'firstname' => $p['firstname'] ?? '',
|
||||
'lastname' => $p['lastname'] ?? '',
|
||||
];
|
||||
}
|
||||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||||
'student_id' => (int) $studentId,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
|
||||
if ($newEnrollmentStatus === 'refund pending') { $refundParents[$parentId] = true; }
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldStatus = $enrollmentRow['enrollment_status'] ?? null;
|
||||
$parentId = $enrollmentRow['parent_id'] ?? null;
|
||||
if (!$parentId) { $errors[] = "No parent ID found for student ID $studentId."; continue; }
|
||||
if ($oldStatus === $newEnrollmentStatus) { continue; }
|
||||
|
||||
$updated = $this->db->table('enrollments')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->update([
|
||||
'enrollment_status' => $newEnrollmentStatus,
|
||||
'admission_status' => $admissionStatus,
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
if (!$updated) { $errors[] = "Failed to update enrollment for student ID $studentId."; continue; }
|
||||
|
||||
$studentRow = $this->student->find($studentId);
|
||||
$studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";
|
||||
|
||||
if (!isset($parentInfo[$parentId])) {
|
||||
$p = $this->user->find($parentId) ?? [];
|
||||
$parentInfo[$parentId] = [
|
||||
'user_id' => $p['id'] ?? $parentId,
|
||||
'email' => $p['email'] ?? null,
|
||||
'firstname' => $p['firstname'] ?? '',
|
||||
'lastname' => $p['lastname'] ?? '',
|
||||
];
|
||||
}
|
||||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||||
'student_id' => (int) $studentId,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
if ($newEnrollmentStatus === 'refund pending') { $refundParents[$parentId] = true; }
|
||||
}
|
||||
|
||||
// Refunds
|
||||
foreach (array_keys($refundParents) as $pid) {
|
||||
$students = $this->enrollment
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->findAll();
|
||||
if (empty($students)) { continue; }
|
||||
|
||||
$invoice = $this->invoice->where('parent_id', $pid)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->first();
|
||||
if (!$invoice) { $errors[] = "No invoice found for parent ID $pid (for refund calc)."; continue; }
|
||||
|
||||
$refundAmount = $refundService->calculateRefund($students, $pid);
|
||||
$refundAmountByParent[$pid] = $refundAmount;
|
||||
|
||||
$existingRefund = $this->refund->where('invoice_id', $invoice['id'])->first();
|
||||
if ($existingRefund) {
|
||||
$this->refund->update($existingRefund['id'], [
|
||||
'refund_amount' => $refundAmount,
|
||||
'status' => 'Pending',
|
||||
'updated_by' => $this->getCurrentUserId(),
|
||||
]);
|
||||
} else {
|
||||
$this->refund->insert([
|
||||
'parent_id' => $pid,
|
||||
'school_year' => $invoice['school_year'],
|
||||
'invoice_id' => $invoice['id'],
|
||||
'refund_amount' => $refundAmount,
|
||||
'refund_paid_amount' => 0.0,
|
||||
'status' => 'Pending',
|
||||
'requested_at' => utc_now(),
|
||||
'updated_by' => $this->getCurrentUserId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
if (!$this->db->transStatus()) {
|
||||
return $this->error('A database error occurred. Changes were rolled back.', 500);
|
||||
}
|
||||
|
||||
// Build response summary
|
||||
return $this->success([
|
||||
'errors' => $errors,
|
||||
'groupsByParentStatus' => $groupsByParentStatus,
|
||||
], empty($errors) ? 'Enrollment statuses updated.' : 'Enrollment updated with some errors.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage());
|
||||
return $this->error('An unexpected error occurred while processing enrollments.', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/students
|
||||
* Return student profiles with parent/emergency and class section name
|
||||
*/
|
||||
public function studentProfiles()
|
||||
{
|
||||
$db = $this->db;
|
||||
$isPg = ($db->getPlatform() === 'Postgre');
|
||||
|
||||
if (!$isPg) { $db->query('SET SESSION group_concat_max_len = 8192'); }
|
||||
$b = $db->table('students');
|
||||
|
||||
if ($isPg) {
|
||||
$students = $b->select([
|
||||
'students.*',
|
||||
'users.firstname AS parent_firstname',
|
||||
'users.lastname AS parent_lastname',
|
||||
'users.email AS parent_email',
|
||||
'users.cellphone AS parent_phone',
|
||||
"(SELECT ec.emergency_contact_name FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_name",
|
||||
"(SELECT ec.relation FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship",
|
||||
"(SELECT ec.cellphone FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_phone",
|
||||
"(SELECT ec.email FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_email",
|
||||
"(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy) FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies",
|
||||
"(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name) FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions",
|
||||
])->join('users', 'users.id = students.parent_id', 'left')
|
||||
->orderBy('students.lastname', 'ASC')->orderBy('students.firstname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
} else {
|
||||
$students = $b->select([
|
||||
'students.*',
|
||||
'users.firstname AS parent_firstname',
|
||||
'users.lastname AS parent_lastname',
|
||||
'users.email AS parent_email',
|
||||
'users.cellphone AS parent_phone',
|
||||
'MIN(emergency_contacts.emergency_contact_name) AS emergency_name',
|
||||
'MIN(emergency_contacts.relation) AS emergency_relationship',
|
||||
'MIN(emergency_contacts.cellphone) AS emergency_phone',
|
||||
'MIN(emergency_contacts.email) AS emergency_email',
|
||||
"GROUP_CONCAT(DISTINCT student_allergies.allergy ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies",
|
||||
"GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions",
|
||||
])->join('users', 'users.id = students.parent_id', 'left')
|
||||
->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left')
|
||||
->join('student_allergies', 'student_allergies.student_id = students.id', 'left')
|
||||
->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left')
|
||||
->groupBy('students.id')
|
||||
->orderBy('students.lastname', 'ASC')->orderBy('students.firstname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
foreach ($students as $i => $row) {
|
||||
$sid = (int) ($row['id'] ?? 0);
|
||||
$students[$i]['class_section_name'] = $sid > 0 ? (string) ($this->studentClass->getClassSectionNameByStudentId($sid) ?? '') : '';
|
||||
}
|
||||
|
||||
return $this->success(['students' => $students]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/parents
|
||||
* Return parent profiles with latest invoice paid/balance
|
||||
*/
|
||||
public function parentProfiles()
|
||||
{
|
||||
$allUsers = $this->user->findAll();
|
||||
$parents = [];
|
||||
|
||||
foreach ($allUsers as $user) {
|
||||
$roles = $this->userRole->getRolesByUserId($user['id']);
|
||||
$isParent = false;
|
||||
if (is_array($roles)) {
|
||||
foreach ($roles as $role) {
|
||||
if (isset($role['role_name']) && $role['role_name'] === 'parent') { $isParent = true; break; }
|
||||
}
|
||||
}
|
||||
if ($isParent) {
|
||||
$paidAmount = $this->invoice->getLatestInvoicePaidAmount($user['id']) ?? 0;
|
||||
$balance = $this->invoice->getLatestInvoiceBalance($user['id']) ?? 0;
|
||||
$parents[] = [
|
||||
'id' => $user['id'],
|
||||
'school_id' => $user['school_id'],
|
||||
'firstname' => $user['firstname'],
|
||||
'lastname' => $user['lastname'],
|
||||
'email' => $user['email'],
|
||||
'cellphone' => $user['cellphone'],
|
||||
'gender' => $user['gender'],
|
||||
'created_at' => $user['created_at'],
|
||||
'school_year' => $user['school_year'],
|
||||
'paid_amount' => $paidAmount,
|
||||
'balance' => $balance,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['parents' => $parents]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/enrollment/new-students
|
||||
* Return students marked as new with parent/emergency + enrollment metadata.
|
||||
*/
|
||||
public function newStudents()
|
||||
{
|
||||
try {
|
||||
$rows = $this->student->getStudentsWithParentsAndEmergency($this->schoolYear, 1);
|
||||
$newStudents = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$studentId = (int) ($row['id'] ?? 0);
|
||||
$classSection = $studentId > 0
|
||||
? ($this->studentClass->getClassSectionsByStudentId($studentId, $this->schoolYear) ?: 'Class not Assigned')
|
||||
: 'Class not Assigned';
|
||||
$enrollmentStatus = $studentId > 0
|
||||
? $this->enrollment->getEnrollmentStatus($studentId, $this->schoolYear)
|
||||
: null;
|
||||
|
||||
$registrationDate = $row['registration_date'] ?? null;
|
||||
if (!empty($registrationDate)) {
|
||||
try {
|
||||
$registrationDate = (new \DateTime($registrationDate))->format('Y-m-d');
|
||||
} catch (\Throwable $e) {
|
||||
$registrationDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
$newStudents[] = array_merge($row, [
|
||||
'student_id' => $studentId,
|
||||
'class_section' => $classSection,
|
||||
'enrollment_status' => $enrollmentStatus,
|
||||
'new_student' => ((int) ($row['is_new'] ?? 0) === 1) ? 'Yes' : 'No',
|
||||
'modalIdContact' => 'contact_' . $studentId,
|
||||
'registration_date' => $registrationDate,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'new_students' => $newStudents,
|
||||
'total_new' => count($newStudents),
|
||||
'school_year' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
], 'New students retrieved successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'newStudents error: ' . $e->getMessage());
|
||||
return $this->error('Failed to fetch new students', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user