add more controller

This commit is contained in:
root
2026-03-10 00:48:32 -04:00
parent 5eeaec0257
commit 20ee70d153
151 changed files with 9481 additions and 8407 deletions
@@ -0,0 +1,290 @@
<?php
namespace App\Services\Teachers;
use App\Libraries\StaffTimeOffLinkService;
use App\Models\StaffAttendance;
use App\Models\TeacherClass;
use App\Models\User;
use App\Services\SemesterRangeService;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
class TeacherAbsenceService
{
public function __construct(
private TeacherConfigService $configService,
private SemesterRangeService $semesterRangeService,
private StaffTimeOffLinkService $staffTimeOffLinkService
) {
}
public function formData(int $userId): array
{
$context = $this->configService->context();
$semester = (string) ($context['semester'] ?? '');
$schoolYear = (string) ($context['school_year'] ?? '');
$teacher = User::query()->find($userId);
$displayName = $teacher
? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? ''))
: 'Teacher';
$existing = StaffAttendance::query()
->where('user_id', $userId)
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear))
->orderByDesc('date')
->get()
->toArray();
return [
'teacher_name' => $displayName,
'semester' => $semester,
'school_year' => $schoolYear,
'existing' => $existing,
'available_dates' => $this->allowedAbsenceDates($schoolYear),
];
}
public function submit(int $userId, array $payload): array
{
$context = $this->configService->context();
$semester = (string) ($context['semester'] ?? '');
$schoolYear = (string) ($context['school_year'] ?? '');
if ($schoolYear === '') {
return [
'ok' => false,
'message' => 'Semester or school year not configured.',
'status' => 422,
];
}
$dates = (array) ($payload['dates'] ?? []);
$reasonType = trim((string) ($payload['reason_type'] ?? ''));
$reasonText = trim((string) ($payload['reason'] ?? ''));
if ($reasonText === '') {
return [
'ok' => false,
'message' => 'Reason is required.',
'status' => 422,
];
}
$reasonBase = $reasonType !== '' ? strtolower($reasonType) : '';
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
$allowedSet = array_fill_keys($this->allowedAbsenceDates($schoolYear), true);
$roleName = User::getUserRoleName($userId) ?: null;
$saved = 0;
$invalid = [];
$savedDates = [];
$dates = array_values(array_unique(array_map('strval', $dates)));
foreach ($dates as $date) {
$date = trim($date);
if ($date === '') {
continue;
}
try {
$parsed = Carbon::createFromFormat('Y-m-d', $date);
if ($parsed->format('Y-m-d') !== $date || empty($allowedSet[$date])) {
$invalid[] = $date;
continue;
}
} catch (\Throwable) {
$invalid[] = $date;
continue;
}
$semesterForDate = $this->semesterRangeService->getSemesterForDate($date);
if ($semesterForDate === '') {
$semesterForDate = $semester;
}
$record = StaffAttendance::upsertOne(
userId: $userId,
roleName: $roleName,
date: $date,
semester: $semesterForDate,
schoolYear: $schoolYear,
status: StaffAttendance::STATUS_ABSENT,
reason: $reason,
editorId: $userId
);
if ($record) {
$saved++;
$savedDates[] = $date;
}
}
if (!empty($invalid)) {
return [
'ok' => false,
'message' => 'Invalid dates: ' . implode(', ', $invalid),
'status' => 422,
];
}
$this->sendPrincipalNotification(
userId: $userId,
roleName: $roleName,
semester: $semester,
schoolYear: $schoolYear,
reasonType: $reasonType,
reasonText: $reasonText,
dates: $dates,
savedDates: $savedDates
);
return [
'ok' => true,
'message' => $saved . ' day(s) saved as absent.',
'saved' => $saved,
'dates' => $savedDates,
'status' => 200,
];
}
private function allowedAbsenceDates(string $schoolYear): array
{
$today = Carbon::today();
$startYear = null;
$endYear = null;
if (preg_match('/^(\d{4})\D+(\d{4})$/', $schoolYear, $m)) {
$startYear = (int) $m[1];
$endYear = (int) $m[2];
} else {
$cy = (int) now()->format('Y');
$cm = (int) now()->format('n');
if ($cm >= 9) {
$startYear = $cy;
$endYear = $cy + 1;
} else {
$startYear = $cy - 1;
$endYear = $cy;
}
}
try {
$start = Carbon::create($startYear, 9, 1)->startOfDay();
$end = Carbon::create($endYear, 5, 31)->startOfDay();
} catch (\Throwable) {
return [];
}
if ($start->lt($today)) {
$start = $today->copy();
}
$dates = [];
$cursor = $start->copy();
while ($cursor->lte($end)) {
if ($cursor->dayOfWeek === Carbon::SUNDAY) {
$dates[] = $cursor->format('Y-m-d');
}
$cursor->addDay();
}
return $dates;
}
private function sendPrincipalNotification(
int $userId,
?string $roleName,
string $semester,
string $schoolYear,
string $reasonType,
string $reasonText,
array $dates,
array $savedDates
): void {
try {
$user = User::query()->find($userId);
$fullName = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) ?: 'Teacher';
$userEmail = $user->email ?? '';
$role = $roleName ?: 'teacher';
$dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
$subject = sprintf(
'TimeOff Request: %s (%s) — %s',
$fullName,
ucfirst((string) $role),
$dateList ?: 'No dates'
);
$submittedAt = now()->toDateTimeString();
$assignedText = $this->formatAssignedClasses($userId, $schoolYear, $semester);
$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 teacher portal.</p>'
. '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
. '<tr><td><strong>Name</strong></td><td>' . e($fullName) . '</td></tr>'
. '<tr><td><strong>Email</strong></td><td>' . e($userEmail) . '</td></tr>'
. '<tr><td><strong>Role</strong></td><td>' . e((string) $role) . '</td></tr>'
. '<tr><td><strong>Semester</strong></td><td>' . e($semester) . '</td></tr>'
. '<tr><td><strong>School Year</strong></td><td>' . e($schoolYear) . '</td></tr>'
. '<tr><td><strong>Reason Type</strong></td><td>' . e($reasonType ?: '-') . '</td></tr>'
. '<tr><td><strong>Reason</strong></td><td>' . e($reasonText) . '</td></tr>'
. '<tr><td><strong>Dates</strong></td><td>' . e($dateList ?: '-') . '</td></tr>'
. '<tr><td><strong>Assigned Class(es)</strong></td><td>' . e($assignedText) . '</td></tr>'
. '<tr><td><strong>Submitted At</strong></td><td>' . e($submittedAt) . '</td></tr>'
. '</table>'
. '</div>';
$token = $this->staffTimeOffLinkService->createToken([
'uid' => $userId,
'email' => $userEmail,
'name' => $fullName,
'role' => $role,
'dates' => $dateList ?: '-',
'reason' => $reasonText,
'reason_type' => $reasonType,
'submitted_at' => $submittedAt,
'origin' => 'teacher portal',
]);
$notifyUrl = url('/timeoff/notify/' . rawurlencode($token));
$body .= '<p style="margin-top:16px;">Click <a href="' . e($notifyUrl) . '">Send confirmation email to '
. e($fullName)
. '</a> so the staff member is notified automatically. This link expires in 14 days.</p>';
$principalEmail = env('PRINCIPAL_EMAIL', 'principal@alrahmaisgl.org');
Mail::html($body, function ($message) use ($principalEmail, $subject) {
$message->to($principalEmail)
->subject($subject);
});
} catch (\Throwable) {
// Email failures should not block absence submissions.
}
}
private function formatAssignedClasses(int $userId, string $schoolYear, string $semester): string
{
$assignments = TeacherClass::getClassAssignmentsByUserId($userId, $schoolYear, $semester);
if (empty($assignments)) {
return 'No assigned class';
}
$parts = [];
foreach ($assignments as $assignment) {
$name = (string) ($assignment['class_section_name'] ?? '');
$role = (string) ($assignment['teacher_role'] ?? '');
if ($name !== '') {
$parts[] = $role !== '' ? ($name . ' (' . $role . ')') : $name;
}
}
return !empty($parts) ? implode(', ', $parts) : 'No assigned class';
}
}
@@ -0,0 +1,186 @@
<?php
namespace App\Services\Teachers;
use App\Models\ClassSection;
use App\Models\TeacherClass;
use App\Models\User;
class TeacherAssignmentService
{
public function __construct(private TeacherConfigService $configService)
{
}
public function listAssignments(?string $schoolYear = null): array
{
$context = $this->configService->context();
$schoolYear = $schoolYear ?: (string) ($context['school_year'] ?? '');
$teachers = User::getTeachersAndTAs();
$classSections = ClassSection::query()
->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear))
->orderBy('class_section_name')
->get()
->toArray();
if (empty($classSections)) {
$classSections = ClassSection::query()->orderBy('class_section_name')->get()->toArray();
}
$classNames = [];
foreach ($classSections as $section) {
$sectionId = (int) ($section['class_section_id'] ?? $section['id'] ?? 0);
if ($sectionId <= 0) {
continue;
}
$classNames[$sectionId] = $section['class_section_name'] ?? 'N/A';
}
$assignments = TeacherClass::query()
->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear))
->get()
->toArray();
$assignmentMap = [];
foreach ($assignments as $assign) {
$teacherId = (int) ($assign['teacher_id'] ?? 0);
$sectionId = (int) ($assign['class_section_id'] ?? 0);
if ($teacherId <= 0 || $sectionId <= 0) {
continue;
}
$role = strtolower((string) ($assign['position'] ?? ''));
if (!in_array($role, ['main', 'ta'], true)) {
continue;
}
$assignmentMap[$teacherId][$role][] = [
'section_id' => $sectionId,
'class_section_name' => $classNames[$sectionId] ?? 'N/A',
'role' => $role,
'semester' => (string) ($assign['semester'] ?? ''),
'school_year' => (string) ($assign['school_year'] ?? $schoolYear),
];
}
$teacherData = [];
foreach ($teachers as $teacher) {
$tid = (int) ($teacher['id'] ?? 0);
if ($tid <= 0) {
continue;
}
$assigned = $assignmentMap[$tid] ?? ['main' => [], 'ta' => []];
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
if ($name === '') {
$name = 'User#' . $tid;
}
$teacherData[] = [
'teacher_id' => $tid,
'firstname' => $teacher['firstname'] ?? '',
'lastname' => $teacher['lastname'] ?? '',
'name' => $name,
'email' => $teacher['email'] ?? '',
'cellphone' => $teacher['cellphone'] ?? '',
'role' => $teacher['role'] ?? '',
'school_year' => $schoolYear,
'main_assignments' => $assigned['main'] ?? [],
'ta_assignments' => $assigned['ta'] ?? [],
];
}
$classesPayload = [];
foreach ($classSections as $section) {
$sectionId = (int) ($section['class_section_id'] ?? $section['id'] ?? 0);
if ($sectionId <= 0) {
continue;
}
$classesPayload[] = [
'class_section_id' => $sectionId,
'class_section_name' => $section['class_section_name'] ?? 'N/A',
];
}
return [
'school_year' => $schoolYear,
'teachers' => $teacherData,
'classes' => $classesPayload,
];
}
public function assign(array $input): array
{
$context = $this->configService->context();
$schoolYear = (string) ($input['school_year'] ?? $context['school_year'] ?? '');
$teacherId = (int) ($input['teacher_id'] ?? 0);
$classSectionId = (int) ($input['class_section_id'] ?? 0);
$teacherRole = (string) ($input['teacher_role'] ?? '');
$loggedInUserId = (int) ($input['updated_by'] ?? 0);
if ($teacherId <= 0 || $classSectionId <= 0 || $schoolYear === '') {
return ['ok' => false, 'message' => 'Missing required parameters for assignment.'];
}
if ($teacherRole === '') {
$teacherRole = (string) (User::getUserRoleName($teacherId) ?? '');
}
$isAssistant = strtolower($teacherRole) === 'teacher_assistant';
$position = $isAssistant ? 'ta' : 'main';
$alreadyAssigned = TeacherClass::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('position', $position)
->where('school_year', $schoolYear)
->first();
if ($alreadyAssigned) {
return ['ok' => false, 'message' => 'This teacher is already assigned to this class with the same role.'];
}
TeacherClass::query()->create([
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'position' => $position,
'school_year' => $schoolYear,
'created_at' => now(),
'updated_at' => now(),
'updated_by' => $loggedInUserId ?: null,
]);
return [
'ok' => true,
'message' => 'Class assigned successfully.',
'position' => $position,
];
}
public function delete(array $input): array
{
$context = $this->configService->context();
$teacherId = (int) ($input['teacher_id'] ?? 0);
$classSectionId = (int) ($input['class_section_id'] ?? 0);
$position = strtolower((string) ($input['position'] ?? ''));
$schoolYear = trim((string) ($input['school_year'] ?? $context['school_year'] ?? ''));
if ($teacherId <= 0 || $classSectionId <= 0 || !in_array($position, ['main', 'ta'], true) || $schoolYear === '') {
return ['ok' => false, 'message' => 'Missing or invalid data for deletion.'];
}
$assignment = TeacherClass::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->where('position', $position)
->first();
if (!$assignment) {
return ['ok' => false, 'message' => 'Assignment not found.'];
}
$assignment->delete();
return ['ok' => true, 'message' => ucfirst($position) . ' assignment removed successfully.'];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Services\Teachers;
use App\Models\Configuration;
class TeacherConfigService
{
public function context(): array
{
return [
'school_year' => Configuration::getConfig('school_year'),
'semester' => Configuration::getConfig('semester'),
];
}
}
@@ -0,0 +1,130 @@
<?php
namespace App\Services\Teachers;
use App\Models\StudentAllergy;
use App\Models\StudentClass;
use App\Models\StudentMedicalCondition;
use App\Models\TeacherClass;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class TeacherDashboardService
{
public function __construct(private TeacherConfigService $configService)
{
}
public function classView(int $userId, ?int $requestedClassSectionId = null): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
$roles = DB::table('user_roles as ur')
->join('roles as r', 'r.id', '=', 'ur.role_id')
->where('ur.user_id', $userId)
->whereNull('ur.deleted_at')
->pluck('r.name')
->map(fn ($r) => strtolower((string) $r))
->all();
if (!array_intersect($roles, ['teacher', 'teacher_assistant'])) {
throw new \RuntimeException('Access denied.');
}
$classes = TeacherClass::getClassAssignmentsByUserId($userId, $schoolYear, $semester);
if (empty($classes)) {
return [
'teacher' => null,
'classes' => [],
'students' => [],
'studentsBySection' => [],
'active_class_section_id' => null,
'assignedNames' => [],
];
}
$classSectionIds = array_values(array_unique(array_map('intval', array_column($classes, 'class_section_id'))));
$activeClassSectionId = null;
if ($requestedClassSectionId && in_array($requestedClassSectionId, $classSectionIds, true)) {
$activeClassSectionId = $requestedClassSectionId;
} elseif (!empty($classSectionIds)) {
$activeClassSectionId = $classSectionIds[0];
}
$studentsBySection = [];
if (!empty($classSectionIds)) {
$students = StudentClass::getStudentsByClassSectionIds($classSectionIds);
if (!empty($students)) {
$studentIds = array_values(array_unique(array_map(
static fn (array $row) => (int) ($row['student_id'] ?? 0),
$students
)));
$studentIds = array_values(array_filter($studentIds));
$conditionsByStudent = !empty($studentIds)
? StudentMedicalCondition::getMedicalByStudentIds($studentIds)
: [];
$allergiesByStudent = !empty($studentIds)
? StudentAllergy::getAllergiesByStudentIds($studentIds)
: [];
foreach ($students as $student) {
$sid = (int) ($student['student_id'] ?? 0);
$cid = (int) ($student['class_section_id'] ?? 0);
$medList = array_column($conditionsByStudent[$sid] ?? [], 'condition_name');
$algList = array_column($allergiesByStudent[$sid] ?? [], 'allergy');
$student['medical_conditions'] = $medList;
$student['allergies'] = $algList;
$student['medical_conditions_text'] = implode(', ', $medList);
$student['allergies_text'] = implode(', ', $algList);
$studentsBySection[$cid][] = $student;
}
}
}
$assignedNames = [];
if (!empty($classSectionIds)) {
$rows = DB::table('teacher_class as tc')
->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname')
->join('users as u', 'u.id', '=', 'tc.teacher_id')
->whereIn('tc.class_section_id', $classSectionIds)
->where('tc.school_year', $schoolYear)
->orderBy('tc.class_section_id')
->orderByRaw("FIELD(tc.position,'main','ta')")
->orderBy('u.firstname')
->get();
foreach ($rows as $row) {
$sid = (int) ($row->class_section_id ?? 0);
$pos = strtolower((string) ($row->position ?? ''));
$name = trim(($row->firstname ?? '') . ' ' . ($row->lastname ?? ''));
if ($sid === 0 || $name === '') {
continue;
}
if (in_array($pos, ['main', 'teacher'], true)) {
$assignedNames[$sid]['mains'][] = $name;
} elseif (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) {
$assignedNames[$sid]['tas'][] = $name;
} else {
$assignedNames[$sid]['others'][] = $name;
}
}
}
$teacher = User::query()->find($userId)?->toArray();
return [
'teacher' => $teacher,
'classes' => $classes,
'students' => $activeClassSectionId ? ($studentsBySection[$activeClassSectionId] ?? []) : [],
'studentsBySection' => $studentsBySection,
'active_class_section_id' => $activeClassSectionId,
'assignedNames' => $assignedNames,
];
}
}