fix parent, teacher and admin pages

This commit is contained in:
root
2026-04-25 00:00:23 -04:00
parent 4cd98f1d30
commit eafe4eb134
30 changed files with 1566 additions and 105 deletions
@@ -15,6 +15,7 @@ use App\Models\User;
use App\Models\UserRole;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class AttendanceQueryService
@@ -181,6 +182,10 @@ class AttendanceQueryService
$attendanceRows = [];
$lockedByStudent = [];
$attendanceTable = $this->attendanceData->getTable();
$dateInSundaysSql = 'DATE(`' . $attendanceTable . '`.`date`) IN (' .
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
foreach ($students as $student) {
$studentId = (int)$student['id'];
@@ -189,11 +194,13 @@ class AttendanceQueryService
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) use ($classSectionId) {
$q->where('class_section_id', $classSectionId)
->orWhere('class_section_id', (int)$classSectionId);
->where(function ($q) use ($classSectionId, $classSectionPk) {
$q->where('class_section_id', $classSectionId);
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
$q->orWhere('class_section_id', $classSectionPk);
}
})
->whereIn('date', $sundayDates)
->whereRaw($dateInSundaysSql, $sundayDates)
->get()
->map(fn($row) => (array)$row)
->all();
@@ -205,9 +212,11 @@ class AttendanceQueryService
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) use ($classSectionId) {
$q->where('class_section_id', $classSectionId)
->orWhere('class_section_id', (int)$classSectionId);
->where(function ($q) use ($classSectionId, $classSectionPk) {
$q->where('class_section_id', $classSectionId);
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
$q->orWhere('class_section_id', $classSectionPk);
}
})
->orderByDesc('date')
->limit(3)
@@ -225,7 +234,10 @@ class AttendanceQueryService
$snapshot = [];
$rowsByDate = [];
foreach ($rows as $entry) {
$d = (string)($entry['date'] ?? '');
$d = $this->calendarDateKey($entry['date'] ?? null);
if ($d === '') {
continue;
}
$snapshot[$d] = $entry['status'] ?? null;
$rowsByDate[$d] = $entry;
}
@@ -237,6 +249,23 @@ class AttendanceQueryService
: ($date === $currentSunday ? null : 'N/A');
}
// Recent list uses the same section filters but no Sunday window; if the windowed query
// missed rows (SQL/driver quirks), still show statuses that match the visible columns.
foreach ($recentRows as $row) {
$d = $this->calendarDateKey($row['date'] ?? null);
if ($d === '' || !in_array($d, $sundayDates, true)) {
continue;
}
$rowStatus = $row['status'] ?? null;
if ($rowStatus === null || $rowStatus === '') {
continue;
}
$current = $statusHistory[$d] ?? null;
if ($current === 'N/A' || $current === null) {
$statusHistory[$d] = $rowStatus;
}
}
$todayRow = $rowsByDate[$currentSunday] ?? null;
$lockInfo = $this->attendanceService->detectExternalSubmission($todayRow);
$todayReason = $todayRow['reason'] ?? null;
@@ -259,7 +288,56 @@ class AttendanceQueryService
];
}
$assigned = $this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$candidates = array_values(array_unique(array_filter(
[$classSectionCode, $classSectionPk],
static fn (int $v): bool => $v > 0,
)));
$alsoSectionId = null;
foreach ($candidates as $cid) {
if ($cid !== $classSectionId) {
$alsoSectionId = $cid;
break;
}
}
$assigned = $this->teacherClass->assignedForSectionTerm(
$classSectionId,
$semester,
$schoolYear,
$alsoSectionId,
true,
);
if ($assigned === []) {
$assigned = $this->teacherClass->assignedForSectionTerm(
$classSectionId,
$semester,
$schoolYear,
$alsoSectionId,
false,
);
}
if ($assigned === []) {
$assigned = $this->teacherRosterFromStaffAttendance(
$sundayDates,
$semester,
$schoolYear,
$classSectionId,
$classSectionPk,
);
}
if ($assigned === [] && $userId > 0) {
$self = $this->user->query()->select(['id', 'firstname', 'lastname'])->find($userId);
if ($self) {
$assigned = [[
'class_section_id' => $classSectionId,
'teacher_id' => (int) $self->id,
'position' => 'main',
'firstname' => $self->firstname,
'lastname' => $self->lastname,
]];
}
}
$teacherRows = [];
if (!empty($assigned)) {
@@ -267,16 +345,34 @@ class AttendanceQueryService
$statusMap = [];
if (!empty($teacherIds)) {
$rows = DB::table('staff_attendance as sa')
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
$staffQ = DB::table('staff_attendance as sa')
->select('sa.user_id', 'sa.date', 'sa.status', 'sa.reason')
->where('sa.semester', $semester)
->where('sa.school_year', $schoolYear)
->whereIn('sa.user_id', $teacherIds)
->whereIn('sa.date', $sundayDates)
->get();
->whereRaw($staffDateInSql, $sundayDates);
if (Schema::hasColumn('staff_attendance', 'class_section_id')) {
$staffQ->where(function ($w) use ($classSectionId, $classSectionPk) {
$w->whereNull('sa.class_section_id')
->orWhere('sa.class_section_id', 0)
->orWhere('sa.class_section_id', $classSectionId);
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
$w->orWhere('sa.class_section_id', $classSectionPk);
}
});
}
$rows = $staffQ->get();
foreach ($rows as $row) {
$statusMap[(int)$row->user_id][(string)$row->date] = [
$dk = $this->calendarDateKey($row->date ?? null);
if ($dk === '') {
continue;
}
$statusMap[(int)$row->user_id][$dk] = [
'status' => $row->status,
'reason' => $row->reason,
];
@@ -296,27 +392,39 @@ class AttendanceQueryService
: ($date === $currentSunday ? null : 'N/A');
}
$todayReason = null;
if (isset($statusMap[$teacherId][$currentSunday]) && is_array($statusMap[$teacherId][$currentSunday])) {
$todayReason = $statusMap[$teacherId][$currentSunday]['reason'] ?? null;
}
$teacherRows[] = [
'teacher_id' => $teacherId,
'position' => $position,
'name' => $name !== '' ? $name : 'User#' . $teacherId,
'sunday_statuses' => $history,
'today' => $history[$currentSunday] ?? null,
'today_reason' => $todayReason,
];
}
}
$noSchoolDays = [];
$calendarTable = $this->calendar->getTable();
$calDateInSql = 'DATE(`' . $calendarTable . '`.`date`) IN (' .
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
$calendarRows = $this->calendar->query()
->where('no_school', 1)
->whereIn('date', $sundayDates)
->whereRaw($calDateInSql, $sundayDates)
->where('semester', $semester)
->where('school_year', $schoolYear)
->get()
->toArray();
foreach ($calendarRows as $row) {
$d = (string)($row['date'] ?? '');
$d = $this->calendarDateKey($row['date'] ?? null);
if ($d === '') {
continue;
}
$noSchoolDays[$d] = [
'title' => $row['title'] ?? 'No School',
'description' => $row['description'] ?? '',
@@ -601,4 +709,171 @@ class AttendanceQueryService
'currentAdminName' => $adminName,
];
}
/**
* Distinct user_ids with staff rows on the given Sundays, scoped by teacher_class section assignment
* (works when staff_attendance has no class_section_id column).
*
* @return list<int>
*/
private function distinctStaffAttendeeIdsViaTeacherClassJoin(
array $sundayDates,
string $semester,
string $schoolYear,
int $classSectionId,
int $classSectionPk,
): array {
$sectionIds = array_values(array_unique(array_filter(
[$classSectionId, $classSectionPk],
static fn (int $v): bool => $v > 0,
)));
if ($sundayDates === [] || $sectionIds === []) {
return [];
}
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
$run = static function (
bool $requireTcSemester,
bool $requireSaSemester,
) use ($sundayDates, $schoolYear, $semester, $staffDateInSql, $sectionIds): array {
$q = DB::table('staff_attendance as sa')
->join('teacher_class as tc', function ($j) use ($sectionIds, $schoolYear, $requireTcSemester, $semester) {
$j->on('tc.teacher_id', '=', 'sa.user_id')
->whereIn('tc.class_section_id', $sectionIds)
->where('tc.school_year', '=', $schoolYear);
if ($requireTcSemester && trim($semester) !== '') {
$j->whereRaw('TRIM(tc.semester) = ?', [trim($semester)]);
}
})
->where('sa.school_year', $schoolYear)
->whereRaw($staffDateInSql, $sundayDates);
if ($requireSaSemester && trim($semester) !== '') {
$q->whereRaw('TRIM(sa.semester) = ?', [trim($semester)]);
}
return $q->distinct()
->pluck('sa.user_id')
->map(static fn ($v): int => (int) $v)
->filter(static fn (int $v): bool => $v > 0)
->values()
->all();
};
$ids = $run(true, true);
if ($ids === []) {
$ids = $run(false, true);
}
if ($ids === []) {
$ids = $run(false, false);
}
return $ids;
}
/**
* When teacher_class has no rows, build a roster from staff_attendance for this section/window.
*
* @return array<int, array{class_section_id:int, teacher_id:int, position:string, firstname:?string, lastname:?string}>
*/
private function teacherRosterFromStaffAttendance(
array $sundayDates,
string $semester,
string $schoolYear,
int $classSectionId,
int $classSectionPk,
): array {
if ($sundayDates === []) {
return [];
}
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
$ids = [];
if (Schema::hasColumn('staff_attendance', 'class_section_id')) {
$applySectionScope = function ($query) use ($classSectionId, $classSectionPk): void {
$query->where(function ($w) use ($classSectionId, $classSectionPk) {
$w->whereNull('sa.class_section_id')
->orWhere('sa.class_section_id', 0)
->orWhere('sa.class_section_id', $classSectionId);
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
$w->orWhere('sa.class_section_id', $classSectionPk);
}
});
};
$base = function (bool $useSemester) use ($sundayDates, $schoolYear, $semester, $staffDateInSql, $applySectionScope) {
$q = DB::table('staff_attendance as sa')
->where('sa.school_year', $schoolYear)
->whereRaw($staffDateInSql, $sundayDates);
if ($useSemester && trim($semester) !== '') {
$q->whereRaw('TRIM(sa.semester) = ?', [trim($semester)]);
}
$applySectionScope($q);
return $q->distinct()->pluck('sa.user_id')->map(static fn ($v) => (int) $v)->filter(static fn (int $v) => $v > 0)->values()->all();
};
$ids = $base(true);
if ($ids === []) {
$ids = $base(false);
}
}
if ($ids === []) {
$ids = $this->distinctStaffAttendeeIdsViaTeacherClassJoin(
$sundayDates,
$semester,
$schoolYear,
$classSectionId,
$classSectionPk,
);
}
if ($ids === []) {
return [];
}
$users = $this->user->query()
->select(['id', 'firstname', 'lastname'])
->whereIn('id', $ids)
->orderBy('firstname')
->orderBy('lastname')
->get();
$out = [];
foreach ($users as $u) {
$out[] = [
'class_section_id' => $classSectionId,
'teacher_id' => (int) $u->id,
'position' => 'main',
'firstname' => $u->firstname,
'lastname' => $u->lastname,
];
}
return $out;
}
/**
* Normalize DB datetimes / date strings to Y-m-d so they match {@see $sundayDates} keys.
*/
private function calendarDateKey(mixed $value): string
{
$s = trim((string)($value ?? ''));
if ($s === '') {
return '';
}
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
return $m[1];
}
try {
return Carbon::parse($s)->toDateString();
} catch (\Throwable) {
return '';
}
}
}
@@ -53,6 +53,8 @@ class TeacherAttendanceSubmissionService
}
$classSectionId = (int)$sectionRow->class_section_id;
$classSectionPk = (int)($sectionRow->id ?? 0);
$alsoSectionId = ($classSectionPk > 0 && $classSectionPk !== $classSectionId) ? $classSectionPk : null;
$classId = (int)$sectionRow->class_id;
$attendanceData = $data['attendance'] ?? [];
$teachersData = $data['teachers'] ?? [];
@@ -108,7 +110,22 @@ class TeacherAttendanceSubmissionService
}
$allowedStatuses = ['present', 'absent', 'late'];
$assignedTeachers = $this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$assignedTeachers = $this->teacherClass->assignedForSectionTerm(
$classSectionId,
$semester,
$schoolYear,
$alsoSectionId,
true,
);
if ($assignedTeachers === []) {
$assignedTeachers = $this->teacherClass->assignedForSectionTerm(
$classSectionId,
$semester,
$schoolYear,
$alsoSectionId,
false,
);
}
foreach ($assignedTeachers as $teacher) {
$teacherId = (int)$teacher['teacher_id'];
@@ -116,12 +116,13 @@ class ApiLoginSecurityService
/**
* Top-level JSON body aligned with the CodeIgniter 4 `AuthController::apiLogin` success payload.
*
* @return array{status: true, token: string, user: array{id: int, name: string, roles: object}}
* @return array{status: true, token: string, user: array{id: int, name: string, roles: object, class_section_id: ?int, class_section_name: ?string}}
*/
public function buildLoginResponse(User $user, string $jwtToken): array
{
$roleNames = $user->roleNamesLikeCodeIgniter();
$fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? ''));
$teacherContext = $user->teacherSessionContext();
return [
'status' => true,
@@ -130,6 +131,8 @@ class ApiLoginSecurityService
'id' => (int) $user->id,
'name' => $fullName,
'roles' => $this->rolesToObjectMap($roleNames),
'class_section_id' => $teacherContext['class_section_id'],
'class_section_name' => $teacherContext['class_section_name'],
],
];
}
+3
View File
@@ -124,6 +124,7 @@ class AuthSessionService
$schoolYear = Configuration::getConfig('school_year');
$semester = Configuration::getConfig('semester');
$teacherContext = $user->teacherSessionContext();
session([
'user_id' => $user->id,
@@ -136,6 +137,8 @@ class AuthSessionService
'roles' => $roleNames,
'semester' => $semester,
'school_year' => $schoolYear,
'class_section_id' => $teacherContext['class_section_id'],
'class_section_name' => $teacherContext['class_section_name'],
]);
$this->applyStylePreferencesToSession((int) $user->id);
@@ -14,15 +14,26 @@ class ClassProgressQueryService
public function listReports(User $user, array $filters): LengthAwarePaginator
{
$query = ClassProgressReport::query()
->with('classSection')
->with(['classSection', 'teacher'])
->orderBy($filters['sort_by'] ?? 'week_start', $filters['sort_dir'] ?? 'desc');
if (!$this->isAdmin($user)) {
$relaxedSectionIds = TeacherClass::distinctSectionIdsForTeacher((int) $user->id);
if (! empty($filters['class_section_id'])) {
$ids = $this->resolveAssignedTeacherIds((int) $filters['class_section_id']);
$query->whereIn('teacher_id', $ids !== [] ? $ids : [(int) $user->id]);
$sid = (int) $filters['class_section_id'];
// Section filter: if this user has ever taught the section, show all rows for that section
// (legacy DB often has `class_progress_reports` without matching current-year `teacher_class`).
if (! in_array($sid, $relaxedSectionIds, true)) {
$query->where('teacher_id', (int) $user->id);
}
} else {
$query->where('teacher_id', $user->id);
$query->where(function ($q) use ($user, $relaxedSectionIds) {
$q->where('teacher_id', (int) $user->id);
if ($relaxedSectionIds !== []) {
$q->orWhereIn('class_section_id', $relaxedSectionIds);
}
});
}
} elseif (! empty($filters['teacher_id'])) {
$query->where('teacher_id', (int) $filters['teacher_id']);
@@ -76,10 +87,7 @@ class ClassProgressQueryService
->whereDate('week_start', $report->week_start)
->orderBy('subject', 'asc');
if (! $this->isAdmin($user)) {
$ids = $this->resolveAssignedTeacherIds((int) $report->class_section_id);
$query->whereIn('teacher_id', $ids !== [] ? $ids : [(int) $user->id]);
}
// Non-admins reach this only after policy `view` passed on the anchor report (author or teaches section).
$rows = $query->get();
$attachments = $this->attachmentMap($rows->pluck('id')->all());
@@ -160,6 +168,9 @@ class ClassProgressQueryService
$semester = (string) (Configuration::getConfig('semester') ?? '');
$rows = TeacherClass::assignedForSectionTerm($classSectionId, $semester, $schoolYear);
if ($rows === []) {
$rows = TeacherClass::assignedForSectionTerm($classSectionId, $semester, $schoolYear, null, false);
}
$ids = [];
foreach ($rows as $row) {
$id = (int) ($row['teacher_id'] ?? 0);
@@ -177,8 +188,10 @@ class ClassProgressQueryService
return true;
}
$ids = $this->resolveAssignedTeacherIds((int) $report->class_section_id);
return $ids !== [] && in_array((int) $user->id, $ids, true);
return in_array(
(int) $report->class_section_id,
TeacherClass::distinctSectionIdsForTeacher((int) $user->id),
true
);
}
}
+8 -2
View File
@@ -13,7 +13,10 @@ class RoleQueryService
{
$query = Role::query()
->select('roles.*')
->leftJoin('user_roles', 'user_roles.role_id', '=', 'roles.id')
->leftJoin('user_roles', function ($join) {
$join->on('user_roles.role_id', '=', 'roles.id')
->whereNull('user_roles.deleted_at');
})
->selectRaw('COUNT(DISTINCT user_roles.user_id) AS user_count')
->groupBy('roles.id');
@@ -42,7 +45,10 @@ class RoleQueryService
public function listUsersWithRoles(array $filters): LengthAwarePaginator
{
$query = DB::table('users')
->leftJoin('user_roles', 'user_roles.user_id', '=', 'users.id')
->leftJoin('user_roles', function ($join) {
$join->on('user_roles.user_id', '=', 'users.id')
->whereNull('user_roles.deleted_at');
})
->leftJoin('roles', 'roles.id', '=', 'user_roles.role_id')
->select(
'users.id',
+30 -12
View File
@@ -11,17 +11,14 @@ class RoleStaffService
public function syncStaffRecord(User $user, array $newRoleNames): void
{
$excludedRoles = ['parent', 'student', 'guest'];
$loweredNewRoles = array_map('strtolower', $newRoleNames);
$existingStaff = Staff::query()->where('user_id', $user->id)->first();
$existingRoles = [];
if ($existingStaff && !empty($existingStaff->role_name)) {
$existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff->role_name)));
}
$allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles));
$staffRoles = array_filter($newRoleNames, fn ($role) => !in_array(strtolower($role), $excludedRoles, true));
$activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive';
$staffRoles = array_values(array_unique(array_filter(
array_map(
static fn ($role) => trim((string) $role),
$newRoleNames
),
static fn ($role) => $role !== '' && !in_array(strtolower($role), $excludedRoles, true)
)));
$activeRole = !empty($staffRoles) ? strtolower((string) $staffRoles[0]) : 'inactive';
$email = $this->generateUniqueStaffEmail($user->firstname ?? '', $user->lastname ?? '', (int) $user->id);
@@ -31,7 +28,7 @@ class RoleStaffService
'lastname' => $user->lastname ?? '',
'email' => $email,
'phone' => $user->cellphone ?? '',
'role_name' => implode(', ', $allRoles),
'role_name' => $this->compactRoleList($staffRoles),
'active_role' => $activeRole,
'school_year' => $user->school_year ?? '',
];
@@ -42,6 +39,27 @@ class RoleStaffService
}
}
/**
* Fit the current staff role list into the legacy `staff.role_name` field.
*/
private function compactRoleList(array $roles, int $maxLength = 100): string
{
if ($roles === []) {
return 'inactive';
}
$out = '';
foreach ($roles as $role) {
$next = $out === '' ? $role : $out . ', ' . $role;
if (strlen($next) > $maxLength) {
break;
}
$out = $next;
}
return $out !== '' ? $out : substr((string) $roles[0], 0, $maxLength);
}
private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string
{
$domain = '@alrahmaisgl.org';
@@ -3,11 +3,16 @@
namespace App\Services\Settings\SchoolCalendar;
use App\Models\CalendarEvent;
use Carbon\Carbon;
use Carbon\CarbonInterface;
class SchoolCalendarFormatterService
{
public function formatEvent(CalendarEvent|array $event, ?string $audience = null): array
{
$eventDate = $event instanceof CalendarEvent
? $this->extractDateValue($event)
: $this->normalizeDateValue($event['date'] ?? null);
$event = $event instanceof CalendarEvent ? $event->toArray() : $event;
$audience = $audience !== null ? strtolower(trim($audience)) : null;
@@ -36,8 +41,9 @@ class SchoolCalendarFormatterService
return [
'id' => (int) ($event['id'] ?? 0),
'title' => $title,
'start' => (string) ($event['date'] ?? ''),
'start' => $eventDate,
'description' => (string) ($event['description'] ?? ''),
'backgroundColor' => $backgroundColor,
'extendedProps' => [
'semester' => (string) ($event['semester'] ?? ''),
'school_year' => (string) ($event['school_year'] ?? ''),
@@ -51,6 +57,43 @@ class SchoolCalendarFormatterService
];
}
private function extractDateValue(CalendarEvent $event): string
{
$raw = $event->getRawOriginal('date');
if (is_string($raw) && trim($raw) !== '') {
return $this->normalizeDateValue($raw);
}
$value = $event->getAttribute('date');
if ($value instanceof CarbonInterface) {
return $value->toDateString();
}
return $this->normalizeDateValue($value);
}
private function normalizeDateValue(mixed $value): string
{
if ($value instanceof CarbonInterface) {
return $value->toDateString();
}
$value = trim((string) $value);
if ($value === '') {
return '';
}
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $value, $matches) === 1) {
return $matches[0];
}
try {
return Carbon::parse($value)->toDateString();
} catch (\Throwable) {
return $value;
}
}
public function formatMeetingEvent(array $row, ?string $audience = null): array
{
$audience = $audience !== null ? strtolower(trim($audience)) : null;
@@ -83,6 +126,7 @@ class SchoolCalendarFormatterService
'title' => $title,
'start' => $start,
'description' => implode("\n", $descParts),
'backgroundColor' => '#6f42c1',
'extendedProps' => [
'semester' => (string) ($row['semester'] ?? ''),
'school_year' => (string) ($row['school_year'] ?? ''),
@@ -2,11 +2,143 @@
namespace App\Services\Students;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class StudentScoreCardService
{
/**
* Students shown on the score-card picker (CI `StudentController::scoreCardList` parity).
* Teachers: only students in `class_section_id` for `school_year` after `teacher_class` check.
* Parents: their children only. Other roles: active students (optional search/limit).
*
* @return list<array{id: int, student_id: int, school_id: string, firstname: string, lastname: string, name: string, is_active: int}>
*/
public function scoreCardSelectableStudents(
User $user,
string $searchQuery,
int $limit,
?int $classSectionId,
?string $schoolYear,
): array {
$rolesLower = array_map('strtolower', $user->roleNamesLikeCodeIgniter());
$isTeacher = in_array('teacher', $rolesLower, true) || in_array('teacher_assistant', $rolesLower, true);
$isParent = in_array('parent', $rolesLower, true);
$cid = (int) ($classSectionId ?? 0);
$year = $schoolYear !== null && trim($schoolYear) !== '' ? trim($schoolYear) : (string) (Configuration::getConfig('school_year') ?? '');
if ($isTeacher && $cid > 0) {
$assigned = TeacherClass::query()
->where('teacher_id', $user->id)
->where('class_section_id', $cid)
->when($year !== '', fn ($q) => $q->where('school_year', $year))
->exists();
if (! $assigned) {
return [];
}
$students = $year !== ''
? Student::getByClassAndYear($cid, $year)
: Student::query()
->whereIn('id', StudentClass::query()->select('student_id')->where('class_section_id', $cid))
->where('is_active', 1)
->orderBy('lastname')
->orderBy('firstname')
->get()
->all();
$rows = array_map(fn (Student $s) => $this->selectableStudentPayload($s), $students);
return $this->applySearchAndLimit($rows, $searchQuery, $limit);
}
if ($isTeacher && ! $isParent) {
return [];
}
if ($isParent) {
$q = Student::query()
->select('id', 'school_id', 'firstname', 'lastname', 'is_active')
->where('parent_id', $user->id)
->where('is_active', 1);
if ($searchQuery !== '') {
$sq = $searchQuery;
$q->where(function ($b) use ($sq) {
$b->where('firstname', 'like', '%'.$sq.'%')
->orWhere('lastname', 'like', '%'.$sq.'%')
->orWhere('school_id', 'like', '%'.$sq.'%');
});
}
$students = $q->orderBy('lastname')->orderBy('firstname')->limit($limit)->get();
return $students->map(fn (Student $s) => $this->selectableStudentPayload($s))->values()->all();
}
$studentsQuery = Student::query()
->select('id', 'school_id', 'firstname', 'lastname', 'is_active')
->where('is_active', 1);
if ($searchQuery !== '') {
$studentsQuery->where(function ($builder) use ($searchQuery) {
$builder->where('firstname', 'like', '%'.$searchQuery.'%')
->orWhere('lastname', 'like', '%'.$searchQuery.'%')
->orWhere('school_id', 'like', '%'.$searchQuery.'%');
});
}
return $studentsQuery
->orderBy('lastname')
->orderBy('firstname')
->limit($limit)
->get()
->map(fn (Student $student) => $this->selectableStudentPayload($student))
->values()
->all();
}
/**
* @param list<array{id: int, student_id: int, school_id: string, firstname: string, lastname: string, name: string, is_active: int}> $rows
* @return list<array{id: int, student_id: int, school_id: string, firstname: string, lastname: string, name: string, is_active: int}>
*/
private function applySearchAndLimit(array $rows, string $searchQuery, int $limit): array
{
if ($searchQuery === '') {
return array_slice($rows, 0, $limit);
}
$needle = strtolower($searchQuery);
$filtered = array_values(array_filter($rows, static function (array $r) use ($needle): bool {
$hay = strtolower(
($r['firstname'] ?? '').' '.($r['lastname'] ?? '').' '.($r['school_id'] ?? '')
);
return str_contains($hay, $needle);
}));
return array_slice($filtered, 0, $limit);
}
private function selectableStudentPayload(Student $student): array
{
return [
'id' => (int) $student->id,
'student_id' => (int) $student->id,
'school_id' => (string) ($student->school_id ?? ''),
'firstname' => (string) ($student->firstname ?? ''),
'lastname' => (string) ($student->lastname ?? ''),
'name' => trim(((string) ($student->firstname ?? '')).' '.((string) ($student->lastname ?? ''))),
'is_active' => (int) ($student->is_active ?? 0),
];
}
public function scoreCard(int $studentId): array
{
$student = Student::query()
@@ -41,6 +41,7 @@ class TeacherDashboardService
'students' => [],
'studentsBySection' => [],
'active_class_section_id' => null,
'class_section_name' => null,
'assignedNames' => [],
];
}
@@ -118,12 +119,24 @@ class TeacherDashboardService
$teacher = User::query()->find($userId)?->toArray();
$classSectionName = null;
if ($activeClassSectionId !== null && (int) $activeClassSectionId > 0) {
foreach ($classes as $row) {
if ((int) ($row['class_section_id'] ?? 0) === (int) $activeClassSectionId) {
$n = trim((string) ($row['class_section_name'] ?? ''));
$classSectionName = $n !== '' ? $n : null;
break;
}
}
}
return [
'teacher' => $teacher,
'classes' => $classes,
'students' => $activeClassSectionId ? ($studentsBySection[$activeClassSectionId] ?? []) : [],
'studentsBySection' => $studentsBySection,
'active_class_section_id' => $activeClassSectionId,
'class_section_name' => $classSectionName,
'assignedNames' => $assignedNames,
];
}