add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+844
View File
@@ -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);
}
}
}