reconstruction of the project
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\AdminNotificationSubject;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminNotificationSubjectService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminNotificationUserService $userService
|
||||
) {
|
||||
}
|
||||
|
||||
public function subjectOptions(): array
|
||||
{
|
||||
return [
|
||||
'academics' => 'Academics',
|
||||
'attendance' => 'Attendance',
|
||||
'events' => 'Events',
|
||||
'finance' => 'Finance',
|
||||
'general' => 'General',
|
||||
'print_requests' => 'Print Requests',
|
||||
];
|
||||
}
|
||||
|
||||
public function alertsData(): array
|
||||
{
|
||||
$admins = $this->userService->fetchAdminNotificationUsers();
|
||||
$subjects = $this->subjectOptions();
|
||||
$assignedSubjects = [];
|
||||
$tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects');
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = AdminNotificationSubject::query()
|
||||
->select('id', 'admin_id', 'subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
$subject = (string) ($row->subject ?? '');
|
||||
|
||||
if ($adminId > 0 && $subject !== '') {
|
||||
$assignedSubjects[$adminId][$subject] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'subjects' => $subjects,
|
||||
'assignedSubjects' => $assignedSubjects,
|
||||
'tableReady' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
public function save(array $posted): array
|
||||
{
|
||||
if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$allowed = array_keys($this->subjectOptions());
|
||||
$admins = $this->userService->fetchAdminNotificationUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
if (empty($adminIds)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No admins found to update.',
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
$existing = AdminNotificationSubject::query()
|
||||
->select('id', 'admin_id', 'subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
$existingMap = [];
|
||||
foreach ($existing as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
$subject = (string) ($row->subject ?? '');
|
||||
if ($adminId > 0 && $subject !== '') {
|
||||
$existingMap[$adminId][$subject] = (int) ($row->id ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$updates = 0;
|
||||
|
||||
foreach ($adminIds as $adminId) {
|
||||
$subjectRaw = $posted[$adminId] ?? [];
|
||||
$selected = [];
|
||||
|
||||
if (is_array($subjectRaw)) {
|
||||
foreach ($subjectRaw as $key => $value) {
|
||||
$candidate = is_string($key) ? $key : $value;
|
||||
if (!is_string($candidate)) {
|
||||
continue;
|
||||
}
|
||||
$candidate = trim($candidate);
|
||||
if ($candidate !== '' && in_array($candidate, $allowed, true)) {
|
||||
$selected[] = $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selected = array_values(array_unique($selected));
|
||||
$current = array_keys($existingMap[$adminId] ?? []);
|
||||
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
AdminNotificationSubject::query()
|
||||
->where('admin_id', $adminId)
|
||||
->whereIn('subject', $toDelete)
|
||||
->delete();
|
||||
$updates += count($toDelete);
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $subject) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => $subject,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
AdminNotificationSubject::insert($batch);
|
||||
$updates += count($toInsert);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.',
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminNotificationUserService
|
||||
{
|
||||
public function excludedRoles(): array
|
||||
{
|
||||
return [
|
||||
'parent',
|
||||
'student',
|
||||
'guest',
|
||||
'teacher',
|
||||
'assistant teacher',
|
||||
'teacher assistant',
|
||||
'teacher_assistant',
|
||||
'assistant_teacher',
|
||||
'ta',
|
||||
'authorized_user',
|
||||
];
|
||||
}
|
||||
|
||||
public function fetchAdminNotificationUsers(): array
|
||||
{
|
||||
$excluded = $this->excludedRoles();
|
||||
|
||||
return DB::table('users as u')
|
||||
->select('u.id', 'u.firstname', 'u.lastname', 'u.email')
|
||||
->join('user_roles as ur', 'ur.user_id', '=', 'u.id')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->whereNotNull('r.name')
|
||||
->whereNull('ur.deleted_at')
|
||||
->whereNotIn(DB::raw('LOWER(r.name)'), array_map('strtolower', $excluded))
|
||||
->groupBy('u.id', 'u.firstname', 'u.lastname', 'u.email')
|
||||
->orderBy('u.lastname')
|
||||
->orderBy('u.firstname')
|
||||
->get()
|
||||
->map(fn($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\AdminNotificationSubject;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminPrintRecipientService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminNotificationUserService $userService
|
||||
) {
|
||||
}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
$admins = $this->userService->fetchAdminNotificationUsers();
|
||||
$tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects');
|
||||
$assigned = [];
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = AdminNotificationSubject::query()
|
||||
->select('admin_id')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
if ($adminId > 0) {
|
||||
$assigned[$adminId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'assigned' => $assigned,
|
||||
'tableReady' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
public function save(array $posted): array
|
||||
{
|
||||
if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$admins = $this->userService->fetchAdminNotificationUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
if (empty($adminIds)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No admins found to update.',
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
$selected = [];
|
||||
foreach ($posted as $key => $value) {
|
||||
$adminId = (int) $key;
|
||||
if ($adminId > 0 && in_array($adminId, $adminIds, true)) {
|
||||
$selected[] = $adminId;
|
||||
}
|
||||
}
|
||||
|
||||
$selected = array_values(array_unique($selected));
|
||||
|
||||
$current = AdminNotificationSubject::query()
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->pluck('admin_id')
|
||||
->map(fn($id) => (int) $id)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
AdminNotificationSubject::query()
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $toDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $adminId) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => 'print_requests',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
AdminNotificationSubject::insert($batch);
|
||||
}
|
||||
|
||||
$changes = count($toDelete) + count($toInsert);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.',
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Libraries\StaffTimeOffLinkService;
|
||||
use App\Models\StaffAttendance;
|
||||
use App\Models\User;
|
||||
use App\Services\SemesterRangeService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class AdministratorAbsenceService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected User $userModel,
|
||||
protected StaffAttendance $staffAttendanceModel,
|
||||
protected SemesterRangeService $semesterRangeService,
|
||||
protected StaffTimeOffLinkService $staffTimeOffLinkService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAbsenceFormData(int $userId): array
|
||||
{
|
||||
$admin = $this->userModel->find($userId);
|
||||
$displayName = $admin
|
||||
? trim(($admin->firstname ?? '') . ' ' . ($admin->lastname ?? ''))
|
||||
: 'Administrator';
|
||||
|
||||
$semester = $this->shared->getSemester();
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
|
||||
$existing = $this->staffAttendanceModel
|
||||
->where('user_id', $userId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('date')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'admin_name' => $displayName,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'existing' => $existing,
|
||||
'availableDates' => $this->shared->allowedAbsenceDates(),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(Request $request, int $userId): array
|
||||
{
|
||||
$semester = $this->shared->getSemester();
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
|
||||
if ($schoolYear === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Semester or school year not configured.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$dates = (array) $request->input('dates', []);
|
||||
$reasonType = trim((string) $request->input('reason_type', ''));
|
||||
$reasonText = trim((string) $request->input('reason', ''));
|
||||
|
||||
if ($reasonText === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Reason is required.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$reasonBase = $reasonType !== '' ? strtolower($reasonType) : '';
|
||||
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
|
||||
|
||||
$allowedSet = array_fill_keys($this->shared->allowedAbsenceDates(), true);
|
||||
|
||||
$roleName = method_exists($this->userModel, 'getUserRole')
|
||||
? ($this->userModel->getUserRole($userId) ?: null)
|
||||
: null;
|
||||
|
||||
$saved = 0;
|
||||
$invalid = [];
|
||||
$savedDates = [];
|
||||
|
||||
$dates = array_values(array_unique(array_map('strval', $dates)));
|
||||
|
||||
foreach ($dates as $d) {
|
||||
$d = trim($d);
|
||||
if ($d === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = \Carbon\Carbon::createFromFormat('Y-m-d', $d);
|
||||
if ($parsed->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
|
||||
$invalid[] = $d;
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$invalid[] = $d;
|
||||
continue;
|
||||
}
|
||||
|
||||
$semesterForDate = $this->semesterRangeService->getSemesterForDate($d);
|
||||
if ($semesterForDate === '') {
|
||||
$semesterForDate = $semester;
|
||||
}
|
||||
|
||||
$ok = $this->staffAttendanceModel->upsertOne(
|
||||
userId: $userId,
|
||||
roleName: $roleName,
|
||||
date: $d,
|
||||
semester: $semesterForDate,
|
||||
schoolYear: $schoolYear,
|
||||
status: StaffAttendance::STATUS_ABSENT,
|
||||
reason: $reason,
|
||||
editorId: $userId
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
$saved++;
|
||||
$savedDates[] = $d;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($invalid)) {
|
||||
return [
|
||||
'success' => 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 [
|
||||
'success' => true,
|
||||
'message' => $saved . ' day(s) saved as absent.',
|
||||
'saved' => $saved,
|
||||
'dates' => $savedDates,
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
protected function sendPrincipalNotification(
|
||||
int $userId,
|
||||
?string $roleName,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
string $reasonType,
|
||||
string $reasonText,
|
||||
array $dates,
|
||||
array $savedDates
|
||||
): void {
|
||||
try {
|
||||
$user = $this->userModel->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'
|
||||
);
|
||||
|
||||
$submittedAt = now()->toDateTimeString();
|
||||
|
||||
$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>' . 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>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' => 'administrator 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 $e) {
|
||||
Log::error('Failed to send TimeOff email (admin): ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
class AdministratorDashboardService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorMetricsService $metricsService,
|
||||
protected AdministratorUserSearchService $userSearchService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function metrics(): array
|
||||
{
|
||||
return $this->metricsService->metrics();
|
||||
}
|
||||
|
||||
public function userSearch(string $query): array
|
||||
{
|
||||
return $this->userSearchService->search($query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class AdministratorEnrollmentEventService
|
||||
{
|
||||
public function dispatchGroupedEvents(
|
||||
array $groupsByParentStatus,
|
||||
array $parentInfo,
|
||||
array $refundAmountByParent,
|
||||
string $schoolYear
|
||||
): void {
|
||||
$eventMap = [
|
||||
'admission under review' => 'admissionUnderReview',
|
||||
'payment pending' => 'paymentPending',
|
||||
'enrolled' => 'studentEnrolled',
|
||||
'withdraw under review' => 'withdrawUnderReview',
|
||||
'refund pending' => 'refundPending',
|
||||
'withdrawn' => 'withdrawn',
|
||||
'denied' => 'denied',
|
||||
'waitlist' => 'waitlist',
|
||||
];
|
||||
|
||||
foreach ($groupsByParentStatus as $pid => $byStatus) {
|
||||
$p = $parentInfo[$pid] ?? [
|
||||
'user_id' => $pid,
|
||||
'email' => null,
|
||||
'firstname' => '',
|
||||
'lastname' => '',
|
||||
];
|
||||
|
||||
$invoice = Invoice::query()
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $schoolYear)
|
||||
->latest('created_at')
|
||||
->first();
|
||||
|
||||
foreach ($byStatus as $status => $studentsArr) {
|
||||
if (!isset($eventMap[$status])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentData = array_map(
|
||||
fn ($s) => [
|
||||
'name' => $s['student_name'],
|
||||
'student_id' => $s['student_id'],
|
||||
],
|
||||
$studentsArr
|
||||
);
|
||||
|
||||
$parentData = [
|
||||
'user_id' => $p['user_id'],
|
||||
'email' => $p['email'],
|
||||
'firstname' => $p['firstname'],
|
||||
'lastname' => $p['lastname'],
|
||||
'school_year' => $schoolYear,
|
||||
'portalLink' => url('/login'),
|
||||
];
|
||||
|
||||
if ($status === 'payment pending' && $invoice) {
|
||||
$parentData['amount'] = (float) ($invoice->balance ?? $invoice->amount_due ?? $invoice->total_amount ?? 0);
|
||||
$parentData['due_date'] = $invoice->due_date ?? null;
|
||||
} elseif ($status === 'refund pending') {
|
||||
$parentData['amount'] = $refundAmountByParent[$pid] ?? null;
|
||||
}
|
||||
|
||||
Event::dispatch($eventMap[$status], [$parentData, $studentData]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdministratorEnrollmentQueryService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected Student $studentModel,
|
||||
protected StudentClass $studentClassModel,
|
||||
protected Enrollment $enrollmentModel,
|
||||
protected ClassSection $classSectionModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function enrollmentWithdrawalData(?string $selectedYear = null): array
|
||||
{
|
||||
$selectedYear = trim((string) ($selectedYear ?: $this->shared->getSchoolYear()));
|
||||
|
||||
$schoolYears = DB::table('enrollments')
|
||||
->distinct()
|
||||
->orderByDesc('school_year')
|
||||
->pluck('school_year')
|
||||
->map(fn ($year) => (string) $year)
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$students = method_exists($this->studentModel, 'getStudentsWithClassAndEnrollment')
|
||||
? ($this->studentModel->getStudentsWithClassAndEnrollment() ?? [])
|
||||
: [];
|
||||
|
||||
$selectedStartYear = $this->shared->getSchoolYearStartYear($selectedYear);
|
||||
$removedPriorIds = [];
|
||||
|
||||
if ($selectedStartYear !== null) {
|
||||
$removedRows = DB::table('enrollments')
|
||||
->select('student_id', 'school_year')
|
||||
->where('is_withdrawn', 1)
|
||||
->get();
|
||||
|
||||
foreach ($removedRows as $row) {
|
||||
$rowYear = $this->shared->getSchoolYearStartYear((string) ($row->school_year ?? ''));
|
||||
if ($rowYear !== null && $rowYear < $selectedStartYear) {
|
||||
$removedPriorIds[(int) ($row->student_id ?? 0)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($students as &$s) {
|
||||
$studentId = (int) ($s['id'] ?? 0);
|
||||
$s['student_id'] = $studentId;
|
||||
$s['removed_previous_year'] = isset($removedPriorIds[$studentId]) ? 'Yes' : 'No';
|
||||
|
||||
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 = method_exists($this->enrollmentModel, 'getEnrollmentStatus')
|
||||
? $this->enrollmentModel->getEnrollmentStatus($studentId, $selectedYear)
|
||||
: null;
|
||||
|
||||
if (!empty($statusForYear)) {
|
||||
$s['enrollment_status'] = $statusForYear;
|
||||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||||
$s['enrollment_status'] = 'denied';
|
||||
}
|
||||
|
||||
$className = method_exists($this->studentClassModel, 'getClassSectionsByStudentId')
|
||||
? $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear)
|
||||
: null;
|
||||
|
||||
$s['class_section'] = $className ?: 'Class not Assigned';
|
||||
|
||||
$s['registration_date_order'] = !empty($s['registration_date'])
|
||||
? Carbon::parse($s['registration_date'])->format('Y-m-d')
|
||||
: '';
|
||||
}
|
||||
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 = ClassSection::query()
|
||||
->select('id', 'class_section_id', 'class_section_name', 'school_year', 'semester')
|
||||
->where('school_year', $selectedYear)
|
||||
->where('semester', $this->shared->getSemester())
|
||||
->orderBy('class_section_name')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
if (empty($classes)) {
|
||||
$classes = ClassSection::query()
|
||||
->select('id', 'class_section_id', 'class_section_name')
|
||||
->orderBy('class_section_name')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return [
|
||||
'students' => $students,
|
||||
'classes' => $classes,
|
||||
'semester' => $this->shared->getSemester(),
|
||||
'school_year' => $selectedYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
];
|
||||
}
|
||||
|
||||
public function newStudentsData(): array
|
||||
{
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
|
||||
$rows = method_exists($this->studentModel, 'getStudentsWithParentsAndEmergency')
|
||||
? ($this->studentModel->getStudentsWithParentsAndEmergency($schoolYear) ?? [])
|
||||
: [];
|
||||
|
||||
$newStudents = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$studentId = (int) ($r['id'] ?? 0);
|
||||
|
||||
$classSection = method_exists($this->studentClassModel, 'getClassSectionsByStudentId')
|
||||
? $this->studentClassModel->getClassSectionsByStudentId($studentId, $schoolYear)
|
||||
: null;
|
||||
|
||||
$enrollmentStatus = method_exists($this->enrollmentModel, 'getEnrollmentStatus')
|
||||
? $this->enrollmentModel->getEnrollmentStatus($studentId, $schoolYear)
|
||||
: null;
|
||||
|
||||
$r['class_section'] = $classSection && trim((string) $classSection) !== ''
|
||||
? $classSection
|
||||
: 'Class not Assigned';
|
||||
|
||||
$r['is_new'] = (int) ($r['is_new'] ?? 0);
|
||||
$r['new_student'] = $r['is_new'] === 1 ? 'Yes' : 'No';
|
||||
$r['modalIdContact'] = 'contact_' . $studentId;
|
||||
$r['enrollment_status'] = $enrollmentStatus;
|
||||
|
||||
if (!empty($r['registration_date'])) {
|
||||
try {
|
||||
$r['registration_date'] = Carbon::parse($r['registration_date'])->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
$r['registration_date'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$newStudents[] = $r;
|
||||
}
|
||||
|
||||
return [
|
||||
'new_students' => $newStudents,
|
||||
'total_new' => count($newStudents),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\RefundModel as Refund;
|
||||
use App\Services\FeeCalculationService;
|
||||
|
||||
class AdministratorEnrollmentRefundService
|
||||
{
|
||||
public function __construct(
|
||||
protected FeeCalculationService $feeCalculationService
|
||||
) {
|
||||
}
|
||||
|
||||
public function processRefunds(array $parentIds, string $schoolYear, int $editorUserId): array
|
||||
{
|
||||
$errors = [];
|
||||
$refundAmountByParent = [];
|
||||
|
||||
foreach ($parentIds as $pid) {
|
||||
$students = Enrollment::query()
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
if (empty($students)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoice = Invoice::query()
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $schoolYear)
|
||||
->latest('created_at')
|
||||
->first();
|
||||
|
||||
if (!$invoice) {
|
||||
$errors[] = "No invoice found for parent ID {$pid} (for refund calc).";
|
||||
continue;
|
||||
}
|
||||
|
||||
$refundAmount = $this->feeCalculationService->calculateRefund($students, $pid);
|
||||
$refundAmountByParent[$pid] = $refundAmount;
|
||||
|
||||
$existingRefund = Refund::query()
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
|
||||
if ($existingRefund) {
|
||||
$existingRefund->update([
|
||||
'refund_amount' => $refundAmount,
|
||||
'status' => 'Pending',
|
||||
'updated_by' => $editorUserId,
|
||||
]);
|
||||
} else {
|
||||
Refund::query()->create([
|
||||
'parent_id' => $pid,
|
||||
'school_year' => $invoice->school_year,
|
||||
'invoice_id' => $invoice->id,
|
||||
'refund_amount' => $refundAmount,
|
||||
'refund_paid_amount' => 0.0,
|
||||
'status' => 'Pending',
|
||||
'requested_at' => now(),
|
||||
'updated_by' => $editorUserId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'errors' => $errors,
|
||||
'refundAmountByParent' => $refundAmountByParent,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
class AdministratorEnrollmentService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorEnrollmentQueryService $queryService,
|
||||
protected AdministratorEnrollmentStatusService $statusService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function enrollmentWithdrawalData(?string $selectedYear = null): array
|
||||
{
|
||||
return $this->queryService->enrollmentWithdrawalData($selectedYear);
|
||||
}
|
||||
|
||||
public function showNewStudentsData(): array
|
||||
{
|
||||
return $this->queryService->newStudentsData();
|
||||
}
|
||||
|
||||
public function updateStatuses(array $enrollmentStatuses, int $editorUserId): array
|
||||
{
|
||||
return $this->statusService->updateStatuses($enrollmentStatuses, $editorUserId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AdministratorEnrollmentStatusService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected Student $studentModel,
|
||||
protected User $userModel,
|
||||
protected AdministratorEnrollmentRefundService $refundService,
|
||||
protected AdministratorEnrollmentEventService $eventService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function updateStatuses(array $enrollmentStatuses, int $editorUserId): array
|
||||
{
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
$semester = $this->shared->getSemester();
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if (empty($enrollmentStatuses)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No enrollment statuses were submitted.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$groupsByParentStatus = [];
|
||||
$parentInfo = [];
|
||||
$refundParents = [];
|
||||
|
||||
$validStatuses = [
|
||||
'admission under review',
|
||||
'payment pending',
|
||||
'enrolled',
|
||||
'withdraw under review',
|
||||
'refund pending',
|
||||
'withdrawn',
|
||||
'denied',
|
||||
'waitlist',
|
||||
];
|
||||
|
||||
foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
|
||||
$studentId = (int) $studentId;
|
||||
|
||||
if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
|
||||
$errors[] = "Invalid enrollment status '{$newEnrollmentStatus}' for student ID {$studentId}.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->upsertSingleStatus(
|
||||
$studentId,
|
||||
$newEnrollmentStatus,
|
||||
$schoolYear,
|
||||
$semester,
|
||||
$parentInfo,
|
||||
$groupsByParentStatus,
|
||||
$refundParents
|
||||
);
|
||||
|
||||
if (!$result['success']) {
|
||||
$errors[] = $result['message'];
|
||||
}
|
||||
}
|
||||
|
||||
$refundResult = $this->refundService->processRefunds(
|
||||
array_keys($refundParents),
|
||||
$schoolYear,
|
||||
$editorUserId
|
||||
);
|
||||
|
||||
$errors = array_merge($errors, $refundResult['errors']);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->eventService->dispatchGroupedEvents(
|
||||
$groupsByParentStatus,
|
||||
$parentInfo,
|
||||
$refundResult['refundAmountByParent'],
|
||||
$schoolYear
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'message' => empty($errors)
|
||||
? 'Enrollment statuses updated and notifications sent.'
|
||||
: implode(' ', $errors),
|
||||
'status' => empty($errors) ? 200 : 207,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Enrollment withdrawal error: ' . $e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'An unexpected error occurred while processing enrollments.',
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function upsertSingleStatus(
|
||||
int $studentId,
|
||||
string $newEnrollmentStatus,
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
array &$parentInfo,
|
||||
array &$groupsByParentStatus,
|
||||
array &$refundParents
|
||||
): array {
|
||||
$admissionStatus = match ($newEnrollmentStatus) {
|
||||
'denied' => 'denied',
|
||||
'enrolled', 'payment pending' => 'accepted',
|
||||
default => 'pending',
|
||||
};
|
||||
|
||||
$enrollmentRow = DB::table('enrollments')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if (!$enrollmentRow) {
|
||||
return $this->createEnrollmentRow(
|
||||
$studentId,
|
||||
$newEnrollmentStatus,
|
||||
$admissionStatus,
|
||||
$schoolYear,
|
||||
$semester,
|
||||
$parentInfo,
|
||||
$groupsByParentStatus,
|
||||
$refundParents
|
||||
);
|
||||
}
|
||||
|
||||
$oldStatus = $enrollmentRow->enrollment_status ?? null;
|
||||
$parentId = (int) ($enrollmentRow->parent_id ?? 0);
|
||||
|
||||
if (!$parentId) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "No parent ID found for student ID {$studentId}.",
|
||||
];
|
||||
}
|
||||
|
||||
if ($oldStatus === $newEnrollmentStatus) {
|
||||
return ['success' => true, 'message' => 'No change'];
|
||||
}
|
||||
|
||||
$updated = DB::table('enrollments')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->update([
|
||||
'enrollment_status' => $newEnrollmentStatus,
|
||||
'admission_status' => $admissionStatus,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (!$updated) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Failed to update enrollment for student ID {$studentId}.",
|
||||
];
|
||||
}
|
||||
|
||||
$studentName = $this->resolveStudentName($studentId);
|
||||
$this->ensureParentInfoLoaded($parentId, $parentInfo);
|
||||
|
||||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
|
||||
if ($newEnrollmentStatus === 'refund pending') {
|
||||
$refundParents[$parentId] = true;
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'Updated'];
|
||||
}
|
||||
|
||||
protected function createEnrollmentRow(
|
||||
int $studentId,
|
||||
string $newEnrollmentStatus,
|
||||
string $admissionStatus,
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
array &$parentInfo,
|
||||
array &$groupsByParentStatus,
|
||||
array &$refundParents
|
||||
): array {
|
||||
$stu = $this->studentModel->find($studentId);
|
||||
$stu = is_array($stu) ? $stu : ($stu?->toArray() ?? []);
|
||||
|
||||
$parentId = (int) ($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0));
|
||||
if (!$parentId) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "No parent ID found for student ID {$studentId}.",
|
||||
];
|
||||
}
|
||||
|
||||
$isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;
|
||||
|
||||
$ok = DB::table('enrollments')->insert([
|
||||
'student_id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'enrollment_date' => now()->toDateString(),
|
||||
'is_withdrawn' => $isWithdrawn,
|
||||
'enrollment_status' => $newEnrollmentStatus,
|
||||
'admission_status' => $admissionStatus,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (!$ok) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Failed to create enrollment for student ID {$studentId}.",
|
||||
];
|
||||
}
|
||||
|
||||
$studentName = $this->resolveStudentName($studentId);
|
||||
$this->ensureParentInfoLoaded($parentId, $parentInfo);
|
||||
|
||||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
|
||||
if ($newEnrollmentStatus === 'refund pending') {
|
||||
$refundParents[$parentId] = true;
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'Created'];
|
||||
}
|
||||
|
||||
protected function resolveStudentName(int $studentId): string
|
||||
{
|
||||
$studentRow = $this->studentModel->find($studentId);
|
||||
$studentData = is_array($studentRow) ? $studentRow : ($studentRow?->toArray() ?? []);
|
||||
|
||||
return trim(($studentData['firstname'] ?? '') . ' ' . ($studentData['lastname'] ?? ''))
|
||||
?: "Student #{$studentId}";
|
||||
}
|
||||
|
||||
protected function ensureParentInfoLoaded(int $parentId, array &$parentInfo): void
|
||||
{
|
||||
if (isset($parentInfo[$parentId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$p = $this->userModel->find($parentId);
|
||||
$parentData = is_array($p) ? $p : ($p?->toArray() ?? []);
|
||||
|
||||
$parentInfo[$parentId] = [
|
||||
'user_id' => $parentData['id'] ?? $parentId,
|
||||
'email' => $parentData['email'] ?? null,
|
||||
'firstname' => $parentData['firstname'] ?? '',
|
||||
'lastname' => $parentData['lastname'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\LoginActivity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdministratorMetricsService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected User $userModel,
|
||||
protected LoginActivity $loginActivityModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function metrics(): array
|
||||
{
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
$semester = $this->shared->getSemester();
|
||||
|
||||
$recentActivities = method_exists($this->loginActivityModel, 'getLastActivities')
|
||||
? ($this->loginActivityModel->getLastActivities(4) ?? [])
|
||||
: [];
|
||||
|
||||
$totalAdmins = method_exists($this->userModel, 'countAdminsBySchoolYear')
|
||||
? (int) ($this->userModel->countAdminsBySchoolYear($schoolYear) ?? 0)
|
||||
: 0;
|
||||
|
||||
$teachers = method_exists($this->userModel, 'getUsersByRoleAndSchoolYear')
|
||||
? ($this->userModel->getUsersByRoleAndSchoolYear('teacher', $schoolYear) ?? [])
|
||||
: [];
|
||||
|
||||
$teacherAssistants = method_exists($this->userModel, 'getUsersByRoleAndSchoolYear')
|
||||
? ($this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $schoolYear) ?? [])
|
||||
: [];
|
||||
|
||||
$parents = method_exists($this->userModel, 'getUsersByRoleAndSchoolYear')
|
||||
? ($this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear) ?? [])
|
||||
: [];
|
||||
|
||||
$totalStudents = (int) (
|
||||
DB::table('student_class')
|
||||
->join('students', 'students.id', '=', 'student_class.student_id')
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->whereNotNull('student_class.class_section_id')
|
||||
->where('students.is_active', 1)
|
||||
->distinct('student_class.student_id')
|
||||
->count('student_class.student_id')
|
||||
);
|
||||
|
||||
return [
|
||||
'counts' => [
|
||||
'students' => $totalStudents,
|
||||
'teachers' => $this->shared->countUniqueEntities($teachers),
|
||||
'teacherAssistants' => $this->shared->countUniqueEntities($teacherAssistants),
|
||||
'admins' => $totalAdmins,
|
||||
'parents' => $this->shared->countUniqueEntities($parents),
|
||||
],
|
||||
'recentActivities' => collect($recentActivities)->map(function ($activity) {
|
||||
return [
|
||||
'login_time' => is_array($activity) ? ($activity['login_time'] ?? null) : ($activity->login_time ?? null),
|
||||
'email' => is_array($activity) ? ($activity['email'] ?? null) : ($activity->email ?? null),
|
||||
];
|
||||
})->values()->all(),
|
||||
'meta' => [
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
class AdministratorNotificationService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminNotificationSubjectService $subjectService,
|
||||
protected AdminPrintRecipientService $printRecipientService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function notificationsAlertsData(): array
|
||||
{
|
||||
return $this->subjectService->alertsData();
|
||||
}
|
||||
|
||||
public function saveNotificationSubjects(array $posted): array
|
||||
{
|
||||
return $this->subjectService->save($posted);
|
||||
}
|
||||
|
||||
public function printNotificationRecipientsData(): array
|
||||
{
|
||||
return $this->printRecipientService->data();
|
||||
}
|
||||
|
||||
public function savePrintNotificationRecipients(array $posted): array
|
||||
{
|
||||
return $this->printRecipientService->save($posted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AdministratorSharedService
|
||||
{
|
||||
public function __construct(
|
||||
protected Configuration $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSemester(): string
|
||||
{
|
||||
return (string) ($this->configuration->getConfig('semester') ?? '');
|
||||
}
|
||||
|
||||
public function getSchoolYear(): string
|
||||
{
|
||||
return (string) ($this->configuration->getConfig('school_year') ?? '');
|
||||
}
|
||||
|
||||
public function getPreviousSchoolYear(string $schoolYear): string
|
||||
{
|
||||
$schoolYear = trim($schoolYear);
|
||||
|
||||
if ($schoolYear === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
|
||||
return ((int) $m[1] - 1) . '-' . ((int) $m[2] - 1);
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
|
||||
$start = (int) $m[1] - 1;
|
||||
$end = (int) $m[2] - 1;
|
||||
if ($end < 0) {
|
||||
$end += 100;
|
||||
}
|
||||
return sprintf('%04d-%02d', $start, $end);
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}$/', $schoolYear)) {
|
||||
return (string) ((int) $schoolYear - 1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getSchoolYearStartYear(string $schoolYear): ?int
|
||||
{
|
||||
$schoolYear = trim($schoolYear);
|
||||
|
||||
if ($schoolYear === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}$/', $schoolYear)) {
|
||||
return (int) $schoolYear;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function allowedAbsenceDates(): array
|
||||
{
|
||||
$today = Carbon::today();
|
||||
$schoolYear = $this->getSchoolYear();
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
public function countUniqueEntities($rows): int
|
||||
{
|
||||
if (!is_iterable($rows)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (is_array($row)) {
|
||||
if (isset($row['id'])) {
|
||||
$ids[] = (int) $row['id'];
|
||||
} elseif (isset($row['user_id'])) {
|
||||
$ids[] = (int) $row['user_id'];
|
||||
}
|
||||
} elseif (is_object($row)) {
|
||||
if (isset($row->id)) {
|
||||
$ids[] = (int) $row->id;
|
||||
} elseif (isset($row->user_id)) {
|
||||
$ids[] = (int) $row->user_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count(array_unique($ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdministratorTeacherSubmissionService
|
||||
{
|
||||
public function __construct(
|
||||
protected TeacherSubmissionReportService $reportService,
|
||||
protected TeacherSubmissionNotificationService $notificationService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function report(): array
|
||||
{
|
||||
return $this->reportService->report();
|
||||
}
|
||||
|
||||
public function sendNotifications(Request $request, int $adminId): array
|
||||
{
|
||||
return $this->notificationService->send($request, $adminId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdministratorUserSearchService
|
||||
{
|
||||
public function search(string $query): array
|
||||
{
|
||||
$q = trim($query);
|
||||
|
||||
if ($q === '') {
|
||||
return [
|
||||
'query' => '',
|
||||
'results' => [],
|
||||
'scope_used' => 'unscoped-raw',
|
||||
'scope_label' => 'all years/semesters (raw)',
|
||||
'total_found' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$tokens = array_values(array_filter(array_map('trim', $rawTokens)));
|
||||
|
||||
$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));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($v)) {
|
||||
$phoneMap[$t] = array_values(array_unique($v));
|
||||
}
|
||||
}
|
||||
|
||||
$applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) {
|
||||
foreach ($tokens as $t) {
|
||||
$qb->where(function ($sub) use ($columns, $phoneCols, $phoneMap, $t) {
|
||||
foreach ($columns as $col) {
|
||||
$sub->orWhere($col, 'like', '%' . $t . '%');
|
||||
}
|
||||
|
||||
if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
|
||||
foreach ($phoneMap[$t] as $pv) {
|
||||
foreach ($phoneCols as $pcol) {
|
||||
$sub->orWhere($pcol, 'like', '%' . $pv . '%');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $qb;
|
||||
};
|
||||
|
||||
$uQB = DB::table('users')
|
||||
->select('id', 'firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state', 'school_year', 'semester');
|
||||
$applyMultiTokenLike($uQB, ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'], $tokens, ['cellphone']);
|
||||
$users = $uQB->limit(150)->get()->map(fn($r) => (array) $r)->all();
|
||||
|
||||
$sQB = DB::table('students')
|
||||
->select('id', 'parent_id', 'school_id', 'firstname', 'lastname', 'dob', 'gender', 'school_year', 'semester', 'rfid_tag');
|
||||
$applyMultiTokenLike($sQB, ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'], $tokens);
|
||||
$students = $sQB->limit(150)->get()->map(fn($r) => (array) $r)->all();
|
||||
|
||||
$pQB = DB::table('parents')
|
||||
->select('id', 'firstparent_id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone', 'school_year', 'semester');
|
||||
$applyMultiTokenLike($pQB, ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'], $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()->map(fn($r) => (array) $r)->all();
|
||||
|
||||
$stQB = DB::table('staff')
|
||||
->select('id', 'user_id', 'firstname', 'lastname', 'email', 'phone', 'role_name', 'school_year', 'active_role');
|
||||
$applyMultiTokenLike($stQB, ['firstname', 'lastname', 'email', 'role_name', 'phone'], $tokens, ['phone']);
|
||||
$staff = $stQB->limit(150)->get()->map(fn($r) => (array) $r)->all();
|
||||
|
||||
$ecQB = DB::table('emergency_contacts')
|
||||
->select('id', 'parent_id', 'emergency_contact_name', 'relation', 'cellphone', 'email', 'school_year', 'semester');
|
||||
$applyMultiTokenLike($ecQB, ['emergency_contact_name', 'relation', 'email', 'cellphone'], $tokens, ['cellphone']);
|
||||
$emergency = $ecQB->limit(150)->get()->map(fn($r) => (array) $r)->all();
|
||||
|
||||
return [
|
||||
'query' => $q,
|
||||
'results' => [
|
||||
'users' => $users,
|
||||
'students' => $students,
|
||||
'parents' => $parents,
|
||||
'staff' => $staff,
|
||||
'emergency_contacts' => $emergency,
|
||||
],
|
||||
'scope_used' => 'unscoped-raw',
|
||||
'scope_label' => 'all years/semesters (raw, tokenized)',
|
||||
'total_found' => count($users) + count($students) + count($parents) + count($staff) + count($emergency),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\TeacherSubmissionNotificationHistory;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class TeacherSubmissionNotificationService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected TeacherSubmissionSupportService $support
|
||||
) {
|
||||
}
|
||||
|
||||
public function send(Request $request, int $adminId): array
|
||||
{
|
||||
$notify = $request->input('notify');
|
||||
if (!is_array($notify)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Select at least one teacher to notify.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$missingItemsPayload = $request->input('missing_items', []);
|
||||
$targets = $this->extractTargets($notify);
|
||||
|
||||
if (empty($targets)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Select at least one teacher to notify.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$teacherIds = array_values(array_unique(array_column($targets, 'teacher_id')));
|
||||
$classSectionIds = array_values(array_unique(array_column($targets, 'class_section_id')));
|
||||
|
||||
$classSections = ClassSection::query()
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->get()
|
||||
->keyBy('class_section_id');
|
||||
|
||||
$teachers = User::query()
|
||||
->select('id', 'firstname', 'lastname', 'email')
|
||||
->whereIn('id', $teacherIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$adminUser = User::find($adminId);
|
||||
$adminName = trim(($adminUser->firstname ?? '') . ' ' . ($adminUser->lastname ?? '')) ?: 'Administrator';
|
||||
|
||||
$scoreUrl = url('/');
|
||||
$sentCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$classSectionId = (int) $target['class_section_id'];
|
||||
$teacherId = (int) $target['teacher_id'];
|
||||
|
||||
$teacher = $teachers->get($teacherId);
|
||||
$sectionName = $classSections->get($classSectionId)?->class_section_name ?? "Section {$classSectionId}";
|
||||
$teacherName = trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) ?: 'Teacher';
|
||||
|
||||
$missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
|
||||
$missingItems = $this->support->parseMissingItemsPayload((string) $missingPayload);
|
||||
|
||||
$missingNote = !empty($missingItems)
|
||||
? '<p>Outstanding items: ' . e($this->support->formatMissingItemsText($missingItems)) . '.</p>'
|
||||
: '<p>Our records show no outstanding submissions for this section, but please verify if anything still needs attention.</p>';
|
||||
|
||||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
||||
$body = "<p>Dear {$teacherName},</p>"
|
||||
. "<p>Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.</p>"
|
||||
. $missingNote
|
||||
. "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>"
|
||||
. "<p>Thank you,<br>{$adminName}</p>";
|
||||
|
||||
$email = $teacher->email ?? '';
|
||||
$status = 'failed';
|
||||
|
||||
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
try {
|
||||
Mail::html($body, function ($message) use ($email, $subject) {
|
||||
$message->to($email)->subject($subject);
|
||||
});
|
||||
$status = 'sent';
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Teacher submission notification failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'sent') {
|
||||
$sentCount++;
|
||||
} else {
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
TeacherSubmissionNotificationHistory::create([
|
||||
'teacher_id' => $teacherId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'admin_id' => $adminId,
|
||||
'notification_category' => 'teacher_submissions',
|
||||
'message' => $this->support->truncateNotificationMessage($body),
|
||||
'status' => $status,
|
||||
'school_year' => $this->shared->getSchoolYear(),
|
||||
'semester' => $this->shared->getSemester(),
|
||||
'sent_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($sentCount > 0) {
|
||||
$parts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent';
|
||||
}
|
||||
if ($failCount > 0) {
|
||||
$parts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $failCount === 0,
|
||||
'message' => !empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.',
|
||||
'sent' => $sentCount,
|
||||
'failed' => $failCount,
|
||||
'status' => $failCount === 0 ? 200 : 207,
|
||||
];
|
||||
}
|
||||
|
||||
protected function extractTargets(array $notify): array
|
||||
{
|
||||
$targets = [];
|
||||
|
||||
foreach ($notify as $sectionIdRaw => $teachers) {
|
||||
$sectionId = (int) $sectionIdRaw;
|
||||
if ($sectionId <= 0 || !is_array($teachers)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($teachers as $teacherIdRaw => $value) {
|
||||
$teacherId = (int) $teacherIdRaw;
|
||||
if ($teacherId <= 0 || $value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets["{$sectionId}_{$teacherId}"] = [
|
||||
'class_section_id' => $sectionId,
|
||||
'teacher_id' => $teacherId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($targets);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\TeacherSubmissionNotificationHistory;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TeacherSubmissionReportService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected TeacherSubmissionSupportService $support
|
||||
) {
|
||||
}
|
||||
|
||||
public function report(): array
|
||||
{
|
||||
$semester = $this->shared->getSemester();
|
||||
$schoolYear = $this->shared->getSchoolYear();
|
||||
|
||||
$assignmentRows = DB::table('teacher_class as tc')
|
||||
->select([
|
||||
'tc.class_section_id',
|
||||
'cs.class_section_name',
|
||||
'tc.teacher_id',
|
||||
'u.firstname',
|
||||
'u.lastname',
|
||||
'tc.position',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'tc.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'tc.teacher_id')
|
||||
->where('tc.school_year', $schoolYear)
|
||||
->where('tc.semester', $semester)
|
||||
->orderBy('cs.class_section_name')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
$teachersBySection = [];
|
||||
foreach ($assignmentRows as $assignment) {
|
||||
$sectionId = (int) ($assignment['class_section_id'] ?? 0);
|
||||
if ($sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$positionKey = strtolower(trim((string) ($assignment['position'] ?? '')));
|
||||
$roleKey = $positionKey !== '' ? $positionKey : 'teacher';
|
||||
|
||||
$positionLabel = match ($roleKey) {
|
||||
'ta' => 'TA',
|
||||
'main' => 'Main',
|
||||
default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher',
|
||||
};
|
||||
|
||||
$teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? ''));
|
||||
$teacherId = (int) ($assignment['teacher_id'] ?? 0);
|
||||
|
||||
if ($teacherFullName === '' || $teacherId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($teachersBySection[$sectionId])) {
|
||||
$teachersBySection[$sectionId] = [
|
||||
'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}",
|
||||
'teachers' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$teachersBySection[$sectionId]['teachers'][] = [
|
||||
'id' => $teacherId,
|
||||
'label' => "{$positionLabel}: {$teacherFullName}",
|
||||
'role_key' => $roleKey,
|
||||
];
|
||||
}
|
||||
|
||||
$today = now()->format('Y-m-d');
|
||||
$rows = [];
|
||||
$totalStatuses = 0;
|
||||
$missingItemCount = 0;
|
||||
$allTeacherIds = [];
|
||||
$allClassSectionIds = [];
|
||||
|
||||
foreach ($teachersBySection as $classSectionId => $section) {
|
||||
$classSectionId = (int) $classSectionId;
|
||||
if ($classSectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentIds = DB::table('student_class')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->pluck('student_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$expected = count($studentIds);
|
||||
|
||||
$midtermStudents = [];
|
||||
$participationStudents = [];
|
||||
|
||||
$scoreRecords = SemesterScore::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
foreach ($scoreRecords as $score) {
|
||||
$sid = (int) ($score->student_id ?? 0);
|
||||
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim((string) ($score->midterm_exam_score ?? '')) !== '') {
|
||||
$midtermStudents[$sid] = true;
|
||||
}
|
||||
|
||||
if (trim((string) ($score->participation_score ?? '')) !== '') {
|
||||
$participationStudents[$sid] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$midtermCommentStudents = [];
|
||||
$ptapCommentStudents = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$comments = ScoreComment::query()
|
||||
->select('student_id', 'score_type', 'comment')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('score_type', ['midterm', 'ptap'])
|
||||
->get();
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
$sid = (int) ($comment->student_id ?? 0);
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$text = trim((string) ($comment->comment ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = strtolower(trim((string) ($comment->score_type ?? '')));
|
||||
if ($type === 'midterm') {
|
||||
$midtermCommentStudents[$sid] = true;
|
||||
}
|
||||
if ($type === 'ptap') {
|
||||
$ptapCommentStudents[$sid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$attendanceRow = AttendanceDay::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', $today)
|
||||
->first();
|
||||
|
||||
$attendanceSubmitted = $attendanceRow
|
||||
&& in_array(strtolower((string) ($attendanceRow->status ?? '')), ['submitted', 'published', 'finalized'], true);
|
||||
|
||||
$teacherList = $section['teachers'] ?? [];
|
||||
usort(
|
||||
$teacherList,
|
||||
fn ($a, $b) => $this->support->teacherRolePriority($a['role_key'] ?? 'teacher')
|
||||
<=> $this->support->teacherRolePriority($b['role_key'] ?? 'teacher')
|
||||
);
|
||||
|
||||
foreach ($teacherList as $teacherEntry) {
|
||||
if (!empty($teacherEntry['id'])) {
|
||||
$allTeacherIds[] = $teacherEntry['id'];
|
||||
}
|
||||
}
|
||||
$allClassSectionIds[] = $classSectionId;
|
||||
|
||||
$midtermScoreStatus = $this->support->submissionStatus(count($midtermStudents), $expected);
|
||||
$midtermCommentStatus = $this->support->submissionStatus(count($midtermCommentStudents), $expected);
|
||||
$participationStatus = $this->support->submissionStatus(count($participationStudents), $expected);
|
||||
$ptapCommentStatus = $this->support->submissionStatus(count($ptapCommentStudents), $expected);
|
||||
$attendanceStatus = $this->support->attendanceStatus($attendanceSubmitted);
|
||||
|
||||
$statusDetails = [
|
||||
'midterm_score_status' => $midtermScoreStatus,
|
||||
'midterm_comment_status' => $midtermCommentStatus,
|
||||
'participation_status' => $participationStatus,
|
||||
'ptap_comment_status' => $ptapCommentStatus,
|
||||
'attendance_status' => $attendanceStatus,
|
||||
];
|
||||
|
||||
$missingItemsForSection = $this->support->buildMissingItems($statusDetails);
|
||||
$missingItemCount += count($missingItemsForSection);
|
||||
$totalStatuses += count($statusDetails);
|
||||
|
||||
$rows[] = [
|
||||
'class_section' => $section['class_section'] ?? "Section {$classSectionId}",
|
||||
'class_section_id' => $classSectionId,
|
||||
'teachers' => array_values($teacherList),
|
||||
'midterm_score_status' => $midtermScoreStatus,
|
||||
'midterm_comment_status' => $midtermCommentStatus,
|
||||
'participation_status' => $participationStatus,
|
||||
'ptap_comment_status' => $ptapCommentStatus,
|
||||
'attendance_status' => $attendanceStatus,
|
||||
'missing_items' => $missingItemsForSection,
|
||||
'student_count' => $expected,
|
||||
];
|
||||
}
|
||||
|
||||
$historyMap = $this->notificationHistoryMap($allTeacherIds, $allClassSectionIds);
|
||||
|
||||
$summary = [
|
||||
'total_items' => $totalStatuses,
|
||||
'missing_items' => $missingItemCount,
|
||||
'submitted_items' => max(0, $totalStatuses - $missingItemCount),
|
||||
'submission_percentage' => $totalStatuses > 0
|
||||
? (int) round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100)
|
||||
: 100,
|
||||
];
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'notificationHistory' => $historyMap,
|
||||
'summary' => $summary,
|
||||
];
|
||||
}
|
||||
|
||||
protected function notificationHistoryMap(array $allTeacherIds, array $allClassSectionIds): array
|
||||
{
|
||||
$historyMap = [];
|
||||
$teacherIds = array_values(array_unique($allTeacherIds));
|
||||
$classSectionIds = array_values(array_unique($allClassSectionIds));
|
||||
|
||||
if (empty($teacherIds) || empty($classSectionIds)) {
|
||||
return $historyMap;
|
||||
}
|
||||
|
||||
$historyRecords = TeacherSubmissionNotificationHistory::query()
|
||||
->select('teacher_submission_notification_history.*', 'u.firstname', 'u.lastname')
|
||||
->leftJoin('users as u', 'u.id', '=', 'teacher_submission_notification_history.admin_id')
|
||||
->where('notification_category', 'teacher_submissions')
|
||||
->whereIn('teacher_submission_notification_history.teacher_id', $teacherIds)
|
||||
->whereIn('teacher_submission_notification_history.class_section_id', $classSectionIds)
|
||||
->orderByDesc('sent_at')
|
||||
->get();
|
||||
|
||||
foreach ($historyRecords as $record) {
|
||||
$sectionId = (int) ($record->class_section_id ?? 0);
|
||||
$teacherId = (int) ($record->teacher_id ?? 0);
|
||||
|
||||
if ($sectionId <= 0 || $teacherId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$adminName = trim(($record->firstname ?? '') . ' ' . ($record->lastname ?? ''));
|
||||
if ($adminName === '') {
|
||||
$adminName = 'Administrator';
|
||||
}
|
||||
|
||||
$historyMap[$sectionId][$teacherId][] = [
|
||||
'sent_at_text' => $record->sent_at ? Carbon::parse($record->sent_at)->format('M j, Y g:i A') : '',
|
||||
'admin_name' => $adminName,
|
||||
'status' => strtolower((string) ($record->status ?? 'sent')),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($historyMap as &$teachersHistory) {
|
||||
foreach ($teachersHistory as &$entries) {
|
||||
$entries = array_slice($entries, 0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
return $historyMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
class TeacherSubmissionSupportService
|
||||
{
|
||||
public function submissionStatus(int $filled, int $expected): array
|
||||
{
|
||||
if ($expected <= 0) {
|
||||
return [
|
||||
'label' => 'No students',
|
||||
'badge' => 'bg-secondary',
|
||||
'detail' => '',
|
||||
'completed' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$completed = $filled >= $expected;
|
||||
|
||||
return [
|
||||
'label' => $completed ? 'Submitted' : 'Missing',
|
||||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||||
'detail' => "{$filled}/{$expected}",
|
||||
'completed' => $completed,
|
||||
];
|
||||
}
|
||||
|
||||
public function attendanceStatus(bool $submitted): array
|
||||
{
|
||||
return [
|
||||
'label' => $submitted ? 'Submitted' : 'Missing',
|
||||
'badge' => $submitted ? 'bg-success' : 'bg-danger',
|
||||
'completed' => $submitted,
|
||||
];
|
||||
}
|
||||
|
||||
public function buildMissingItems(array $statusMap): array
|
||||
{
|
||||
$labels = [
|
||||
'midterm_score_status' => 'midterm scores',
|
||||
'midterm_comment_status' => 'midterm comments',
|
||||
'participation_status' => 'participation',
|
||||
'ptap_comment_status' => 'PTAP comments',
|
||||
'attendance_status' => 'attendance',
|
||||
];
|
||||
|
||||
$items = [];
|
||||
foreach ($statusMap as $key => $status) {
|
||||
if (!(bool) ($status['completed'] ?? true) && isset($labels[$key])) {
|
||||
$items[] = $labels[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($items);
|
||||
}
|
||||
|
||||
public function teacherRolePriority(string $roleKey): int
|
||||
{
|
||||
return match (strtolower($roleKey)) {
|
||||
'main' => 1,
|
||||
'ta' => 2,
|
||||
default => 3,
|
||||
};
|
||||
}
|
||||
|
||||
public function truncateNotificationMessage(string $html, int $limit = 1000): string
|
||||
{
|
||||
$text = trim(strip_tags($html));
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strlen($text) <= $limit
|
||||
? $text
|
||||
: mb_substr($text, 0, $limit) . '…';
|
||||
}
|
||||
|
||||
public function parseMissingItemsPayload(string $payload): array
|
||||
{
|
||||
if ($payload === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode(base64_decode($payload, true) ?: '', true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($decoded as $item) {
|
||||
$item = trim((string) $item);
|
||||
if ($item !== '') {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($items));
|
||||
}
|
||||
|
||||
public function formatMissingItemsText(array $items): string
|
||||
{
|
||||
$items = array_values(array_filter(array_map('trim', $items), fn ($v) => $v !== ''));
|
||||
$count = count($items);
|
||||
|
||||
if ($count === 0) {
|
||||
return '';
|
||||
}
|
||||
if ($count === 1) {
|
||||
return $items[0];
|
||||
}
|
||||
if ($count === 2) {
|
||||
return $items[0] . ' and ' . $items[1];
|
||||
}
|
||||
|
||||
$last = array_pop($items);
|
||||
return implode(', ', $items) . ' and ' . $last;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user