Fix Laravel Pint formatting
This commit is contained in:
@@ -2,17 +2,19 @@
|
||||
|
||||
namespace App\Services\Admin\CompetitionWinners;
|
||||
|
||||
use App\Controllers\Admin\CompetitionWinnersController;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Competition;
|
||||
use App\Models\CompetitionClassWinner;
|
||||
use App\Models\CompetitionScore;
|
||||
use App\Models\CompetitionWinner;
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Administrative competition-winner workflows (ported from legacy {@see \App\Controllers\Admin\CompetitionWinnersController}).
|
||||
* Administrative competition-winner workflows (ported from legacy {@see CompetitionWinnersController}).
|
||||
*/
|
||||
class CompetitionWinnersAdminService
|
||||
{
|
||||
@@ -180,6 +182,7 @@ class CompetitionWinnersAdminService
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -253,7 +256,7 @@ class CompetitionWinnersAdminService
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @return array{competitions: \Illuminate\Support\Collection, sectionMap: array<int, string>} */
|
||||
/** @return array{competitions: Collection, sectionMap: array<int, string>} */
|
||||
public function indexData(): array
|
||||
{
|
||||
$competitions = Competition::query()->orderByDesc('id')->get();
|
||||
|
||||
@@ -138,12 +138,14 @@ final class CompetitionWinnersDomain
|
||||
$override = $overrides[$classId] ?? null;
|
||||
if ($override !== null) {
|
||||
$map[$classId] = (int) $override;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$baseCount = $classCounts[$classId] ?? ($scoreCounts[$classId] ?? 0);
|
||||
if ($baseCount <= 0) {
|
||||
$map[$classId] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ class AdminNotificationSubjectService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminNotificationUserService $userService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function subjectOptions(): array
|
||||
{
|
||||
@@ -31,7 +30,7 @@ class AdminNotificationSubjectService
|
||||
$assignedSubjects = [];
|
||||
$tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects');
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
if ($tableReady && ! empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = AdminNotificationSubject::query()
|
||||
@@ -59,7 +58,7 @@ class AdminNotificationSubjectService
|
||||
|
||||
public function save(array $posted): array
|
||||
{
|
||||
if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
if (! DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
@@ -102,7 +101,7 @@ class AdminNotificationSubjectService
|
||||
if (is_array($subjectRaw)) {
|
||||
foreach ($subjectRaw as $key => $value) {
|
||||
$candidate = is_string($key) ? $key : $value;
|
||||
if (!is_string($candidate)) {
|
||||
if (! is_string($candidate)) {
|
||||
continue;
|
||||
}
|
||||
$candidate = trim($candidate);
|
||||
@@ -118,7 +117,7 @@ class AdminNotificationSubjectService
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
if (! empty($toDelete)) {
|
||||
AdminNotificationSubject::query()
|
||||
->where('admin_id', $adminId)
|
||||
->whereIn('subject', $toDelete)
|
||||
@@ -126,7 +125,7 @@ class AdminNotificationSubjectService
|
||||
$updates += count($toDelete);
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
if (! empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $subject) {
|
||||
$batch[] = [
|
||||
@@ -147,4 +146,4 @@ class AdminNotificationSubjectService
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class AdminNotificationUserService
|
||||
->orderBy('u.lastname')
|
||||
->orderBy('u.firstname')
|
||||
->get()
|
||||
->map(fn($r) => (array) $r)
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ class AdminPrintRecipientService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminNotificationUserService $userService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
@@ -18,7 +17,7 @@ class AdminPrintRecipientService
|
||||
$tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects');
|
||||
$assigned = [];
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
if ($tableReady && ! empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = AdminNotificationSubject::query()
|
||||
@@ -44,7 +43,7 @@ class AdminPrintRecipientService
|
||||
|
||||
public function save(array $posted): array
|
||||
{
|
||||
if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
if (! DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
@@ -77,7 +76,7 @@ class AdminPrintRecipientService
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->pluck('admin_id')
|
||||
->map(fn($id) => (int) $id)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
@@ -85,14 +84,14 @@ class AdminPrintRecipientService
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
if (! empty($toDelete)) {
|
||||
AdminNotificationSubject::query()
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $toDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
if (! empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $adminId) {
|
||||
$batch[] = [
|
||||
@@ -113,4 +112,4 @@ class AdminPrintRecipientService
|
||||
'status' => 200,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Services\Staff\StaffTimeOffLinkService;
|
||||
use App\Models\StaffAttendance;
|
||||
use App\Models\User;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Services\SemesterRangeService;
|
||||
use App\Services\Staff\StaffTimeOffLinkService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -20,14 +21,13 @@ class AdministratorAbsenceService
|
||||
protected SemesterRangeService $semesterRangeService,
|
||||
protected StaffTimeOffLinkService $staffTimeOffLinkService,
|
||||
protected ApplicationUrlService $urls,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getAbsenceFormData(int $userId): array
|
||||
{
|
||||
$admin = $this->userModel->find($userId);
|
||||
$displayName = $admin
|
||||
? trim(($admin->firstname ?? '') . ' ' . ($admin->lastname ?? ''))
|
||||
? trim(($admin->firstname ?? '').' '.($admin->lastname ?? ''))
|
||||
: 'Administrator';
|
||||
|
||||
$semester = $this->shared->getSemester();
|
||||
@@ -76,7 +76,7 @@ class AdministratorAbsenceService
|
||||
}
|
||||
|
||||
$reasonBase = $reasonType !== '' ? strtolower($reasonType) : '';
|
||||
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
|
||||
$reason = $reasonBase !== '' ? ($reasonBase.': '.$reasonText) : $reasonText;
|
||||
|
||||
$allowedSet = array_fill_keys($this->shared->allowedAbsenceDates(), true);
|
||||
|
||||
@@ -97,13 +97,15 @@ class AdministratorAbsenceService
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = \Carbon\Carbon::createFromFormat('Y-m-d', $d);
|
||||
$parsed = Carbon::createFromFormat('Y-m-d', $d);
|
||||
if ($parsed->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
|
||||
$invalid[] = $d;
|
||||
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$invalid[] = $d;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -129,10 +131,10 @@ class AdministratorAbsenceService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($invalid)) {
|
||||
if (! empty($invalid)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid dates: ' . implode(', ', $invalid),
|
||||
'message' => 'Invalid dates: '.implode(', ', $invalid),
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
@@ -150,7 +152,7 @@ class AdministratorAbsenceService
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $saved . ' day(s) saved as absent.',
|
||||
'message' => $saved.' day(s) saved as absent.',
|
||||
'saved' => $saved,
|
||||
'dates' => $savedDates,
|
||||
'status' => 200,
|
||||
@@ -169,10 +171,10 @@ class AdministratorAbsenceService
|
||||
): void {
|
||||
try {
|
||||
$user = $this->userModel->find($userId);
|
||||
$fullName = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) ?: 'Administrator';
|
||||
$fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? '')) ?: 'Administrator';
|
||||
$userEmail = $user->email ?? '';
|
||||
$role = $roleName ?: 'admin';
|
||||
$dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
|
||||
$dateList = ! empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
|
||||
|
||||
$subject = sprintf(
|
||||
'TimeOff Request: %s (%s) — %s',
|
||||
@@ -184,20 +186,20 @@ class AdministratorAbsenceService
|
||||
$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>';
|
||||
.'<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,
|
||||
@@ -213,9 +215,9 @@ class AdministratorAbsenceService
|
||||
|
||||
$notifyUrl = $this->urls->timeoffNotifyUrl($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>';
|
||||
$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');
|
||||
|
||||
@@ -223,7 +225,7 @@ class AdministratorAbsenceService
|
||||
$message->to($principalEmail)->subject($subject);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to send TimeOff email (admin): ' . $e->getMessage());
|
||||
Log::error('Failed to send TimeOff email (admin): '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ class AdministratorDashboardService
|
||||
public function __construct(
|
||||
protected AdministratorMetricsService $metricsService,
|
||||
protected AdministratorUserSearchService $userSearchService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function metrics(): array
|
||||
{
|
||||
@@ -19,4 +18,4 @@ class AdministratorDashboardService
|
||||
{
|
||||
return $this->userSearchService->search($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ class AdministratorEnrollmentEventService
|
||||
{
|
||||
public function __construct(
|
||||
private ApplicationUrlService $urls,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function dispatchGroupedEvents(
|
||||
array $groupsByParentStatus,
|
||||
@@ -45,7 +44,7 @@ class AdministratorEnrollmentEventService
|
||||
->first();
|
||||
|
||||
foreach ($byStatus as $status => $studentsArr) {
|
||||
if (!isset($eventMap[$status])) {
|
||||
if (! isset($eventMap[$status])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -77,4 +76,4 @@ class AdministratorEnrollmentEventService
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ class AdministratorEnrollmentQueryService
|
||||
protected StudentClass $studentClassModel,
|
||||
protected Enrollment $enrollmentModel,
|
||||
protected ClassSection $classSectionModel,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function enrollmentWithdrawalData(?string $selectedYear = null): array
|
||||
{
|
||||
@@ -59,7 +58,7 @@ class AdministratorEnrollmentQueryService
|
||||
$s['student_id'] = $studentId;
|
||||
$s['removed_previous_year'] = isset($removedPriorIds[$studentId]) ? 'Yes' : 'No';
|
||||
|
||||
if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
|
||||
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);
|
||||
@@ -68,14 +67,14 @@ class AdministratorEnrollmentQueryService
|
||||
$pf = trim((string) ($s['parent_firstname'] ?? ''));
|
||||
$pl = trim((string) ($s['parent_lastname'] ?? ''));
|
||||
|
||||
if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
|
||||
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['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';
|
||||
@@ -84,7 +83,7 @@ class AdministratorEnrollmentQueryService
|
||||
? $this->enrollmentModel->getEnrollmentStatus($studentId, $selectedYear)
|
||||
: null;
|
||||
|
||||
if (!empty($statusForYear)) {
|
||||
if (! empty($statusForYear)) {
|
||||
$s['enrollment_status'] = $statusForYear;
|
||||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||||
$s['enrollment_status'] = 'denied';
|
||||
@@ -96,7 +95,7 @@ class AdministratorEnrollmentQueryService
|
||||
|
||||
$s['class_section'] = $className ?: 'Class not Assigned';
|
||||
|
||||
$s['registration_date_order'] = !empty($s['registration_date'])
|
||||
$s['registration_date_order'] = ! empty($s['registration_date'])
|
||||
? Carbon::parse($s['registration_date'])->format('Y-m-d')
|
||||
: '';
|
||||
}
|
||||
@@ -172,10 +171,10 @@ class AdministratorEnrollmentQueryService
|
||||
|
||||
$r['is_new'] = (int) ($r['is_new'] ?? 0);
|
||||
$r['new_student'] = $r['is_new'] === 1 ? 'Yes' : 'No';
|
||||
$r['modalIdContact'] = 'contact_' . $studentId;
|
||||
$r['modalIdContact'] = 'contact_'.$studentId;
|
||||
$r['enrollment_status'] = $enrollmentStatus;
|
||||
|
||||
if (!empty($r['registration_date'])) {
|
||||
if (! empty($r['registration_date'])) {
|
||||
try {
|
||||
$r['registration_date'] = Carbon::parse($r['registration_date'])->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
@@ -191,4 +190,4 @@ class AdministratorEnrollmentQueryService
|
||||
'total_new' => count($newStudents),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ class AdministratorEnrollmentRefundService
|
||||
{
|
||||
public function __construct(
|
||||
protected FeeCalculationService $feeCalculationService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function processRefunds(array $parentIds, string $schoolYear, int $editorUserId): array
|
||||
{
|
||||
@@ -36,8 +35,9 @@ class AdministratorEnrollmentRefundService
|
||||
->latest('created_at')
|
||||
->first();
|
||||
|
||||
if (!$invoice) {
|
||||
if (! $invoice) {
|
||||
$errors[] = "No invoice found for parent ID {$pid} (for refund calc).";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ class AdministratorEnrollmentService
|
||||
public function __construct(
|
||||
protected AdministratorEnrollmentQueryService $queryService,
|
||||
protected AdministratorEnrollmentStatusService $statusService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function enrollmentWithdrawalData(?string $selectedYear = null): array
|
||||
{
|
||||
|
||||
@@ -15,8 +15,7 @@ class AdministratorEnrollmentStatusService
|
||||
protected User $userModel,
|
||||
protected AdministratorEnrollmentRefundService $refundService,
|
||||
protected AdministratorEnrollmentEventService $eventService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function updateStatuses(array $enrollmentStatuses, int $editorUserId): array
|
||||
{
|
||||
@@ -53,8 +52,9 @@ class AdministratorEnrollmentStatusService
|
||||
foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
|
||||
$studentId = (int) $studentId;
|
||||
|
||||
if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
|
||||
if (! in_array($newEnrollmentStatus, $validStatuses, true)) {
|
||||
$errors[] = "Invalid enrollment status '{$newEnrollmentStatus}' for student ID {$studentId}.";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class AdministratorEnrollmentStatusService
|
||||
$refundParents
|
||||
);
|
||||
|
||||
if (!$result['success']) {
|
||||
if (! $result['success']) {
|
||||
$errors[] = $result['message'];
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class AdministratorEnrollmentStatusService
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Enrollment withdrawal error: ' . $e->getMessage());
|
||||
Log::error('Enrollment withdrawal error: '.$e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
@@ -129,7 +129,7 @@ class AdministratorEnrollmentStatusService
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if (!$enrollmentRow) {
|
||||
if (! $enrollmentRow) {
|
||||
return $this->createEnrollmentRow(
|
||||
$studentId,
|
||||
$newEnrollmentStatus,
|
||||
@@ -145,7 +145,7 @@ class AdministratorEnrollmentStatusService
|
||||
$oldStatus = $enrollmentRow->enrollment_status ?? null;
|
||||
$parentId = (int) ($enrollmentRow->parent_id ?? 0);
|
||||
|
||||
if (!$parentId) {
|
||||
if (! $parentId) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "No parent ID found for student ID {$studentId}.",
|
||||
@@ -165,7 +165,7 @@ class AdministratorEnrollmentStatusService
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (!$updated) {
|
||||
if (! $updated) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Failed to update enrollment for student ID {$studentId}.",
|
||||
@@ -201,7 +201,7 @@ class AdministratorEnrollmentStatusService
|
||||
$stu = is_array($stu) ? $stu : ($stu?->toArray() ?? []);
|
||||
|
||||
$parentId = (int) ($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0));
|
||||
if (!$parentId) {
|
||||
if (! $parentId) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "No parent ID found for student ID {$studentId}.",
|
||||
@@ -223,7 +223,7 @@ class AdministratorEnrollmentStatusService
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (!$ok) {
|
||||
if (! $ok) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Failed to create enrollment for student ID {$studentId}.",
|
||||
@@ -250,7 +250,7 @@ class AdministratorEnrollmentStatusService
|
||||
$studentRow = $this->studentModel->find($studentId);
|
||||
$studentData = is_array($studentRow) ? $studentRow : ($studentRow?->toArray() ?? []);
|
||||
|
||||
return trim(($studentData['firstname'] ?? '') . ' ' . ($studentData['lastname'] ?? ''))
|
||||
return trim(($studentData['firstname'] ?? '').' '.($studentData['lastname'] ?? ''))
|
||||
?: "Student #{$studentId}";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ class AdministratorMetricsService
|
||||
protected AdministratorSharedService $shared,
|
||||
protected User $userModel,
|
||||
protected LoginActivity $loginActivityModel,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function metrics(): array
|
||||
{
|
||||
@@ -70,4 +69,4 @@ class AdministratorMetricsService
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ class AdministratorNotificationService
|
||||
public function __construct(
|
||||
protected AdminNotificationSubjectService $subjectService,
|
||||
protected AdminPrintRecipientService $printRecipientService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function notificationsAlertsData(): array
|
||||
{
|
||||
@@ -29,4 +28,4 @@ class AdministratorNotificationService
|
||||
{
|
||||
return $this->printRecipientService->save($posted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ class AdministratorSharedService
|
||||
{
|
||||
public function __construct(
|
||||
protected Configuration $configuration
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getSemester(?string $override = null): string
|
||||
{
|
||||
@@ -35,7 +34,7 @@ class AdministratorSharedService
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
|
||||
return ((int) $m[1] - 1) . '-' . ((int) $m[2] - 1);
|
||||
return ((int) $m[1] - 1).'-'.((int) $m[2] - 1);
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
|
||||
@@ -44,6 +43,7 @@ class AdministratorSharedService
|
||||
if ($end < 0) {
|
||||
$end += 100;
|
||||
}
|
||||
|
||||
return sprintf('%04d-%02d', $start, $end);
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class AdministratorSharedService
|
||||
|
||||
public function countUniqueEntities($rows): int
|
||||
{
|
||||
if (!is_iterable($rows)) {
|
||||
if (! is_iterable($rows)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ class AdministratorTeacherSubmissionService
|
||||
public function __construct(
|
||||
protected TeacherSubmissionReportService $reportService,
|
||||
protected TeacherSubmissionNotificationService $notificationService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function report(array $filters = []): array
|
||||
{
|
||||
|
||||
@@ -38,23 +38,23 @@ class AdministratorUserSearchService
|
||||
$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));
|
||||
$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));
|
||||
$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)) {
|
||||
if (! empty($v)) {
|
||||
$phoneMap[$t] = array_values(array_unique($v));
|
||||
}
|
||||
}
|
||||
@@ -63,13 +63,13 @@ class AdministratorUserSearchService
|
||||
foreach ($tokens as $t) {
|
||||
$qb->where(function ($sub) use ($columns, $phoneCols, $phoneMap, $t) {
|
||||
foreach ($columns as $col) {
|
||||
$sub->orWhere($col, 'like', '%' . $t . '%');
|
||||
$sub->orWhere($col, 'like', '%'.$t.'%');
|
||||
}
|
||||
|
||||
if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
|
||||
if (! empty($phoneMap[$t]) && ! empty($phoneCols)) {
|
||||
foreach ($phoneMap[$t] as $pv) {
|
||||
foreach ($phoneCols as $pcol) {
|
||||
$sub->orWhere($pcol, 'like', '%' . $pv . '%');
|
||||
$sub->orWhere($pcol, 'like', '%'.$pv.'%');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,12 +82,12 @@ class AdministratorUserSearchService
|
||||
$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();
|
||||
$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();
|
||||
$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');
|
||||
@@ -99,17 +99,17 @@ class AdministratorUserSearchService
|
||||
}
|
||||
}
|
||||
|
||||
$parents = $pQB->limit(150)->get()->map(fn($r) => (array) $r)->all();
|
||||
$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();
|
||||
$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();
|
||||
$emergency = $ecQB->limit(150)->get()->map(fn ($r) => (array) $r)->all();
|
||||
|
||||
return [
|
||||
'query' => $q,
|
||||
@@ -125,4 +125,4 @@ class AdministratorUserSearchService
|
||||
'total_found' => count($users) + count($students) + count($parents) + count($staff) + count($emergency),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Services\Administrator;
|
||||
|
||||
use App\Mail\TeacherSubmissionReminderMail;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\TeacherSubmissionNotificationHistory;
|
||||
use App\Models\User;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -17,13 +17,12 @@ class TeacherSubmissionNotificationService
|
||||
protected AdministratorSharedService $shared,
|
||||
protected TeacherSubmissionSupportService $support,
|
||||
protected ApplicationUrlService $urls,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function send(Request $request, int $adminId): array
|
||||
{
|
||||
$notify = $request->input('notify');
|
||||
if (!is_array($notify)) {
|
||||
if (! is_array($notify)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Select at least one teacher to notify.',
|
||||
@@ -60,7 +59,7 @@ class TeacherSubmissionNotificationService
|
||||
->keyBy('id');
|
||||
|
||||
$adminUser = User::find($adminId);
|
||||
$adminName = trim(($adminUser->firstname ?? '') . ' ' . ($adminUser->lastname ?? '')) ?: 'Administrator';
|
||||
$adminName = trim(($adminUser->firstname ?? '').' '.($adminUser->lastname ?? '')) ?: 'Administrator';
|
||||
|
||||
$scoreUrl = $this->urls->docsHomeUrl();
|
||||
$sentCount = 0;
|
||||
@@ -72,30 +71,30 @@ class TeacherSubmissionNotificationService
|
||||
|
||||
$teacher = $teachers->get($teacherId);
|
||||
$sectionName = $classSections->get($classSectionId)?->class_section_name ?? "Section {$classSectionId}";
|
||||
$teacherName = trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) ?: 'Teacher';
|
||||
$teacherName = trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? '')) ?: 'Teacher';
|
||||
|
||||
$missingItems = $this->resolveMissingItems($missingItemsPayload, $classSectionId, $teacherId);
|
||||
|
||||
$missingNote = !empty($missingItems)
|
||||
? '<p>Outstanding items: ' . e($this->support->formatMissingItemsText($missingItems)) . '.</p>'
|
||||
$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>";
|
||||
."<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)) {
|
||||
if (! empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
try {
|
||||
Mail::to($email)->send(new TeacherSubmissionReminderMail($subject, $body));
|
||||
$status = 'sent';
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Teacher submission notification failed: ' . $e->getMessage());
|
||||
Log::error('Teacher submission notification failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,15 +119,15 @@ class TeacherSubmissionNotificationService
|
||||
|
||||
$parts = [];
|
||||
if ($sentCount > 0) {
|
||||
$parts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent';
|
||||
$parts[] = $sentCount.' reminder'.($sentCount === 1 ? '' : 's').' sent';
|
||||
}
|
||||
if ($failCount > 0) {
|
||||
$parts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed';
|
||||
$parts[] = $failCount.' reminder'.($failCount === 1 ? '' : 's').' failed';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $failCount === 0,
|
||||
'message' => !empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.',
|
||||
'message' => ! empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.',
|
||||
'sent' => $sentCount,
|
||||
'failed' => $failCount,
|
||||
'status' => $failCount === 0 ? 200 : 207,
|
||||
@@ -164,7 +163,7 @@ class TeacherSubmissionNotificationService
|
||||
|
||||
foreach ($notify as $sectionIdRaw => $teachers) {
|
||||
$sectionId = (int) $sectionIdRaw;
|
||||
if ($sectionId <= 0 || !is_array($teachers)) {
|
||||
if ($sectionId <= 0 || ! is_array($teachers)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ class TeacherSubmissionNotificationService
|
||||
|
||||
protected function resolveMissingItems($payload, int $classSectionId, int $teacherId): array
|
||||
{
|
||||
if (!is_array($payload)) {
|
||||
if (! is_array($payload)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ class TeacherSubmissionReportService
|
||||
public function __construct(
|
||||
protected AdministratorSharedService $shared,
|
||||
protected TeacherSubmissionSupportService $support
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function report(array $filters = []): array
|
||||
{
|
||||
@@ -56,14 +55,14 @@ class TeacherSubmissionReportService
|
||||
default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher',
|
||||
};
|
||||
|
||||
$teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? ''));
|
||||
$teacherFullName = trim(($assignment['firstname'] ?? '').' '.($assignment['lastname'] ?? ''));
|
||||
$teacherId = (int) ($assignment['teacher_id'] ?? 0);
|
||||
|
||||
if ($teacherFullName === '' || $teacherId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($teachersBySection[$sectionId])) {
|
||||
if (! isset($teachersBySection[$sectionId])) {
|
||||
$teachersBySection[$sectionId] = [
|
||||
'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}",
|
||||
'teachers' => [],
|
||||
@@ -113,7 +112,7 @@ class TeacherSubmissionReportService
|
||||
|
||||
foreach ($scoreRecords as $score) {
|
||||
$sid = (int) ($score->student_id ?? 0);
|
||||
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
||||
if ($sid <= 0 || ($expected > 0 && ! in_array($sid, $studentIds, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -129,7 +128,7 @@ class TeacherSubmissionReportService
|
||||
$midtermCommentStudents = [];
|
||||
$ptapCommentStudents = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentIds)) {
|
||||
$comments = ScoreComment::query()
|
||||
->select('student_id', 'score_type', 'comment')
|
||||
->whereIn('student_id', $studentIds)
|
||||
@@ -177,7 +176,7 @@ class TeacherSubmissionReportService
|
||||
);
|
||||
|
||||
foreach ($teacherList as $teacherEntry) {
|
||||
if (!empty($teacherEntry['id'])) {
|
||||
if (! empty($teacherEntry['id'])) {
|
||||
$allTeacherIds[] = $teacherEntry['id'];
|
||||
}
|
||||
}
|
||||
@@ -264,7 +263,7 @@ class TeacherSubmissionReportService
|
||||
continue;
|
||||
}
|
||||
|
||||
$adminName = trim(($record->firstname ?? '') . ' ' . ($record->lastname ?? ''));
|
||||
$adminName = trim(($record->firstname ?? '').' '.($record->lastname ?? ''));
|
||||
if ($adminName === '') {
|
||||
$adminName = 'Administrator';
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class TeacherSubmissionSupportService
|
||||
|
||||
$items = [];
|
||||
foreach ($statusMap as $key => $status) {
|
||||
if (!(bool) ($status['completed'] ?? true) && isset($labels[$key])) {
|
||||
if (! (bool) ($status['completed'] ?? true) && isset($labels[$key])) {
|
||||
$items[] = $labels[$key];
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class TeacherSubmissionSupportService
|
||||
|
||||
return mb_strlen($text) <= $limit
|
||||
? $text
|
||||
: mb_substr($text, 0, $limit) . '…';
|
||||
: mb_substr($text, 0, $limit).'…';
|
||||
}
|
||||
|
||||
public function parseMissingItemsPayload($payload): array
|
||||
@@ -103,7 +103,7 @@ class TeacherSubmissionSupportService
|
||||
}
|
||||
|
||||
$decoded = json_decode(base64_decode($payload, true) ?: '', true);
|
||||
if (!is_array($decoded)) {
|
||||
if (! is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -130,10 +130,11 @@ class TeacherSubmissionSupportService
|
||||
return $items[0];
|
||||
}
|
||||
if ($count === 2) {
|
||||
return $items[0] . ' and ' . $items[1];
|
||||
return $items[0].' and '.$items[1];
|
||||
}
|
||||
|
||||
$last = array_pop($items);
|
||||
return implode(', ', $items) . ' and ' . $last;
|
||||
|
||||
return implode(', ', $items).' and '.$last;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class TrophyReportService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
private function studentRows(string $schoolYear): Collection
|
||||
{
|
||||
@@ -393,7 +393,7 @@ class TrophyReportService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, array<string, mixed>> $rows
|
||||
* @param Collection<int, array<string, mixed>> $rows
|
||||
* @return array<int, array{name: string, students: list<array<string, mixed>>}>
|
||||
*/
|
||||
private function sectionMap(Collection $rows): array
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Api\Reports\FilesController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
@@ -138,10 +139,10 @@ final class ApplicationUrlService
|
||||
return route('api.timeoff.notify', ['token' => $token]);
|
||||
}
|
||||
|
||||
/** Served by {@see \App\Http\Controllers\Api\Reports\FilesController::receipt} (uploads/receipts). */
|
||||
/** Served by {@see FilesController::receipt} (uploads/receipts). */
|
||||
public function forReceiptStorage(?string $filename): ?string
|
||||
{
|
||||
if (!$filename) {
|
||||
if (! $filename) {
|
||||
return null;
|
||||
}
|
||||
$safe = basename(trim($filename));
|
||||
@@ -149,10 +150,10 @@ final class ApplicationUrlService
|
||||
return $safe !== '' ? route('api.v1.files.receipt', ['name' => $safe]) : null;
|
||||
}
|
||||
|
||||
/** Served by {@see \App\Http\Controllers\Api\Reports\FilesController::reimb} (uploads/reimbursements). */
|
||||
/** Served by {@see FilesController::reimb} (uploads/reimbursements). */
|
||||
public function forReimbursementStorage(?string $filename): ?string
|
||||
{
|
||||
if (!$filename) {
|
||||
if (! $filename) {
|
||||
return null;
|
||||
}
|
||||
$safe = basename(trim($filename));
|
||||
|
||||
@@ -31,7 +31,7 @@ class AssignmentDataLoaderService
|
||||
->get()
|
||||
->map(function ($row) {
|
||||
$row->teacher_full_name = trim(
|
||||
((string) optional($row->teacher)->firstname) . ' ' .
|
||||
((string) optional($row->teacher)->firstname).' '.
|
||||
((string) optional($row->teacher)->lastname)
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class AssignmentSectionService
|
||||
->get()
|
||||
->mapWithKeys(function ($section) {
|
||||
$name = $section->class_section_name ?? $section->section_name ?? $section->name ?? '';
|
||||
|
||||
return [(int) $section->class_section_id => (string) $name];
|
||||
})
|
||||
->all();
|
||||
|
||||
@@ -93,7 +93,7 @@ class AssignmentService
|
||||
]);
|
||||
|
||||
$students = $studentRows
|
||||
->filter(fn ($row) => !empty($row->student))
|
||||
->filter(fn ($row) => ! empty($row->student))
|
||||
->map(function ($row) {
|
||||
$student = $row->student;
|
||||
|
||||
@@ -122,10 +122,10 @@ class AssignmentService
|
||||
'description' => (string) $descriptionMeta,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
->all();
|
||||
->filter()
|
||||
->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'classSections' => $classSections,
|
||||
@@ -220,7 +220,7 @@ class AssignmentService
|
||||
]);
|
||||
|
||||
$students = $studentRows
|
||||
->filter(fn ($row) => !empty($row->student))
|
||||
->filter(fn ($row) => ! empty($row->student))
|
||||
->map(function ($row) {
|
||||
$student = $row->student;
|
||||
|
||||
@@ -249,10 +249,10 @@ class AssignmentService
|
||||
'description' => (string) $descriptionMeta,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
->all();
|
||||
->filter()
|
||||
->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'classSections' => $classSections,
|
||||
|
||||
@@ -4,6 +4,4 @@ namespace App\Services;
|
||||
|
||||
use App\Services\Assignment\AssignmentService as AssignmentDomainService;
|
||||
|
||||
class AssignmentService extends AssignmentDomainService
|
||||
{
|
||||
}
|
||||
class AssignmentService extends AssignmentDomainService {}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceAutoPublishJobService
|
||||
{
|
||||
public function __construct(private AttendanceAutoPublishService $autoPublish)
|
||||
{
|
||||
}
|
||||
public function __construct(private AttendanceAutoPublishService $autoPublish) {}
|
||||
|
||||
public function run(?DateTimeImmutable $now = null): array
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use Config\School;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
@@ -9,7 +10,7 @@ class AttendanceAutoPublishService
|
||||
{
|
||||
public function timezone(?string $tzFromCfg = null): DateTimeZone
|
||||
{
|
||||
$tz = $tzFromCfg ?: (class_exists(\Config\School::class)
|
||||
$tz = $tzFromCfg ?: (class_exists(School::class)
|
||||
? (config('School')->attendance['timezone'] ?? null)
|
||||
: null);
|
||||
|
||||
@@ -19,7 +20,7 @@ class AttendanceAutoPublishService
|
||||
public function secondSundayAfterEndOfDay(string $ymd, ?string $tz = null): string
|
||||
{
|
||||
$zone = $this->timezone($tz);
|
||||
$day = new DateTimeImmutable($ymd . ' 00:00:00', $zone);
|
||||
$day = new DateTimeImmutable($ymd.' 00:00:00', $zone);
|
||||
|
||||
$weekday = (int) $day->format('w');
|
||||
$daysToNextSunday = (7 - $weekday) % 7;
|
||||
|
||||
@@ -29,13 +29,13 @@ class AttendanceCommentService
|
||||
$lastChar = $name !== '' ? substr($name, -1) : '';
|
||||
$namePossessive = ($name === 'This student')
|
||||
? "This student's"
|
||||
: ($lastChar === 's' || $lastChar === 'S' ? ($name . "'") : ($name . "'s"));
|
||||
: ($lastChar === 's' || $lastChar === 'S' ? ($name."'") : ($name."'s"));
|
||||
|
||||
if (strpos($text, '{name}') !== false) {
|
||||
return str_replace('{name}', $namePossessive, $text);
|
||||
}
|
||||
|
||||
return $namePossessive . ' ' . $text;
|
||||
return $namePossessive.' '.$text;
|
||||
}
|
||||
|
||||
public function templateForScore(float $score): ?array
|
||||
|
||||
@@ -12,8 +12,7 @@ class AttendanceConsequenceService
|
||||
public function __construct(
|
||||
private EmailService $emailService,
|
||||
private UserNotificationDispatchService $notifier
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function sendFollowUp(array $payload): array
|
||||
{
|
||||
@@ -55,10 +54,10 @@ class AttendanceConsequenceService
|
||||
string $_viewName,
|
||||
string $logLevel
|
||||
): array {
|
||||
$studentName = trim((string) ($payload['student']['firstname'] ?? '') . ' ' . (string) ($payload['student']['lastname'] ?? ''));
|
||||
$studentName = trim((string) ($payload['student']['firstname'] ?? '').' '.(string) ($payload['student']['lastname'] ?? ''));
|
||||
$parentEmail = (string) ($payload['parent']['email'] ?? '');
|
||||
$parentName = (string) ($payload['parent']['name'] ?? '');
|
||||
$teacherName = trim((string) ($payload['teacher']['firstname'] ?? '') . ' ' . (string) ($payload['teacher']['lastname'] ?? ''));
|
||||
$teacherName = trim((string) ($payload['teacher']['firstname'] ?? '').' '.(string) ($payload['teacher']['lastname'] ?? ''));
|
||||
$className = (string) ($payload['class']['class_section_name'] ?? '');
|
||||
$semester = (string) ($payload['semester'] ?? '');
|
||||
$schoolYear = (string) ($payload['school_year'] ?? '');
|
||||
@@ -69,6 +68,7 @@ class AttendanceConsequenceService
|
||||
Log::warning("Attendance {$type}: missing parent email", [
|
||||
'student_id' => $payload['student_id'] ?? null,
|
||||
]);
|
||||
|
||||
return ['ok' => false, 'message' => 'Missing parent email.'];
|
||||
}
|
||||
|
||||
@@ -76,21 +76,21 @@ class AttendanceConsequenceService
|
||||
|
||||
$html = trim((string) ($payload['html'] ?? ''));
|
||||
if ($html === '') {
|
||||
$period = trim($semester . ' ' . $schoolYear);
|
||||
$html = '<p>' . e($subject) . '</p>'
|
||||
. '<p><strong>Student:</strong> ' . e($studentName) . '</p>'
|
||||
. '<p><strong>Parent:</strong> ' . e($parentName) . '</p>'
|
||||
. '<p><strong>Teacher:</strong> ' . e($teacherName) . '</p>'
|
||||
. '<p><strong>Class:</strong> ' . e($className) . '</p>'
|
||||
. ($period !== '' ? '<p><strong>Term:</strong> ' . e($period) . '</p>' : '')
|
||||
. '<p><strong>Date:</strong> ' . e($dateFmt) . '</p>';
|
||||
$period = trim($semester.' '.$schoolYear);
|
||||
$html = '<p>'.e($subject).'</p>'
|
||||
.'<p><strong>Student:</strong> '.e($studentName).'</p>'
|
||||
.'<p><strong>Parent:</strong> '.e($parentName).'</p>'
|
||||
.'<p><strong>Teacher:</strong> '.e($teacherName).'</p>'
|
||||
.'<p><strong>Class:</strong> '.e($className).'</p>'
|
||||
.($period !== '' ? '<p><strong>Term:</strong> '.e($period).'</p>' : '')
|
||||
.'<p><strong>Date:</strong> '.e($dateFmt).'</p>';
|
||||
}
|
||||
|
||||
$ok = false;
|
||||
try {
|
||||
$ok = $this->emailService->send($parentEmail, $subject, $html, 'attendance');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Attendance email send failed: ' . $e->getMessage());
|
||||
Log::error('Attendance email send failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$summary = $this->buildInAppSummary($type, $studentName, $dateFmt);
|
||||
@@ -106,11 +106,11 @@ class AttendanceConsequenceService
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($payload['tracking_id'])) {
|
||||
if (! empty($payload['tracking_id'])) {
|
||||
AttendanceTracking::markAsNotified((int) $payload['tracking_id']);
|
||||
}
|
||||
|
||||
Log::log($logLevel, "Attendance {$type} email " . ($ok ? 'sent' : 'failed'), [
|
||||
Log::log($logLevel, "Attendance {$type} email ".($ok ? 'sent' : 'failed'), [
|
||||
'student_id' => $payload['student_id'] ?? null,
|
||||
'recipient' => $parentEmail,
|
||||
]);
|
||||
@@ -122,7 +122,8 @@ class AttendanceConsequenceService
|
||||
{
|
||||
$name = $studentName !== '' ? " — {$studentName}" : '';
|
||||
$date = $dateFmt !== '' ? " ({$dateFmt})" : '';
|
||||
return $base . $name . $date;
|
||||
|
||||
return $base.$name.$date;
|
||||
}
|
||||
|
||||
private function buildInAppSummary(string $type, string $studentName, string $dateFmt): string
|
||||
|
||||
@@ -11,18 +11,19 @@ class AttendanceDailySummaryService
|
||||
public function __construct(
|
||||
private UserNotificationDispatchService $notifier,
|
||||
private EmailService $emailService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function sendAbsenteesSummary(): int
|
||||
{
|
||||
$rows = $this->fetchTodaySummary('absent');
|
||||
|
||||
return $this->notifyParents($rows, 'absent');
|
||||
}
|
||||
|
||||
public function sendLatesSummary(): int
|
||||
{
|
||||
$rows = $this->fetchTodaySummary('late');
|
||||
|
||||
return $this->notifyParents($rows, 'late');
|
||||
}
|
||||
|
||||
@@ -49,12 +50,12 @@ class AttendanceDailySummaryService
|
||||
|
||||
if ($type === 'absent') {
|
||||
$query->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(ad.status))"), ['ABSENT', 'ABS', 'A'])
|
||||
$w->whereIn(DB::raw('UPPER(TRIM(ad.status))'), ['ABSENT', 'ABS', 'A'])
|
||||
->orWhereRaw("UPPER(TRIM(ad.status)) LIKE 'ABS%'");
|
||||
});
|
||||
} else {
|
||||
$query->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(ad.status))"), ['LATE', 'L'])
|
||||
$w->whereIn(DB::raw('UPPER(TRIM(ad.status))'), ['LATE', 'L'])
|
||||
->orWhereRaw("UPPER(TRIM(ad.status)) LIKE 'LATE%'");
|
||||
});
|
||||
}
|
||||
@@ -71,7 +72,7 @@ class AttendanceDailySummaryService
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentName = trim((string) ($row['student_firstname'] ?? '') . ' ' . (string) ($row['student_lastname'] ?? ''));
|
||||
$studentName = trim((string) ($row['student_firstname'] ?? '').' '.(string) ($row['student_lastname'] ?? ''));
|
||||
$date = substr((string) ($row['date'] ?? ''), 0, 10);
|
||||
|
||||
$message = sprintf(
|
||||
@@ -85,7 +86,7 @@ class AttendanceDailySummaryService
|
||||
|
||||
$email = (string) ($row['parent_email'] ?? '');
|
||||
if ($email !== '') {
|
||||
$body = '<p>' . e($message) . '</p>';
|
||||
$body = '<p>'.e($message).'</p>';
|
||||
$this->emailService->send($email, 'Daily Attendance Update', $body, 'attendance');
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class AttendancePolicyService
|
||||
{
|
||||
public function canEditDay(array $dayRow, array $currentUser): bool
|
||||
{
|
||||
$status = strtolower((string)($dayRow['status'] ?? 'draft'));
|
||||
$roles = array_map([$this, 'normalizeRole'], (array)($currentUser['roles'] ?? []));
|
||||
$permissions = array_map('strtolower', (array)($currentUser['permissions'] ?? []));
|
||||
$status = strtolower((string) ($dayRow['status'] ?? 'draft'));
|
||||
$roles = array_map([$this, 'normalizeRole'], (array) ($currentUser['roles'] ?? []));
|
||||
$permissions = array_map('strtolower', (array) ($currentUser['permissions'] ?? []));
|
||||
|
||||
$isAdmin = $this->isAdminLike($roles, $permissions);
|
||||
$isTeacher = in_array('teacher', $roles, true) || in_array('ta', $roles, true) || in_array('teacher_assistant', $roles, true);
|
||||
@@ -34,6 +33,7 @@ class AttendancePolicyService
|
||||
public function isTeacher(array $roles): bool
|
||||
{
|
||||
$roles = array_map([$this, 'normalizeRole'], $roles);
|
||||
|
||||
return in_array('teacher', $roles, true)
|
||||
|| in_array('ta', $roles, true)
|
||||
|| in_array('teacher_assistant', $roles, true)
|
||||
@@ -45,7 +45,7 @@ class AttendancePolicyService
|
||||
$roles = array_map([$this, 'normalizeRole'], $roles);
|
||||
$adminTokens = $this->adminRoleTokens();
|
||||
|
||||
if (!empty($adminTokens) && count(array_intersect($roles, $adminTokens)) > 0) {
|
||||
if (! empty($adminTokens) && count(array_intersect($roles, $adminTokens)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class AttendancePolicyService
|
||||
|
||||
public function normalizeRole(?string $role): string
|
||||
{
|
||||
return str_replace([' ', '-'], '_', strtolower(trim((string)$role)));
|
||||
return str_replace([' ', '-'], '_', strtolower(trim((string) $role)));
|
||||
}
|
||||
|
||||
public function canManageEarlyDismissals(array $userRoles): bool
|
||||
@@ -123,6 +123,7 @@ class AttendancePolicyService
|
||||
}
|
||||
|
||||
$cache = array_keys($tokens);
|
||||
|
||||
return $cache;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class AttendanceQueryService
|
||||
$sectionCodes = array_keys($bySection);
|
||||
|
||||
$labels = [];
|
||||
if (!empty($sectionCodes)) {
|
||||
if (! empty($sectionCodes)) {
|
||||
$rows = $this->classSection
|
||||
->query()
|
||||
->select(['class_section_id', 'class_section_name'])
|
||||
@@ -52,13 +52,13 @@ class AttendanceQueryService
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$code = (int)$row['class_section_id'];
|
||||
$name = trim((string)($row['class_section_name'] ?? ''));
|
||||
$labels[$code] = $name !== '' ? $name : 'Section #' . $code;
|
||||
$code = (int) $row['class_section_id'];
|
||||
$name = trim((string) ($row['class_section_name'] ?? ''));
|
||||
$labels[$code] = $name !== '' ? $name : 'Section #'.$code;
|
||||
}
|
||||
|
||||
foreach ($sectionCodes as $code) {
|
||||
$labels[$code] ??= 'Section #' . $code;
|
||||
$labels[$code] ??= 'Section #'.$code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ class AttendanceQueryService
|
||||
|
||||
if ($sectionCode > 0) {
|
||||
$assigned = $this->teacherClass->assignedForSectionTerm($sectionCode, $semester, $schoolYear);
|
||||
$teacherIds = array_map(fn($r) => (int)$r['teacher_id'], $assigned);
|
||||
$teacherIds = array_map(fn ($r) => (int) $r['teacher_id'], $assigned);
|
||||
|
||||
$statusMap = [];
|
||||
if (!empty($teacherIds)) {
|
||||
if (! empty($teacherIds)) {
|
||||
$rows = DB::table('staff_attendance as sa')
|
||||
->select('sa.user_id', 'sa.status', 'sa.reason')
|
||||
->where('sa.semester', $semester)
|
||||
@@ -80,7 +80,7 @@ class AttendanceQueryService
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statusMap[(int)$row->user_id] = [
|
||||
$statusMap[(int) $row->user_id] = [
|
||||
'status' => $row->status,
|
||||
'reason' => $row->reason,
|
||||
];
|
||||
@@ -88,15 +88,15 @@ class AttendanceQueryService
|
||||
}
|
||||
|
||||
foreach ($assigned as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
||||
$teacherId = (int) $teacher['teacher_id'];
|
||||
$position = strtolower((string) ($teacher['position'] ?? 'main'));
|
||||
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
||||
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
||||
$name = trim(($teacher['firstname'] ?? '').' '.($teacher['lastname'] ?? ''));
|
||||
$cell = $statusMap[$teacherId] ?? ['status' => null, 'reason' => null];
|
||||
|
||||
$grid[] = [
|
||||
'teacher_id' => $teacherId,
|
||||
'name' => $name !== '' ? $name : 'User#' . $teacherId,
|
||||
'name' => $name !== '' ? $name : 'User#'.$teacherId,
|
||||
'position' => $position,
|
||||
'status' => $cell['status'],
|
||||
'reason' => $cell['reason'],
|
||||
@@ -125,13 +125,13 @@ class AttendanceQueryService
|
||||
|
||||
public function teacherAttendanceFormData(?int $userId, int $requestedSectionId = 0): array
|
||||
{
|
||||
if (!$userId) {
|
||||
if (! $userId) {
|
||||
throw new RuntimeException('User not logged in.');
|
||||
}
|
||||
|
||||
$teacher = $this->user->find($userId);
|
||||
$teacherName = $teacher
|
||||
? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? ''))
|
||||
? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? ''))
|
||||
: 'Unknown Teacher';
|
||||
|
||||
$semester = $this->attendanceService->currentSemester();
|
||||
@@ -145,8 +145,8 @@ class AttendanceQueryService
|
||||
|
||||
$activeSection = null;
|
||||
foreach ($assignments as $assignment) {
|
||||
$cid = (int)($assignment['class_section_id'] ?? 0);
|
||||
$pk = (int)($assignment['class_section_pk'] ?? 0);
|
||||
$cid = (int) ($assignment['class_section_id'] ?? 0);
|
||||
$pk = (int) ($assignment['class_section_pk'] ?? 0);
|
||||
|
||||
if ($requestedSectionId > 0 && ($requestedSectionId === $cid || $requestedSectionId === $pk)) {
|
||||
$activeSection = $assignment;
|
||||
@@ -156,8 +156,8 @@ class AttendanceQueryService
|
||||
|
||||
$activeSection ??= $assignments[0];
|
||||
|
||||
$classSectionCode = (int)($activeSection['class_section_id'] ?? 0);
|
||||
$classSectionPk = (int)($activeSection['class_section_pk'] ?? 0);
|
||||
$classSectionCode = (int) ($activeSection['class_section_id'] ?? 0);
|
||||
$classSectionPk = (int) ($activeSection['class_section_pk'] ?? 0);
|
||||
$classSectionId = $classSectionCode > 0 ? $classSectionCode : $classSectionPk;
|
||||
|
||||
if ($classSectionId <= 0) {
|
||||
@@ -183,11 +183,11 @@ class AttendanceQueryService
|
||||
$lockedByStudent = [];
|
||||
|
||||
$attendanceTable = $this->attendanceData->getTable();
|
||||
$dateInSundaysSql = 'DATE(`' . $attendanceTable . '`.`date`) IN (' .
|
||||
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
||||
$dateInSundaysSql = 'DATE(`'.$attendanceTable.'`.`date`) IN ('.
|
||||
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
||||
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int)$student['id'];
|
||||
$studentId = (int) $student['id'];
|
||||
|
||||
$rows = AttendanceData::query()
|
||||
->select(['date', 'status', 'is_reported', 'reason', 'modified_by'])
|
||||
@@ -202,7 +202,7 @@ class AttendanceQueryService
|
||||
})
|
||||
->whereRaw($dateInSundaysSql, $sundayDates)
|
||||
->get()
|
||||
->map(fn($row) => (array)$row)
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$rows = $this->attendanceService->normalizeAttendanceEntries($rows);
|
||||
@@ -226,8 +226,8 @@ class AttendanceQueryService
|
||||
$recentHistory = [];
|
||||
foreach ($recentRows as $row) {
|
||||
$recentHistory[] = [
|
||||
'date' => substr((string)($row['date'] ?? ''), 0, 10),
|
||||
'status' => strtolower((string)($row['status'] ?? '')),
|
||||
'date' => substr((string) ($row['date'] ?? ''), 0, 10),
|
||||
'status' => strtolower((string) ($row['status'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ class AttendanceQueryService
|
||||
// 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)) {
|
||||
if ($d === '' || ! in_array($d, $sundayDates, true)) {
|
||||
continue;
|
||||
}
|
||||
$rowStatus = $row['status'] ?? null;
|
||||
@@ -340,13 +340,13 @@ class AttendanceQueryService
|
||||
|
||||
$teacherRows = [];
|
||||
|
||||
if (!empty($assigned)) {
|
||||
$teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);
|
||||
if (! empty($assigned)) {
|
||||
$teacherIds = array_map(static fn ($r) => (int) $r['teacher_id'], $assigned);
|
||||
|
||||
$statusMap = [];
|
||||
if (!empty($teacherIds)) {
|
||||
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
||||
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
||||
if (! empty($teacherIds)) {
|
||||
$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)
|
||||
@@ -372,7 +372,7 @@ class AttendanceQueryService
|
||||
if ($dk === '') {
|
||||
continue;
|
||||
}
|
||||
$statusMap[(int)$row->user_id][$dk] = [
|
||||
$statusMap[(int) $row->user_id][$dk] = [
|
||||
'status' => $row->status,
|
||||
'reason' => $row->reason,
|
||||
];
|
||||
@@ -380,10 +380,10 @@ class AttendanceQueryService
|
||||
}
|
||||
|
||||
foreach ($assigned as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
||||
$teacherId = (int) $teacher['teacher_id'];
|
||||
$position = strtolower((string) ($teacher['position'] ?? 'main'));
|
||||
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
||||
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
||||
$name = trim(($teacher['firstname'] ?? '').' '.($teacher['lastname'] ?? ''));
|
||||
|
||||
$history = [];
|
||||
foreach ($sundayDates as $date) {
|
||||
@@ -400,7 +400,7 @@ class AttendanceQueryService
|
||||
$teacherRows[] = [
|
||||
'teacher_id' => $teacherId,
|
||||
'position' => $position,
|
||||
'name' => $name !== '' ? $name : 'User#' . $teacherId,
|
||||
'name' => $name !== '' ? $name : 'User#'.$teacherId,
|
||||
'sunday_statuses' => $history,
|
||||
'today' => $history[$currentSunday] ?? null,
|
||||
'today_reason' => $todayReason,
|
||||
@@ -410,8 +410,8 @@ class AttendanceQueryService
|
||||
|
||||
$noSchoolDays = [];
|
||||
$calendarTable = $this->calendar->getTable();
|
||||
$calDateInSql = 'DATE(`' . $calendarTable . '`.`date`) IN (' .
|
||||
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
||||
$calDateInSql = 'DATE(`'.$calendarTable.'`.`date`) IN ('.
|
||||
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
||||
$calendarRows = $this->calendar->query()
|
||||
->where('no_school', 1)
|
||||
->whereRaw($calDateInSql, $sundayDates)
|
||||
@@ -438,9 +438,9 @@ class AttendanceQueryService
|
||||
'class_section_id' => $classSectionId,
|
||||
'sunday_dates' => $sundayDates,
|
||||
'current_sunday' => $currentSunday,
|
||||
'enable_attendance' => (int)$this->configuration->getConfig('enable_attendance'),
|
||||
'can_edit' => ((int)$this->configuration->getConfig('enable_attendance') === 1),
|
||||
'class_id' => (int)$this->classSection->getClassId($classSectionId),
|
||||
'enable_attendance' => (int) $this->configuration->getConfig('enable_attendance'),
|
||||
'can_edit' => ((int) $this->configuration->getConfig('enable_attendance') === 1),
|
||||
'class_id' => (int) $this->classSection->getClassId($classSectionId),
|
||||
'no_school_days' => $noSchoolDays,
|
||||
'today' => $currentSunday,
|
||||
'locked_by_student' => $lockedByStudent,
|
||||
@@ -463,7 +463,7 @@ class AttendanceQueryService
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->groupBy('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
|
||||
->get()
|
||||
->map(fn($row) => $row->toArray())
|
||||
->map(fn ($row) => $row->toArray())
|
||||
->all();
|
||||
};
|
||||
|
||||
@@ -492,32 +492,32 @@ class AttendanceQueryService
|
||||
$sectionCounts = $this->studentClass->getStudentCountsBySection($effectiveTermYear);
|
||||
|
||||
foreach ($classSections as $classSection) {
|
||||
$secPk = (int)($classSection['id'] ?? 0);
|
||||
$secCodeRaw = (string)($classSection['class_section_id'] ?? '');
|
||||
$secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk;
|
||||
$classId = (int)($classSection['class_id'] ?? 0);
|
||||
$secPk = (int) ($classSection['id'] ?? 0);
|
||||
$secCodeRaw = (string) ($classSection['class_section_id'] ?? '');
|
||||
$secCode = $secCodeRaw !== '' ? $secCodeRaw : (string) $secPk;
|
||||
$classId = (int) ($classSection['class_id'] ?? 0);
|
||||
|
||||
$studentsBySection[$secCode] = [];
|
||||
$datesBySection[$secCode] = [];
|
||||
$attendanceData[$secCode] = [];
|
||||
$attendanceRecord[$secCode] = [];
|
||||
|
||||
$countForCode = (int)($sectionCounts[$secCode] ?? 0);
|
||||
$countForPk = (int)($sectionCounts[$secPk] ?? 0);
|
||||
$countForCode = (int) ($sectionCounts[$secCode] ?? 0);
|
||||
$countForPk = (int) ($sectionCounts[$secPk] ?? 0);
|
||||
$sectionHasStudents = ($countForCode + $countForPk) > 0;
|
||||
|
||||
$students = $this->studentClass->getClassStudents($secCode, $effectiveTermYear);
|
||||
if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
|
||||
if (! $students && $secPk && (string) $secPk !== (string) $secCode) {
|
||||
$students = $this->studentClass->getClassStudents($secPk, $effectiveTermYear);
|
||||
}
|
||||
|
||||
if (!$sectionHasStudents || !$students) {
|
||||
if (! $sectionHasStudents || ! $students) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bulk-load student profiles for the whole section in one query
|
||||
$studentsArray = is_array($students) ? $students : $students->toArray();
|
||||
$studentIds = array_map(fn($sc) => (int)($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray);
|
||||
$studentIds = array_map(fn ($sc) => (int) ($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray);
|
||||
$studentProfiles = $this->student
|
||||
->query()
|
||||
->select(['id', 'firstname', 'lastname', 'school_id'])
|
||||
@@ -525,12 +525,13 @@ class AttendanceQueryService
|
||||
->where('is_active', 1)
|
||||
->get()
|
||||
->keyBy('id')
|
||||
->map(fn($row) => $row->toArray())
|
||||
->map(fn ($row) => $row->toArray())
|
||||
->all();
|
||||
|
||||
$activeIds = array_keys($studentProfiles);
|
||||
if (empty($activeIds)) {
|
||||
unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -539,17 +540,17 @@ class AttendanceQueryService
|
||||
->whereIn('student_id', $activeIds)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
if ($secPk && (string) $secPk !== (string) $secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->when($termSemester !== '', fn ($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn ($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->map(fn($row) => $row->toArray())
|
||||
->map(fn ($row) => $row->toArray())
|
||||
->groupBy('student_id')
|
||||
->map(fn($group) => $group->values()->all())
|
||||
->map(fn ($group) => $group->values()->all())
|
||||
->all();
|
||||
|
||||
// Bulk-load all attendance records (summaries) for the section in one query
|
||||
@@ -557,36 +558,36 @@ class AttendanceQueryService
|
||||
->whereIn('student_id', $activeIds)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
if ($secPk && (string) $secPk !== (string) $secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->when($termSemester !== '', fn ($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn ($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->get()
|
||||
->keyBy('student_id')
|
||||
->map(fn($row) => $row->toArray())
|
||||
->map(fn ($row) => $row->toArray())
|
||||
->all();
|
||||
|
||||
$hasRoster = false;
|
||||
|
||||
foreach ($studentIds as $studentId) {
|
||||
$student = $studentProfiles[$studentId] ?? null;
|
||||
if (!$student) {
|
||||
if (! $student) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentsBySection[$secCode][] = $student;
|
||||
$studentSchoolMap[$studentId] = (string)($student['school_id'] ?? '');
|
||||
$studentSchoolMap[$studentId] = (string) ($student['school_id'] ?? '');
|
||||
$hasRoster = true;
|
||||
|
||||
$entries = $this->attendanceService->normalizeAttendanceEntries(
|
||||
array_map(fn($r) => (array)$r, $allEntries[$studentId] ?? [])
|
||||
array_map(fn ($r) => (array) $r, $allEntries[$studentId] ?? [])
|
||||
);
|
||||
$attendanceData[$secCode][$studentId] = $entries;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$d = (string)($entry['date'] ?? '');
|
||||
$d = (string) ($entry['date'] ?? '');
|
||||
if ($d !== '') {
|
||||
$datesBySection[$secCode][$d] = true;
|
||||
}
|
||||
@@ -600,8 +601,9 @@ class AttendanceQueryService
|
||||
];
|
||||
}
|
||||
|
||||
if (!$hasRoster) {
|
||||
if (! $hasRoster) {
|
||||
unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -610,7 +612,7 @@ class AttendanceQueryService
|
||||
|
||||
foreach ($attendanceData as $secId => &$studentEntries) {
|
||||
foreach ($studentEntries as &$entries) {
|
||||
usort($entries, static fn($a, $b) => strcmp((string)($a['date'] ?? ''), (string)($b['date'] ?? '')));
|
||||
usort($entries, static fn ($a, $b) => strcmp((string) ($a['date'] ?? ''), (string) ($b['date'] ?? '')));
|
||||
}
|
||||
}
|
||||
unset($studentEntries, $entries);
|
||||
@@ -644,9 +646,9 @@ class AttendanceQueryService
|
||||
->toArray();
|
||||
|
||||
foreach ($events as $event) {
|
||||
$d = substr((string)($event['date'] ?? ''), 0, 10);
|
||||
$d = substr((string) ($event['date'] ?? ''), 0, 10);
|
||||
if ($d !== '') {
|
||||
$noSchoolDays[$d] = trim((string)($event['title'] ?? 'No School')) ?: 'No School';
|
||||
$noSchoolDays[$d] = trim((string) ($event['title'] ?? 'No School')) ?: 'No School';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,12 +670,12 @@ class AttendanceQueryService
|
||||
foreach ($studentEntries as $studentId => $entries) {
|
||||
$statusByDate = [];
|
||||
foreach ($entries as $entry) {
|
||||
$d = substr((string)($entry['date'] ?? ''), 0, 10);
|
||||
$d = substr((string) ($entry['date'] ?? ''), 0, 10);
|
||||
if ($d === '' || empty($passedDatesSet[$d])) {
|
||||
continue;
|
||||
}
|
||||
$st = strtolower(trim((string)($entry['status'] ?? '')));
|
||||
if (!in_array($st, ['present', 'absent', 'late'], true)) {
|
||||
$st = strtolower(trim((string) ($entry['status'] ?? '')));
|
||||
if (! in_array($st, ['present', 'absent', 'late'], true)) {
|
||||
continue;
|
||||
}
|
||||
$statusByDate[$d] = $st;
|
||||
@@ -695,9 +697,9 @@ class AttendanceQueryService
|
||||
|
||||
$sum = $p + $l + $a;
|
||||
$record = $attendanceRecord[$secCode][$studentId] ?? [];
|
||||
$storedSum = (int)($record['total_presence'] ?? 0)
|
||||
+ (int)($record['total_late'] ?? 0)
|
||||
+ (int)($record['total_absence'] ?? 0);
|
||||
$storedSum = (int) ($record['total_presence'] ?? 0)
|
||||
+ (int) ($record['total_late'] ?? 0)
|
||||
+ (int) ($record['total_absence'] ?? 0);
|
||||
|
||||
if ($storedSum !== $totalPassedDays && $storedSum !== $sum) {
|
||||
DB::table('attendance_record')
|
||||
@@ -733,7 +735,7 @@ class AttendanceQueryService
|
||||
if (auth()->id()) {
|
||||
$u = $this->user->query()->select('firstname', 'lastname')->find(auth()->id());
|
||||
if ($u) {
|
||||
$adminName = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? ''));
|
||||
$adminName = trim(($u->firstname ?? '').' '.($u->lastname ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,8 +775,8 @@ class AttendanceQueryService
|
||||
return [];
|
||||
}
|
||||
|
||||
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
||||
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
||||
$staffDateInSql = 'DATE(`sa`.`date`) IN ('.
|
||||
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
||||
|
||||
$run = static function (
|
||||
bool $requireTcSemester,
|
||||
@@ -830,8 +832,8 @@ class AttendanceQueryService
|
||||
return [];
|
||||
}
|
||||
|
||||
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
||||
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
||||
$staffDateInSql = 'DATE(`sa`.`date`) IN ('.
|
||||
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
||||
|
||||
$ids = [];
|
||||
|
||||
@@ -905,7 +907,7 @@ class AttendanceQueryService
|
||||
*/
|
||||
private function calendarDateKey(mixed $value): string
|
||||
{
|
||||
$s = trim((string)($value ?? ''));
|
||||
$s = trim((string) ($value ?? ''));
|
||||
if ($s === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -25,16 +25,16 @@ class AttendanceRecordSyncService
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
if (! $record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = strtolower($newStatus);
|
||||
$oldStatus = strtolower((string)$oldStatus);
|
||||
$oldStatus = strtolower((string) $oldStatus);
|
||||
|
||||
$presence = (int)($record->total_presence ?? 0);
|
||||
$absence = (int)($record->total_absence ?? 0);
|
||||
$late = (int)($record->total_late ?? 0);
|
||||
$presence = (int) ($record->total_presence ?? 0);
|
||||
$absence = (int) ($record->total_absence ?? 0);
|
||||
$late = (int) ($record->total_late ?? 0);
|
||||
|
||||
if ($oldStatus === 'present') {
|
||||
$presence--;
|
||||
@@ -80,13 +80,14 @@ class AttendanceRecordSyncService
|
||||
|
||||
if ($record) {
|
||||
$record->update([
|
||||
'total_presence' => (int)($record->total_presence ?? 0) + ($status === 'present' ? 1 : 0),
|
||||
'total_absence' => (int)($record->total_absence ?? 0) + ($status === 'absent' ? 1 : 0),
|
||||
'total_late' => (int)($record->total_late ?? 0) + ($status === 'late' ? 1 : 0),
|
||||
'total_attendance' => (int)($record->total_attendance ?? 0) + 1,
|
||||
'total_presence' => (int) ($record->total_presence ?? 0) + ($status === 'present' ? 1 : 0),
|
||||
'total_absence' => (int) ($record->total_absence ?? 0) + ($status === 'absent' ? 1 : 0),
|
||||
'total_late' => (int) ($record->total_late ?? 0) + ($status === 'late' ? 1 : 0),
|
||||
'total_attendance' => (int) ($record->total_attendance ?? 0) + 1,
|
||||
'updated_at' => now(),
|
||||
'modified_by' => $modifiedBy,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,13 +142,13 @@ class AttendanceRecordSyncService
|
||||
return $record->update($payload);
|
||||
}
|
||||
|
||||
$payload['class_section_id'] = (int)$classSectionId;
|
||||
$payload['class_section_id'] = (int) $classSectionId;
|
||||
$payload['student_id'] = $studentId;
|
||||
$payload['school_id'] = $schoolId;
|
||||
$payload['semester'] = $semester;
|
||||
$payload['school_year'] = $schoolYear;
|
||||
$payload['created_at'] = now();
|
||||
|
||||
return (bool)AttendanceRecord::query()->create($payload);
|
||||
return (bool) AttendanceRecord::query()->create($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,24 +27,24 @@ class AttendanceService
|
||||
|
||||
public function currentSemester(): string
|
||||
{
|
||||
return (string)($this->configuration->getConfig('semester') ?? 'Fall');
|
||||
return (string) ($this->configuration->getConfig('semester') ?? 'Fall');
|
||||
}
|
||||
|
||||
public function currentSchoolYear(): string
|
||||
{
|
||||
return (string)($this->configuration->getConfig('school_year') ?? now()->year . '-' . (now()->year + 1));
|
||||
return (string) ($this->configuration->getConfig('school_year') ?? now()->year.'-'.(now()->year + 1));
|
||||
}
|
||||
|
||||
public function updateAttendanceManagement(Authenticatable $user, array $data): array
|
||||
{
|
||||
$classSectionId = (int)$data['class_section_id'];
|
||||
$studentId = (int)$data['student_id'];
|
||||
$status = strtolower((string)$data['status']);
|
||||
$date = (string)$data['date'];
|
||||
$classSectionId = (int) $data['class_section_id'];
|
||||
$studentId = (int) $data['student_id'];
|
||||
$status = strtolower((string) $data['status']);
|
||||
$date = (string) $data['date'];
|
||||
$reason = $data['reason'] ?? null;
|
||||
$classId = $data['class_id'] ?? null;
|
||||
$semester = (string)($data['semester'] ?? $this->currentSemester());
|
||||
$schoolYear = (string)($data['school_year'] ?? $this->currentSchoolYear());
|
||||
$semester = (string) ($data['semester'] ?? $this->currentSemester());
|
||||
$schoolYear = (string) ($data['school_year'] ?? $this->currentSchoolYear());
|
||||
|
||||
$dayRow = AttendanceDay::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
@@ -67,11 +67,11 @@ class AttendanceService
|
||||
'permissions' => method_exists($user, 'permissions') ? $user->permissions->pluck('name')->all() : [],
|
||||
];
|
||||
|
||||
if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
throw new RuntimeException('Attendance is locked for your role.');
|
||||
}
|
||||
|
||||
$reportedRaw = strtolower((string)($data['is_reported'] ?? ''));
|
||||
$reportedRaw = strtolower((string) ($data['is_reported'] ?? ''));
|
||||
$isReported = $status === 'present'
|
||||
? 'no'
|
||||
: (in_array($reportedRaw, ['1', 'yes', 'y', 'true', 'on'], true) ? 'yes' : 'no');
|
||||
@@ -98,7 +98,7 @@ class AttendanceService
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date' => $date,
|
||||
'class_id' => (int)$classId,
|
||||
'class_id' => (int) $classId,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
@@ -136,13 +136,13 @@ class AttendanceService
|
||||
|
||||
public function adminAddEntry(Authenticatable $user, array $data): array
|
||||
{
|
||||
$classSectionId = (int)$data['class_section_id'];
|
||||
$studentId = (int)$data['student_id'];
|
||||
$status = strtolower((string)$data['status']);
|
||||
$date = (string)($data['date'] ?? now()->toDateString());
|
||||
$classSectionId = (int) $data['class_section_id'];
|
||||
$studentId = (int) $data['student_id'];
|
||||
$status = strtolower((string) $data['status']);
|
||||
$date = (string) ($data['date'] ?? now()->toDateString());
|
||||
$reason = $data['reason'] ?? null;
|
||||
$isReported = in_array(strtolower((string)($data['is_reported'] ?? 'no')), ['yes', 'no'], true)
|
||||
? strtolower((string)($data['is_reported'] ?? 'no'))
|
||||
$isReported = in_array(strtolower((string) ($data['is_reported'] ?? 'no')), ['yes', 'no'], true)
|
||||
? strtolower((string) ($data['is_reported'] ?? 'no'))
|
||||
: 'no';
|
||||
|
||||
$section = $this->classSection
|
||||
@@ -152,7 +152,7 @@ class AttendanceService
|
||||
->orWhere('class_section_id', $classSectionId)
|
||||
->first();
|
||||
|
||||
$resolvedClassId = (int)($section->class_id ?? 0);
|
||||
$resolvedClassId = (int) ($section->class_id ?? 0);
|
||||
|
||||
if ($resolvedClassId <= 0) {
|
||||
throw new RuntimeException('Cannot resolve class_id for this section.');
|
||||
@@ -210,29 +210,32 @@ class AttendanceService
|
||||
$reportedRaw = $row['is_reported'] ?? ($row['reported'] ?? '');
|
||||
$row['is_reported'] = $this->normalizeReportedFlag($reportedRaw);
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function normalizeStatus(?string $status): string
|
||||
{
|
||||
$status = strtolower(trim((string)$status));
|
||||
$status = strtolower(trim((string) $status));
|
||||
|
||||
return in_array($status, ['present', 'absent', 'late'], true) ? $status : $status;
|
||||
}
|
||||
|
||||
public function normalizeReportedFlag(mixed $flag): string
|
||||
{
|
||||
$v = strtolower(trim((string)$flag));
|
||||
$v = strtolower(trim((string) $flag));
|
||||
|
||||
return in_array($v, ['yes', '1', 'true', 'y', 'on'], true) ? 'yes' : 'no';
|
||||
}
|
||||
|
||||
public function detectExternalSubmission(?array $row): array
|
||||
{
|
||||
if (empty($row) || !is_array($row)) {
|
||||
if (empty($row) || ! is_array($row)) {
|
||||
return ['locked' => false, 'source' => null];
|
||||
}
|
||||
|
||||
$reportedRaw = strtolower((string)($row['is_reported'] ?? ''));
|
||||
$reason = strtolower((string)($row['reason'] ?? ''));
|
||||
$reportedRaw = strtolower((string) ($row['is_reported'] ?? ''));
|
||||
$reason = strtolower((string) ($row['reason'] ?? ''));
|
||||
|
||||
$isParent = in_array($reportedRaw, ['yes', '1', 'true', 'y'], true)
|
||||
|| str_contains($reason, 'parent-reported')
|
||||
@@ -242,7 +245,7 @@ class AttendanceService
|
||||
return ['locked' => true, 'source' => 'parent'];
|
||||
}
|
||||
|
||||
$modifierId = (int)($row['modified_by'] ?? 0);
|
||||
$modifierId = (int) ($row['modified_by'] ?? 0);
|
||||
if ($modifierId > 0 && $this->isAdminLikeUserId($modifierId)) {
|
||||
return ['locked' => true, 'source' => 'admin'];
|
||||
}
|
||||
@@ -264,12 +267,12 @@ class AttendanceService
|
||||
$excluded = ['parent', 'guest', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta'];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$name = strtolower(str_replace([' ', '-'], '_', (string)($role['role_name'] ?? '')));
|
||||
if ($name !== '' && !in_array($name, $excluded, true)) {
|
||||
$name = strtolower(str_replace([' ', '-'], '_', (string) ($role['role_name'] ?? '')));
|
||||
if ($name !== '' && ! in_array($name, $excluded, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ class AttendanceSummaryRebuildService
|
||||
$semester = (string) ($row['semester'] ?? '');
|
||||
$status = strtolower((string) ($row['status'] ?? ''));
|
||||
|
||||
$key = $studentId . '-' . $schoolYear . '-' . $semester;
|
||||
if (!isset($summary[$key])) {
|
||||
$key = $studentId.'-'.$schoolYear.'-'.$semester;
|
||||
if (! isset($summary[$key])) {
|
||||
$summary[$key] = [
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
|
||||
@@ -11,16 +11,16 @@ class LateSlipLogQueryService
|
||||
{
|
||||
$query = LateSlipLog::query();
|
||||
|
||||
if (!empty($filters['school_year'])) {
|
||||
if (! empty($filters['school_year'])) {
|
||||
$query->where('school_year', (string) $filters['school_year']);
|
||||
}
|
||||
|
||||
if (!empty($filters['semester'])) {
|
||||
if (! empty($filters['semester'])) {
|
||||
$query->where('semester', (string) $filters['semester']);
|
||||
}
|
||||
|
||||
if (!empty($filters['q'])) {
|
||||
$query->where('student_name', 'like', '%' . $filters['q'] . '%');
|
||||
if (! empty($filters['q'])) {
|
||||
$query->where('student_name', 'like', '%'.$filters['q'].'%');
|
||||
}
|
||||
|
||||
$dateFrom = $this->toDbDate($filters['date_from'] ?? null);
|
||||
@@ -58,6 +58,7 @@ class LateSlipLogQueryService
|
||||
return null;
|
||||
}
|
||||
$ts = strtotime($value);
|
||||
|
||||
return $ts ? date('Y-m-d', $ts) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class SemesterRangeService
|
||||
{
|
||||
public function normalizeSemester(?string $semester): string
|
||||
{
|
||||
$value = strtolower(trim((string)$semester));
|
||||
$value = strtolower(trim((string) $semester));
|
||||
|
||||
return match ($value) {
|
||||
'fall', 'autumn' => 'Fall',
|
||||
@@ -76,7 +76,7 @@ class SemesterRangeService
|
||||
protected function parseSchoolYear(string $schoolYear): array
|
||||
{
|
||||
if (preg_match('/^\s*(\d{4})\s*-\s*(\d{4})\s*$/', $schoolYear, $m)) {
|
||||
return [(int)$m[1], (int)$m[2]];
|
||||
return [(int) $m[1], (int) $m[2]];
|
||||
}
|
||||
|
||||
$configured = trim((string) Configuration::getConfig('school_year'));
|
||||
@@ -85,7 +85,7 @@ class SemesterRangeService
|
||||
}
|
||||
|
||||
$year = (int) date('Y');
|
||||
$derived = $year . '-' . ($year + 1);
|
||||
$derived = $year.'-'.($year + 1);
|
||||
Configuration::setConfigValueByKey('school_year', $derived);
|
||||
|
||||
return [$year, $year + 1];
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Controllers\View\AttendanceController;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\StaffAttendance;
|
||||
use App\Models\TeacherClass;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use App\Models\ClassSection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
@@ -23,7 +24,7 @@ class StaffAttendanceService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* legacy {@see \App\Controllers\View\AttendanceController::index} — single date / section teacher grid JSON.
|
||||
* legacy {@see AttendanceController::index} — single date / section teacher grid JSON.
|
||||
*/
|
||||
public function teacherAttendanceDayIndex(?string $semester, ?string $schoolYear, string $dateYmd, int $sectionCode): array
|
||||
{
|
||||
@@ -44,7 +45,7 @@ class StaffAttendanceService
|
||||
->where('class_section_id', $code)
|
||||
->value('class_section_name');
|
||||
$name = trim((string) $name);
|
||||
$labels[$code] = $name !== '' ? $name : ('Section #' . $code);
|
||||
$labels[$code] = $name !== '' ? $name : ('Section #'.$code);
|
||||
}
|
||||
|
||||
$grid = [];
|
||||
@@ -76,7 +77,7 @@ class StaffAttendanceService
|
||||
$tid = (int) ($t['teacher_id'] ?? 0);
|
||||
$posRaw = strtolower((string) ($t['position'] ?? 'main'));
|
||||
$pos = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main';
|
||||
$name = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? '')) ?: ('User#' . $tid);
|
||||
$name = trim(($t['firstname'] ?? '').' '.($t['lastname'] ?? '')) ?: ('User#'.$tid);
|
||||
|
||||
$cell = $statusMap[$tid] ?? ['status' => null, 'reason' => null];
|
||||
|
||||
@@ -108,7 +109,7 @@ class StaffAttendanceService
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk save for legacy {@see \App\Controllers\View\AttendanceController::save} (method missing in legacy; implemented here).
|
||||
* Bulk save for legacy {@see AttendanceController::save} (method missing in legacy; implemented here).
|
||||
*/
|
||||
public function saveTeacherAttendanceBulk(Authenticatable $user, array $payload): array
|
||||
{
|
||||
@@ -181,7 +182,7 @@ class StaffAttendanceService
|
||||
|
||||
public function monthData(?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$semester = $semester ?: (string) $this->configuration->getConfig('semester');
|
||||
[
|
||||
'school_year' => $schoolYear,
|
||||
'current_year' => $currentYear,
|
||||
@@ -232,7 +233,7 @@ class StaffAttendanceService
|
||||
$sectionCodes = array_keys($assignedBySection);
|
||||
$labels = [];
|
||||
|
||||
if (!empty($sectionCodes)) {
|
||||
if (! empty($sectionCodes)) {
|
||||
$rows = $this->classSection->query()
|
||||
->select(['class_section_id', 'class_section_name'])
|
||||
->whereIn('class_section_id', $sectionCodes)
|
||||
@@ -240,20 +241,20 @@ class StaffAttendanceService
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$labels[(int)$row['class_section_id']] = trim((string)$row['class_section_name']) ?: 'Section #' . (int)$row['class_section_id'];
|
||||
$labels[(int) $row['class_section_id']] = trim((string) $row['class_section_name']) ?: 'Section #'.(int) $row['class_section_id'];
|
||||
}
|
||||
}
|
||||
|
||||
$teacherIds = [];
|
||||
foreach ($assignedBySection as $rows) {
|
||||
foreach ($rows as $row) {
|
||||
$teacherIds[(int)$row['teacher_id']] = true;
|
||||
$teacherIds[(int) $row['teacher_id']] = true;
|
||||
}
|
||||
}
|
||||
$teacherIds = array_keys($teacherIds);
|
||||
|
||||
$statusByTeacher = [];
|
||||
if (!empty($teacherIds) && !empty($days)) {
|
||||
if (! empty($teacherIds) && ! empty($days)) {
|
||||
$query = DB::table('staff_attendance')
|
||||
->select('user_id', 'date', 'status', 'reason')
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -267,8 +268,8 @@ class StaffAttendanceService
|
||||
$rows = $query->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statusByTeacher[(int)$row->user_id][(string)$row->date] = [
|
||||
'status' => strtolower((string)$row->status),
|
||||
$statusByTeacher[(int) $row->user_id][(string) $row->date] = [
|
||||
'status' => strtolower((string) $row->status),
|
||||
'reason' => $row->reason,
|
||||
];
|
||||
}
|
||||
@@ -284,7 +285,7 @@ class StaffAttendanceService
|
||||
|
||||
$noSchoolDays = [];
|
||||
foreach ($noSchoolRows as $row) {
|
||||
$noSchoolDays[] = (string)$row->date;
|
||||
$noSchoolDays[] = (string) $row->date;
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
@@ -292,9 +293,9 @@ class StaffAttendanceService
|
||||
$teachersPayload = [];
|
||||
|
||||
foreach (($assignedBySection[$code] ?? []) as $row) {
|
||||
$teacherId = (int)$row['teacher_id'];
|
||||
$name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')) ?: ('User#' . $teacherId);
|
||||
$position = strtolower((string)($row['position'] ?? 'main'));
|
||||
$teacherId = (int) $row['teacher_id'];
|
||||
$name = trim(($row['firstname'] ?? '').' '.($row['lastname'] ?? '')) ?: ('User#'.$teacherId);
|
||||
$position = strtolower((string) ($row['position'] ?? 'main'));
|
||||
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
||||
|
||||
$cells = [];
|
||||
@@ -302,7 +303,7 @@ class StaffAttendanceService
|
||||
|
||||
foreach ($days as $date) {
|
||||
$cell = $statusByTeacher[$teacherId][$date] ?? null;
|
||||
if (!$cell || empty($cell['status'])) {
|
||||
if (! $cell || empty($cell['status'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -327,15 +328,15 @@ class StaffAttendanceService
|
||||
}
|
||||
|
||||
$sections[] = [
|
||||
'section_code' => (int)$code,
|
||||
'label' => $labels[$code] ?? ('Section #' . (int)$code),
|
||||
'section_code' => (int) $code,
|
||||
'label' => $labels[$code] ?? ('Section #'.(int) $code),
|
||||
'teachers' => $teachersPayload,
|
||||
];
|
||||
}
|
||||
|
||||
$adminStatusByUser = [];
|
||||
$adminIds = [];
|
||||
if (!empty($days)) {
|
||||
if (! empty($days)) {
|
||||
$query = DB::table('staff_attendance')
|
||||
->select('user_id', 'date', 'status', 'reason')
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -361,7 +362,7 @@ class StaffAttendanceService
|
||||
|
||||
$adminIds = array_keys($adminIds);
|
||||
$admins = [];
|
||||
if (!empty($adminIds)) {
|
||||
if (! empty($adminIds)) {
|
||||
$rolesRows = DB::table('user_roles as ur')
|
||||
->select('ur.user_id', 'r.name as role_name', 'r.slug as role_slug', 'r.priority')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
@@ -389,20 +390,20 @@ class StaffAttendanceService
|
||||
|
||||
$namesByUser = [];
|
||||
foreach ($userRows as $row) {
|
||||
$namesByUser[(int) $row->id] = trim(((string) ($row->firstname ?? '')) . ' ' . ((string) ($row->lastname ?? '')));
|
||||
$namesByUser[(int) $row->id] = trim(((string) ($row->firstname ?? '')).' '.((string) ($row->lastname ?? '')));
|
||||
}
|
||||
|
||||
foreach ($adminIds as $uid) {
|
||||
$picked = null;
|
||||
foreach (($rolesByUser[$uid] ?? []) as $role) {
|
||||
$slug = strtolower((string) ($role['role_slug'] ?? $role['role_name'] ?? ''));
|
||||
if (!in_array($slug, $excludeSlugs, true)) {
|
||||
if (! in_array($slug, $excludeSlugs, true)) {
|
||||
$picked = $role;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$picked) {
|
||||
if (! $picked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -410,7 +411,7 @@ class StaffAttendanceService
|
||||
$cells = [];
|
||||
foreach ($days as $date) {
|
||||
$cell = $adminStatusByUser[$uid][$date] ?? null;
|
||||
if (!$cell || empty($cell['status'])) {
|
||||
if (! $cell || empty($cell['status'])) {
|
||||
continue;
|
||||
}
|
||||
$cells[$date] = $cell;
|
||||
@@ -425,7 +426,7 @@ class StaffAttendanceService
|
||||
|
||||
$admins[] = [
|
||||
'user_id' => $uid,
|
||||
'name' => $namesByUser[$uid] !== '' ? $namesByUser[$uid] : ('User#' . $uid),
|
||||
'name' => $namesByUser[$uid] !== '' ? $namesByUser[$uid] : ('User#'.$uid),
|
||||
'role' => (string) ($picked['role_name'] ?? 'Admin'),
|
||||
'cells' => $cells,
|
||||
'totals' => $totals,
|
||||
@@ -508,7 +509,7 @@ class StaffAttendanceService
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentYear !== '' && !in_array($currentYear, $schoolYears, true)) {
|
||||
if ($currentYear !== '' && ! in_array($currentYear, $schoolYears, true)) {
|
||||
array_unshift($schoolYears, $currentYear);
|
||||
}
|
||||
|
||||
@@ -523,7 +524,7 @@ class StaffAttendanceService
|
||||
? $requestedSchoolYear
|
||||
: ($currentYear !== '' ? $currentYear : ($schoolYears[0] ?? ''));
|
||||
|
||||
if ($resolvedYear !== '' && !in_array($resolvedYear, $schoolYears, true)) {
|
||||
if ($resolvedYear !== '' && ! in_array($resolvedYear, $schoolYears, true)) {
|
||||
array_unshift($schoolYears, $resolvedYear);
|
||||
}
|
||||
|
||||
@@ -539,7 +540,7 @@ class StaffAttendanceService
|
||||
protected function buildRangeLabel(string $schoolYear, string $semesterNorm, string $rangeStart, string $rangeEnd): string
|
||||
{
|
||||
if ($semesterNorm === '') {
|
||||
return 'School Year ' . $schoolYear;
|
||||
return 'School Year '.$schoolYear;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
@@ -553,14 +554,14 @@ class StaffAttendanceService
|
||||
public function adminsAttendanceData(?string $date, ?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
$date = $date ?: now()->toDateString();
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year');
|
||||
$semester = $semester ?: (string) $this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string) $this->configuration->getConfig('school_year');
|
||||
|
||||
$allStaff = $this->staffAttendance->staffWithStatusForDay($date, $semester, $schoolYear, []);
|
||||
$userIds = [];
|
||||
|
||||
foreach ($allStaff as $row) {
|
||||
$uid = (int)($row['user_id'] ?? 0);
|
||||
$uid = (int) ($row['user_id'] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$userIds[$uid] = true;
|
||||
}
|
||||
@@ -577,19 +578,19 @@ class StaffAttendanceService
|
||||
|
||||
$rolesByUser = [];
|
||||
foreach ($rolesRows as $row) {
|
||||
$rolesByUser[(int)$row->user_id][] = (array)$row;
|
||||
$rolesByUser[(int) $row->user_id][] = (array) $row;
|
||||
}
|
||||
|
||||
foreach ($rolesByUser as &$list) {
|
||||
usort($list, fn($a, $b) => ((int)($a['priority'] ?? 100)) <=> ((int)($b['priority'] ?? 100)));
|
||||
usort($list, fn ($a, $b) => ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100)));
|
||||
}
|
||||
|
||||
$excludeSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher'];
|
||||
|
||||
$staffByUser = [];
|
||||
foreach ($allStaff as $row) {
|
||||
$uid = (int)($row['user_id'] ?? 0);
|
||||
if ($uid > 0 && !isset($staffByUser[$uid])) {
|
||||
$uid = (int) ($row['user_id'] ?? 0);
|
||||
if ($uid > 0 && ! isset($staffByUser[$uid])) {
|
||||
$staffByUser[$uid] = $row;
|
||||
}
|
||||
}
|
||||
@@ -600,14 +601,14 @@ class StaffAttendanceService
|
||||
$picked = null;
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$slug = strtolower((string)($role['role_slug'] ?? $role['role_name'] ?? ''));
|
||||
if (!in_array($slug, $excludeSlugs, true)) {
|
||||
$slug = strtolower((string) ($role['role_slug'] ?? $role['role_name'] ?? ''));
|
||||
if (! in_array($slug, $excludeSlugs, true)) {
|
||||
$picked = $role;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$picked) {
|
||||
if (! $picked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -618,8 +619,9 @@ class StaffAttendanceService
|
||||
}
|
||||
|
||||
usort($adminsGrid, function ($a, $b) {
|
||||
$an = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? ''));
|
||||
$bn = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? ''));
|
||||
$an = trim(($a['firstname'] ?? '').' '.($a['lastname'] ?? ''));
|
||||
$bn = trim(($b['firstname'] ?? '').' '.($b['lastname'] ?? ''));
|
||||
|
||||
return strcasecmp($an, $bn);
|
||||
});
|
||||
|
||||
@@ -633,20 +635,20 @@ class StaffAttendanceService
|
||||
|
||||
public function saveAdmins(Authenticatable $user, array $data): array
|
||||
{
|
||||
$date = (string)$data['date'];
|
||||
$semester = (string)$data['semester'];
|
||||
$schoolYear = (string)$data['school_year'];
|
||||
$admins = (array)($data['admins'] ?? $data['staff'] ?? []);
|
||||
$date = (string) $data['date'];
|
||||
$semester = (string) $data['semester'];
|
||||
$schoolYear = (string) $data['school_year'];
|
||||
$admins = (array) ($data['admins'] ?? $data['staff'] ?? []);
|
||||
$allowedStatuses = ['present', 'absent', 'late'];
|
||||
$excludedRoleSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant'];
|
||||
|
||||
DB::transaction(function () use ($admins, $date, $semester, $schoolYear, $allowedStatuses, $excludedRoleSlugs) {
|
||||
foreach ($admins as $uid => $row) {
|
||||
$uid = (int)$uid;
|
||||
$status = strtolower(trim((string)($row['status'] ?? '')));
|
||||
$reason = trim((string)($row['reason'] ?? ''));
|
||||
$roleName = (string)($row['role_name'] ?? '');
|
||||
$roleSlug = strtolower(str_replace(['-', ' '], '_', (string)($row['role_slug'] ?? $roleName)));
|
||||
$uid = (int) $uid;
|
||||
$status = strtolower(trim((string) ($row['status'] ?? '')));
|
||||
$reason = trim((string) ($row['reason'] ?? ''));
|
||||
$roleName = (string) ($row['role_name'] ?? '');
|
||||
$roleSlug = strtolower(str_replace(['-', ' '], '_', (string) ($row['role_slug'] ?? $roleName)));
|
||||
|
||||
if (in_array($roleSlug, $excludedRoleSlugs, true)) {
|
||||
continue;
|
||||
@@ -659,10 +661,11 @@ class StaffAttendanceService
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($status, $allowedStatuses, true)) {
|
||||
if (! in_array($status, $allowedStatuses, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -692,13 +695,13 @@ class StaffAttendanceService
|
||||
|
||||
public function saveCell(Authenticatable $user, array $data): array
|
||||
{
|
||||
$date = (string)$data['date'];
|
||||
$status = (string)($data['status'] ?? '');
|
||||
$targetId = (int)$data['user_id'];
|
||||
$roleName = trim((string)($data['role_name'] ?? ''));
|
||||
$semester = (string)$data['semester'];
|
||||
$schoolYear = (string)$data['school_year'];
|
||||
$reason = trim((string)($data['reason'] ?? ''));
|
||||
$date = (string) $data['date'];
|
||||
$status = (string) ($data['status'] ?? '');
|
||||
$targetId = (int) $data['user_id'];
|
||||
$roleName = trim((string) ($data['role_name'] ?? ''));
|
||||
$semester = (string) $data['semester'];
|
||||
$schoolYear = (string) $data['school_year'];
|
||||
$reason = trim((string) ($data['reason'] ?? ''));
|
||||
|
||||
if ($status === '') {
|
||||
DB::table('staff_attendance')
|
||||
@@ -732,9 +735,9 @@ class StaffAttendanceService
|
||||
|
||||
public function monthCsv(?string $semester, ?string $schoolYear, ?string $scope): StreamedResponse
|
||||
{
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year');
|
||||
$scope = strtolower((string)$scope);
|
||||
$semester = $semester ?: (string) $this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string) $this->configuration->getConfig('school_year');
|
||||
$scope = strtolower((string) $scope);
|
||||
|
||||
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
|
||||
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
|
||||
@@ -748,9 +751,9 @@ class StaffAttendanceService
|
||||
$daysList = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd);
|
||||
|
||||
$filename = ($scope === 'admins' ? 'admin_attendance_' : 'teacher_attendance_')
|
||||
. $semester . '_' . str_replace('/', '-', $schoolYear) . '.csv';
|
||||
.$semester.'_'.str_replace('/', '-', $schoolYear).'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($semester, $schoolYear, $scope, $daysList, $semesterNorm, $rangeStart, $rangeEnd) {
|
||||
return response()->streamDownload(function () use ($schoolYear, $scope, $semesterNorm, $rangeStart, $rangeEnd) {
|
||||
$out = fopen('php://output', 'w');
|
||||
|
||||
if ($scope === 'admins') {
|
||||
@@ -770,12 +773,13 @@ class StaffAttendanceService
|
||||
fputcsv($out, [
|
||||
$row->user_id,
|
||||
$row->date,
|
||||
strtoupper(substr((string)$row->status, 0, 1)),
|
||||
strtoupper(substr((string) $row->status, 0, 1)),
|
||||
$row->reason,
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -795,7 +799,7 @@ class StaffAttendanceService
|
||||
fputcsv($out, [
|
||||
$row->user_id,
|
||||
$row->date,
|
||||
strtoupper(substr((string)$row->status, 0, 1)),
|
||||
strtoupper(substr((string) $row->status, 0, 1)),
|
||||
$row->reason,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class StudentAttendanceWriterService
|
||||
|
||||
public function resolveStudentSchoolId(int $studentId, ?string $studentSchoolId = null): string
|
||||
{
|
||||
$studentSchoolId = trim((string)$studentSchoolId);
|
||||
$studentSchoolId = trim((string) $studentSchoolId);
|
||||
|
||||
if ($studentSchoolId !== '') {
|
||||
return $studentSchoolId;
|
||||
@@ -78,25 +78,25 @@ class StudentAttendanceWriterService
|
||||
|
||||
$studentRow = $this->student->query()->select('school_id')->find($studentId);
|
||||
|
||||
if (!$studentRow || empty($studentRow->school_id)) {
|
||||
if (! $studentRow || empty($studentRow->school_id)) {
|
||||
throw new RuntimeException('Student school ID not found.');
|
||||
}
|
||||
|
||||
return (string)$studentRow->school_id;
|
||||
return (string) $studentRow->school_id;
|
||||
}
|
||||
|
||||
public function saveOrUpdateStudentAttendance(array $payload): array
|
||||
{
|
||||
$classSectionId = (int)$payload['class_section_id'];
|
||||
$studentId = (int)$payload['student_id'];
|
||||
$classSectionId = (int) $payload['class_section_id'];
|
||||
$studentId = (int) $payload['student_id'];
|
||||
$studentSchoolId = $this->resolveStudentSchoolId($studentId, $payload['school_id'] ?? null);
|
||||
$status = strtolower((string)$payload['status']);
|
||||
$status = strtolower((string) $payload['status']);
|
||||
$reason = $payload['reason'] ?? null;
|
||||
$reported = $payload['is_reported'] ?? null;
|
||||
$semester = (string)$payload['semester'];
|
||||
$schoolYear = (string)$payload['school_year'];
|
||||
$date = (string)$payload['date'];
|
||||
$classId = (int)$payload['class_id'];
|
||||
$semester = (string) $payload['semester'];
|
||||
$schoolYear = (string) $payload['school_year'];
|
||||
$date = (string) $payload['date'];
|
||||
$classId = (int) $payload['class_id'];
|
||||
$userId = $payload['user_id'] ?? null;
|
||||
|
||||
$existing = AttendanceData::query()
|
||||
@@ -110,7 +110,7 @@ class StudentAttendanceWriterService
|
||||
$oldStatus = $existing->status;
|
||||
|
||||
$this->updateAttendanceData(
|
||||
(int)$existing->id,
|
||||
(int) $existing->id,
|
||||
$status,
|
||||
$reason,
|
||||
$reported,
|
||||
@@ -131,7 +131,7 @@ class StudentAttendanceWriterService
|
||||
|
||||
return [
|
||||
'mode' => 'updated',
|
||||
'attendance_id' => (int)$existing->id,
|
||||
'attendance_id' => (int) $existing->id,
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $status,
|
||||
'school_id' => $studentSchoolId,
|
||||
@@ -172,10 +172,10 @@ class StudentAttendanceWriterService
|
||||
|
||||
return [
|
||||
'mode' => 'created',
|
||||
'attendance_id' => (int)($created->id ?? 0),
|
||||
'attendance_id' => (int) ($created->id ?? 0),
|
||||
'old_status' => null,
|
||||
'new_status' => $status,
|
||||
'school_id' => $studentSchoolId,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class TeacherAttendanceSubmissionService
|
||||
callable $currentSemester,
|
||||
callable $currentSchoolYear
|
||||
): array {
|
||||
$rawSection = trim((string)$data['class_section_id']);
|
||||
$rawSection = trim((string) $data['class_section_id']);
|
||||
|
||||
if ($rawSection === '') {
|
||||
throw new RuntimeException('Missing or invalid class section.');
|
||||
@@ -42,36 +42,36 @@ class TeacherAttendanceSubmissionService
|
||||
->where('class_section_id', $rawSection)
|
||||
->first();
|
||||
|
||||
if (!$sectionRow && ctype_digit($rawSection)) {
|
||||
if (! $sectionRow && ctype_digit($rawSection)) {
|
||||
$sectionRow = DB::table('classSection')
|
||||
->select('id', 'class_id', 'class_section_id')
|
||||
->where('id', (int)$rawSection)
|
||||
->where('id', (int) $rawSection)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$sectionRow) {
|
||||
if (! $sectionRow) {
|
||||
throw new RuntimeException('Invalid class section.');
|
||||
}
|
||||
|
||||
$classSectionId = (int)$sectionRow->class_section_id;
|
||||
$classSectionPk = (int)($sectionRow->id ?? 0);
|
||||
$classSectionId = (int) $sectionRow->class_section_id;
|
||||
$classSectionPk = (int) ($sectionRow->id ?? 0);
|
||||
$alsoSectionId = ($classSectionPk > 0 && $classSectionPk !== $classSectionId) ? $classSectionPk : null;
|
||||
$classId = (int)$sectionRow->class_id;
|
||||
$classId = (int) $sectionRow->class_id;
|
||||
$attendanceData = $data['attendance'] ?? [];
|
||||
$teachersData = $data['teachers'] ?? [];
|
||||
|
||||
if (empty($attendanceData) || !is_array($attendanceData)) {
|
||||
if (empty($attendanceData) || ! is_array($attendanceData)) {
|
||||
throw new RuntimeException('No student attendance data provided.');
|
||||
}
|
||||
|
||||
if (empty($teachersData) || !is_array($teachersData)) {
|
||||
if (empty($teachersData) || ! is_array($teachersData)) {
|
||||
throw new RuntimeException('No teacher attendance data provided.');
|
||||
}
|
||||
|
||||
$rawDate = $data['date'] ?? now()->toDateString();
|
||||
$attendDate = Carbon::parse($rawDate)->toDateString();
|
||||
$semester = (string)$currentSemester();
|
||||
$schoolYear = (string)$currentSchoolYear();
|
||||
$semester = (string) $currentSemester();
|
||||
$schoolYear = (string) $currentSchoolYear();
|
||||
|
||||
$dayRow = AttendanceDay::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
@@ -94,7 +94,7 @@ class TeacherAttendanceSubmissionService
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
|
||||
if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
throw new RuntimeException('Attendance is locked for your role.');
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class TeacherAttendanceSubmissionService
|
||||
|
||||
$existingByStudent = [];
|
||||
foreach ($existingRows as $row) {
|
||||
$existingByStudent[(int)$row->student_id] = $row->toArray();
|
||||
$existingByStudent[(int) $row->student_id] = $row->toArray();
|
||||
}
|
||||
|
||||
$allowedStatuses = ['present', 'absent', 'late'];
|
||||
@@ -129,10 +129,10 @@ class TeacherAttendanceSubmissionService
|
||||
}
|
||||
|
||||
foreach ($assignedTeachers as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$status = strtolower((string)($teachersData[$teacherId]['status'] ?? ''));
|
||||
$teacherId = (int) $teacher['teacher_id'];
|
||||
$status = strtolower((string) ($teachersData[$teacherId]['status'] ?? ''));
|
||||
|
||||
if (!in_array($status, $allowedStatuses, true)) {
|
||||
if (! in_array($status, $allowedStatuses, true)) {
|
||||
throw new RuntimeException('Please fill teacher attendance for all assigned teachers.');
|
||||
}
|
||||
}
|
||||
@@ -168,13 +168,13 @@ class TeacherAttendanceSubmissionService
|
||||
);
|
||||
|
||||
foreach ($assignedTeachers as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
||||
$teacherId = (int) $teacher['teacher_id'];
|
||||
$position = strtolower((string) ($teacher['position'] ?? 'main'));
|
||||
$position = $position === 'ta' ? 'ta' : 'main';
|
||||
|
||||
$staffPayload = [
|
||||
'role_name' => 'Teacher',
|
||||
'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')),
|
||||
'status' => strtolower((string) ($teachersData[$teacherId]['status'] ?? 'present')),
|
||||
'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
@@ -198,9 +198,9 @@ class TeacherAttendanceSubmissionService
|
||||
}
|
||||
|
||||
foreach ($attendanceData as $row) {
|
||||
$studentId = (int)($row['student_id'] ?? 0);
|
||||
$status = strtolower(trim((string)($row['status'] ?? '')));
|
||||
$reason = trim((string)($row['reason'] ?? ''));
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$status = strtolower(trim((string) ($row['status'] ?? '')));
|
||||
$reason = trim((string) ($row['reason'] ?? ''));
|
||||
$existing = $existingByStudent[$studentId] ?? null;
|
||||
|
||||
if ($isTeacher && $existing) {
|
||||
|
||||
@@ -7,17 +7,21 @@ use App\Models\LateSlipLog;
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AttendanceManagementService
|
||||
{
|
||||
public const STATUS_PRESENT = 'present';
|
||||
|
||||
public const STATUS_ABSENT_PENDING = 'absent_pending_verification';
|
||||
|
||||
public const STATUS_ABSENT = 'absent';
|
||||
|
||||
public const STATUS_LATE = 'late';
|
||||
|
||||
public const STATUS_EARLY_DISMISSAL = 'early_dismissal';
|
||||
|
||||
public const STATUS_LATE_WITHOUT_SLIP = 'late_without_slip';
|
||||
|
||||
public function dashboard(array $filters = []): array
|
||||
@@ -204,6 +208,7 @@ class AttendanceManagementService
|
||||
if ($person['type'] === 'student' && $status === self::STATUS_LATE) {
|
||||
$row['late_slip'] = $this->printLateSlip($row, $actor);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -253,6 +258,7 @@ class AttendanceManagementService
|
||||
if (($data['exit_method'] ?? '') === 'manual_exit') {
|
||||
$this->recordBadgeException((int) $id, $person, $exitAt, 'manual exit entry', $badgeExceptions, $actor, $data['notes'] ?? null);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -292,6 +298,7 @@ class AttendanceManagementService
|
||||
];
|
||||
$id = DB::table('attendance_management_events')->insertGetId($row);
|
||||
$row['id'] = $id;
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -312,6 +319,7 @@ class AttendanceManagementService
|
||||
$update['report_status'] = $this->normalizeReportStatus($data['report_status']);
|
||||
}
|
||||
DB::table('attendance_management_events')->where('id', $id)->update($update);
|
||||
|
||||
return (array) DB::table('attendance_management_events')->where('id', $id)->first();
|
||||
}
|
||||
|
||||
@@ -347,6 +355,7 @@ class AttendanceManagementService
|
||||
'final_decision' => 'Late slip reprinted',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -367,12 +376,24 @@ class AttendanceManagementService
|
||||
|
||||
$student = null;
|
||||
$user = null;
|
||||
if ($type === 'student' && $id > 0) $student = Student::query()->find($id);
|
||||
if (in_array($type, ['staff', 'user', 'teacher', 'administrator'], true) && $id > 0) $user = User::query()->find($id);
|
||||
if (! $student && $badge !== '') $student = Student::query()->where('rfid_tag', $badge)->first();
|
||||
if (! $student && ! $user && $badge !== '') $user = User::query()->where('rfid_tag', $badge)->first();
|
||||
if (! $student && ! $user && $id > 0) $student = Student::query()->find($id);
|
||||
if (! $student && ! $user && $id > 0) $user = User::query()->find($id);
|
||||
if ($type === 'student' && $id > 0) {
|
||||
$student = Student::query()->find($id);
|
||||
}
|
||||
if (in_array($type, ['staff', 'user', 'teacher', 'administrator'], true) && $id > 0) {
|
||||
$user = User::query()->find($id);
|
||||
}
|
||||
if (! $student && $badge !== '') {
|
||||
$student = Student::query()->where('rfid_tag', $badge)->first();
|
||||
}
|
||||
if (! $student && ! $user && $badge !== '') {
|
||||
$user = User::query()->where('rfid_tag', $badge)->first();
|
||||
}
|
||||
if (! $student && ! $user && $id > 0) {
|
||||
$student = Student::query()->find($id);
|
||||
}
|
||||
if (! $student && ! $user && $id > 0) {
|
||||
$user = User::query()->find($id);
|
||||
}
|
||||
|
||||
if ($student) {
|
||||
return [
|
||||
@@ -397,6 +418,7 @@ class AttendanceManagementService
|
||||
if ($name === '') {
|
||||
throw new \InvalidArgumentException('Person could not be resolved. Provide person_id, badge_id, or person_name.');
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $type !== '' ? $type : 'visitor',
|
||||
'id' => $id > 0 ? $id : null,
|
||||
@@ -419,6 +441,7 @@ class AttendanceManagementService
|
||||
'admin_name' => $actor ? trim(($actor->firstname ?? '').' '.($actor->lastname ?? '')) : 'System',
|
||||
];
|
||||
$logged = LateSlipLog::logSlip($payload, $actor?->id);
|
||||
|
||||
return [
|
||||
'printed' => $logged,
|
||||
'slip_number' => 'AME-'.($event['id'] ?? time()),
|
||||
@@ -458,9 +481,16 @@ class AttendanceManagementService
|
||||
$absence = (clone $q)->where('attendance_status', self::STATUS_ABSENT)->count();
|
||||
$late = (clone $q)->whereIn('attendance_status', [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP])->count();
|
||||
$ed = (clone $q)->where('attendance_status', self::STATUS_EARLY_DISMISSAL)->count();
|
||||
if ($newStatus === self::STATUS_ABSENT) $absence++;
|
||||
if (in_array($newStatus, [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP], true)) $late++;
|
||||
if ($newStatus === self::STATUS_EARLY_DISMISSAL) $ed++;
|
||||
if ($newStatus === self::STATUS_ABSENT) {
|
||||
$absence++;
|
||||
}
|
||||
if (in_array($newStatus, [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP], true)) {
|
||||
$late++;
|
||||
}
|
||||
if ($newStatus === self::STATUS_EARLY_DISMISSAL) {
|
||||
$ed++;
|
||||
}
|
||||
|
||||
return ['absence_count' => $absence, 'late_count' => $late, 'early_dismissal_count' => $ed];
|
||||
}
|
||||
|
||||
@@ -528,6 +558,7 @@ class AttendanceManagementService
|
||||
private function schoolStartFor(Carbon $date): Carbon
|
||||
{
|
||||
$configured = Configuration::getConfigValueByKey('school_start_time') ?: config('attendance.school_start_time', '09:00:00');
|
||||
|
||||
return Carbon::parse($date->toDateString().' '.$configured, $date->timezone);
|
||||
}
|
||||
|
||||
@@ -616,12 +647,20 @@ class AttendanceManagementService
|
||||
}));
|
||||
}
|
||||
|
||||
private function dateTime($value): Carbon { return $value instanceof Carbon ? $value : Carbon::parse((string) $value); }
|
||||
private function dateOnly($value): string { return Carbon::parse((string) $value)->toDateString(); }
|
||||
private function dateTime($value): Carbon
|
||||
{
|
||||
return $value instanceof Carbon ? $value : Carbon::parse((string) $value);
|
||||
}
|
||||
|
||||
private function dateOnly($value): string
|
||||
{
|
||||
return Carbon::parse((string) $value)->toDateString();
|
||||
}
|
||||
|
||||
private function normalizeReportStatus($value): string
|
||||
{
|
||||
$v = strtolower(str_replace(['-', ' '], '_', trim((string) $value)));
|
||||
|
||||
return in_array($v, ['reported', 'not_reported', 'pending_clarification'], true) ? $v : 'pending_clarification';
|
||||
}
|
||||
|
||||
@@ -641,30 +680,61 @@ class AttendanceManagementService
|
||||
private function combinationCode(int $abs, int $late, int $ed, int $badge): string
|
||||
{
|
||||
$parts = [];
|
||||
if ($abs > 0) $parts[] = min($abs, 5).'ABS';
|
||||
if ($late > 0) $parts[] = min($late, 5).'Late';
|
||||
if ($ed > 0) $parts[] = min($ed, 3).'ED';
|
||||
if ($badge > 0) $parts[] = min($badge, 5).'Badge Exceptions';
|
||||
if ($abs > 0) {
|
||||
$parts[] = min($abs, 5).'ABS';
|
||||
}
|
||||
if ($late > 0) {
|
||||
$parts[] = min($late, 5).'Late';
|
||||
}
|
||||
if ($ed > 0) {
|
||||
$parts[] = min($ed, 3).'ED';
|
||||
}
|
||||
if ($badge > 0) {
|
||||
$parts[] = min($badge, 5).'Badge Exceptions';
|
||||
}
|
||||
|
||||
return $parts ? implode(' + ', $parts) : 'Clear';
|
||||
}
|
||||
|
||||
private function riskLevel(int $abs, int $late, int $ed, int $badge): string
|
||||
{
|
||||
if ($abs >= 5 && $late >= 5) return 'severe';
|
||||
if ($abs >= 5 || $late >= 5) return 'critical';
|
||||
if ($abs + $late + $ed + $badge >= 6 || $abs >= 4 || $late >= 4) return 'very_high';
|
||||
if ($abs + $late + $ed + $badge >= 4 || $abs >= 3 || $late >= 3 || $badge >= 4) return 'high';
|
||||
if ($abs + $late + $ed + $badge >= 2 || $badge >= 2) return 'medium';
|
||||
if ($abs >= 5 && $late >= 5) {
|
||||
return 'severe';
|
||||
}
|
||||
if ($abs >= 5 || $late >= 5) {
|
||||
return 'critical';
|
||||
}
|
||||
if ($abs + $late + $ed + $badge >= 6 || $abs >= 4 || $late >= 4) {
|
||||
return 'very_high';
|
||||
}
|
||||
if ($abs + $late + $ed + $badge >= 4 || $abs >= 3 || $late >= 3 || $badge >= 4) {
|
||||
return 'high';
|
||||
}
|
||||
if ($abs + $late + $ed + $badge >= 2 || $badge >= 2) {
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
return 'low';
|
||||
}
|
||||
|
||||
private function actionNeeded(string $status, string $reportStatus, int $badgeCount): string
|
||||
{
|
||||
if ($badgeCount >= 5) return 'Leadership review for badge compliance';
|
||||
if ($badgeCount >= 4) return 'Formal badge follow-up required';
|
||||
if ($status === self::STATUS_LATE && $reportStatus !== 'reported') return 'Print late slip and contact parent or staff';
|
||||
if ($status === self::STATUS_ABSENT && $reportStatus !== 'reported') return 'Same-day contact required';
|
||||
if ($status === self::STATUS_EARLY_DISMISSAL && $reportStatus !== 'reported') return 'Verify authorization and contact guardian/supervisor';
|
||||
if ($badgeCount >= 5) {
|
||||
return 'Leadership review for badge compliance';
|
||||
}
|
||||
if ($badgeCount >= 4) {
|
||||
return 'Formal badge follow-up required';
|
||||
}
|
||||
if ($status === self::STATUS_LATE && $reportStatus !== 'reported') {
|
||||
return 'Print late slip and contact parent or staff';
|
||||
}
|
||||
if ($status === self::STATUS_ABSENT && $reportStatus !== 'reported') {
|
||||
return 'Same-day contact required';
|
||||
}
|
||||
if ($status === self::STATUS_EARLY_DISMISSAL && $reportStatus !== 'reported') {
|
||||
return 'Verify authorization and contact guardian/supervisor';
|
||||
}
|
||||
|
||||
return 'Record and monitor';
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ class AttendanceCaseQueryService
|
||||
protected AttendanceTracking $attendanceTrackingModel,
|
||||
protected ViolationRuleEngineService $violationRuleEngine,
|
||||
protected AttendanceParentLookupService $parentLookupService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getStudentCase(
|
||||
int $studentId,
|
||||
@@ -32,16 +31,16 @@ class AttendanceCaseQueryService
|
||||
?string $defaultSemester = null
|
||||
): array {
|
||||
$student = $this->studentModel->query()->find($studentId);
|
||||
if (!$student) {
|
||||
if (! $student) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Student not found',
|
||||
'status' => 404,
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
$student = $student->toArray();
|
||||
$student['name'] = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));
|
||||
$student['name'] = trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? ''));
|
||||
|
||||
$codeParam = strtoupper((string) ($codeParam ?? ''));
|
||||
$incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate) ? (string) $incidentDate : '';
|
||||
@@ -66,13 +65,13 @@ class AttendanceCaseQueryService
|
||||
}
|
||||
|
||||
[$syStart, $syEnd] = $this->violationRuleEngine->deriveSchoolYearBounds($schoolYear ?: date('Y'));
|
||||
$today = Carbon::today();
|
||||
$start = Carbon::parse($syStart)->startOfDay();
|
||||
$today = Carbon::today();
|
||||
$start = Carbon::parse($syStart)->startOfDay();
|
||||
$rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
|
||||
|
||||
$windowWeeks = $this->violationRuleEngine->windowWeeksForViolationCode($codeParam);
|
||||
$endYmd = $incidentDate !== '' ? $incidentDate : date('Y-m-d');
|
||||
$startYmd = $this->violationRuleEngine->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester);
|
||||
$endYmd = $incidentDate !== '' ? $incidentDate : date('Y-m-d');
|
||||
$startYmd = $this->violationRuleEngine->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester);
|
||||
|
||||
$studentIdValues = [(int) $studentId];
|
||||
$studentCodeRaw = trim((string) ($student['student_code'] ?? $student['school_id'] ?? ''));
|
||||
@@ -108,7 +107,7 @@ class AttendanceCaseQueryService
|
||||
])
|
||||
->where(function ($q) use ($studentIdValues, $studentCodeValues) {
|
||||
$q->whereIn('student_id', $studentIdValues);
|
||||
if (!empty($studentCodeValues)) {
|
||||
if (! empty($studentCodeValues)) {
|
||||
$q->orWhereIn('student_id', $studentCodeValues);
|
||||
}
|
||||
})
|
||||
@@ -126,7 +125,7 @@ class AttendanceCaseQueryService
|
||||
if ($pendingOnly) {
|
||||
$this->violationRuleEngine->applyUnreportedAttendanceFilter($qb, $this->attendanceDataModel->getTable());
|
||||
|
||||
if (!$includeNotified) {
|
||||
if (! $includeNotified) {
|
||||
$qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))");
|
||||
}
|
||||
}
|
||||
@@ -142,21 +141,21 @@ class AttendanceCaseQueryService
|
||||
$attendanceRows = $qb->orderByDesc('date')->get()->toArray();
|
||||
|
||||
$data = [
|
||||
'student' => $student,
|
||||
'attendance' => $attendanceRows,
|
||||
'violation_code' => $codeParam !== '' ? $codeParam : null,
|
||||
'violation_date' => $incidentDate,
|
||||
'violation_notified' => null,
|
||||
'student' => $student,
|
||||
'attendance' => $attendanceRows,
|
||||
'violation_code' => $codeParam !== '' ? $codeParam : null,
|
||||
'violation_date' => $incidentDate,
|
||||
'violation_notified' => null,
|
||||
'show_violation_summary' => true,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
|
||||
if ($data['violation_date'] === '' && !empty($data['attendance'][0]['date'])) {
|
||||
if ($data['violation_date'] === '' && ! empty($data['attendance'][0]['date'])) {
|
||||
$data['violation_date'] = substr((string) $data['attendance'][0]['date'], 0, 10);
|
||||
}
|
||||
|
||||
if (!empty($data['attendance'][0]['is_notified'])) {
|
||||
if (! empty($data['attendance'][0]['is_notified'])) {
|
||||
$data['violation_notified'] = $data['attendance'][0]['is_notified'];
|
||||
}
|
||||
|
||||
@@ -172,7 +171,7 @@ class AttendanceCaseQueryService
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -183,7 +182,7 @@ class AttendanceCaseQueryService
|
||||
?string $defaultSemester = null
|
||||
): array {
|
||||
$schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear;
|
||||
$semester = filled($semesterParam) ? (string) $semesterParam : (string) $defaultSemester;
|
||||
$semester = filled($semesterParam) ? (string) $semesterParam : (string) $defaultSemester;
|
||||
|
||||
$trackingData = $this->attendanceTrackingModel->query()
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -211,7 +210,7 @@ class AttendanceCaseQueryService
|
||||
|
||||
if ($latest) {
|
||||
$schoolYear = (string) ($latest->school_year ?? $schoolYear);
|
||||
$semester = (string) ($latest->semester ?? $semester);
|
||||
$semester = (string) ($latest->semester ?? $semester);
|
||||
|
||||
$trackingData = $this->attendanceTrackingModel->query()
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -229,9 +228,9 @@ class AttendanceCaseQueryService
|
||||
|
||||
if (empty($trackingData)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'show_actions' => false,
|
||||
];
|
||||
}
|
||||
@@ -258,9 +257,9 @@ class AttendanceCaseQueryService
|
||||
|
||||
if (empty($trackingData)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'show_actions' => false,
|
||||
];
|
||||
}
|
||||
@@ -289,12 +288,12 @@ class AttendanceCaseQueryService
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int) ($row->student_id ?? 0);
|
||||
if ($sid > 0 && !isset($classByStudent[$sid])) {
|
||||
if ($sid > 0 && ! isset($classByStudent[$sid])) {
|
||||
$classByStudent[$sid] = [
|
||||
'class_section_id' => $row->class_section_id ?? null,
|
||||
'class_section_id' => $row->class_section_id ?? null,
|
||||
'class_section_name' => (string) ($row->class_section_name ?? ''),
|
||||
'class_id' => $row->class_id ?? null,
|
||||
'class_name' => (string) ($row->class_name ?? ''),
|
||||
'class_id' => $row->class_id ?? null,
|
||||
'class_name' => (string) ($row->class_name ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -304,7 +303,7 @@ class AttendanceCaseQueryService
|
||||
$mergedData = [];
|
||||
foreach ($trackingData as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
if (!isset($studentMap[$sid])) {
|
||||
if (! isset($studentMap[$sid])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -312,15 +311,15 @@ class AttendanceCaseQueryService
|
||||
|
||||
if (isset($classByStudent[$sid])) {
|
||||
$merged['class_section_name'] = (string) ($classByStudent[$sid]['class_section_name'] ?? '');
|
||||
$merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? '');
|
||||
} elseif (!empty($merged['registration_grade'])) {
|
||||
$merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? '');
|
||||
} elseif (! empty($merged['registration_grade'])) {
|
||||
$merged['grade'] = (string) $merged['registration_grade'];
|
||||
}
|
||||
|
||||
try {
|
||||
$p = $this->parentLookupService->getPrimaryParentForStudent($sid);
|
||||
if ($p) {
|
||||
$merged['parent_name'] = $p['parent_name'] ?? '';
|
||||
$merged['parent_name'] = $p['parent_name'] ?? '';
|
||||
$merged['parent_email'] = $p['email'] ?? '';
|
||||
$merged['parent_phone'] = $p['phone'] ?? '';
|
||||
}
|
||||
@@ -331,9 +330,9 @@ class AttendanceCaseQueryService
|
||||
}
|
||||
|
||||
return [
|
||||
'students' => $mergedData,
|
||||
'students' => $mergedData,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'semester' => $semester,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\DB;
|
||||
class AttendanceCommunicationSupportService
|
||||
{
|
||||
protected string $semester;
|
||||
|
||||
protected string $schoolYear;
|
||||
|
||||
public function __construct(
|
||||
@@ -19,7 +20,7 @@ class AttendanceCommunicationSupportService
|
||||
protected AttendanceParentLookupService $parentLookupService,
|
||||
protected AttendanceEmailComposerService $emailComposerService
|
||||
) {
|
||||
$this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
|
||||
@@ -33,12 +34,12 @@ class AttendanceCommunicationSupportService
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Missing student_id.',
|
||||
'status' => 422,
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
$student = $this->studentModel->query()->find($studentId)?->toArray() ?? [];
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($studentId);
|
||||
$student = $this->studentModel->query()->find($studentId)?->toArray() ?? [];
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($studentId);
|
||||
$secondary = $this->parentLookupService->getSecondaryParentForStudent($studentId, $this->schoolYear);
|
||||
|
||||
$result = $this->emailComposerService->composePreview(
|
||||
@@ -52,7 +53,7 @@ class AttendanceCommunicationSupportService
|
||||
);
|
||||
|
||||
if (($result['success'] ?? false) && isset($result['data'])) {
|
||||
$result['data']['semester'] = $this->semester;
|
||||
$result['data']['semester'] = $this->semester;
|
||||
$result['data']['school_year'] = $this->schoolYear;
|
||||
}
|
||||
|
||||
@@ -100,4 +101,4 @@ class AttendanceCommunicationSupportService
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,25 @@ namespace App\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceEmailTemplate;
|
||||
use App\Support\MailHtml;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AttendanceEmailComposerService
|
||||
{
|
||||
public function __construct(
|
||||
protected AttendanceEmailTemplate $attendanceEmailTemplateModel
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function buildTemplateContext(array $violation, ?array $parent = null): array
|
||||
{
|
||||
$studentName = (string) ($violation['name'] ?? '');
|
||||
$incident = (string) ($violation['last_date'] ?? date('Y-m-d'));
|
||||
$incident = (string) ($violation['last_date'] ?? date('Y-m-d'));
|
||||
|
||||
return [
|
||||
'{{parent_name}}' => (string) ($parent['parent_name'] ?? 'Parent/Guardian'),
|
||||
'{{student_name}}' => $studentName,
|
||||
'{{incident_date}}' => $incident,
|
||||
'{{school_phone}}' => '978-364-0219',
|
||||
'{{parent_name}}' => (string) ($parent['parent_name'] ?? 'Parent/Guardian'),
|
||||
'{{student_name}}' => $studentName,
|
||||
'{{incident_date}}' => $incident,
|
||||
'{{school_phone}}' => '978-364-0219',
|
||||
'{{voicemail_phone}}' => (string) ($parent['phone'] ?? '—'),
|
||||
'{{class_time}}' => '10:00 AM',
|
||||
'{{class_time}}' => '10:00 AM',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,7 +40,7 @@ class AttendanceEmailComposerService
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
$row = $this->attendanceEmailTemplateModel->query()
|
||||
->where('code', $code)
|
||||
->where('variant', 'default')
|
||||
@@ -53,12 +51,12 @@ class AttendanceEmailComposerService
|
||||
$row = $row?->toArray();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$subject = strtr($row['subject'], $context);
|
||||
$body = strtr($row['body_html'], $context);
|
||||
$body = strtr($row['body_html'], $context);
|
||||
|
||||
return [$subject, $body];
|
||||
}
|
||||
@@ -70,35 +68,35 @@ class AttendanceEmailComposerService
|
||||
|
||||
public function pickVariant(string $code, ?string $globalVariantOverride = null): string
|
||||
{
|
||||
if (!empty($globalVariantOverride)) {
|
||||
if (! empty($globalVariantOverride)) {
|
||||
return $globalVariantOverride;
|
||||
}
|
||||
|
||||
return match ($code) {
|
||||
'ABS_2', 'ABS_2_IN3W' => 'no_answer',
|
||||
default => 'default',
|
||||
default => 'default',
|
||||
};
|
||||
}
|
||||
|
||||
public function buildFallbackAutoEmail(string $code, array $violation, string $parentName = ''): array
|
||||
{
|
||||
$subject = match ($code) {
|
||||
'ABS_1' => 'Attendance Notice: Unreported Absence',
|
||||
'ABS_1' => 'Attendance Notice: Unreported Absence',
|
||||
'LATE_2' => 'Attendance Notice: Repeated Lateness',
|
||||
default => 'Attendance Notice',
|
||||
default => 'Attendance Notice',
|
||||
};
|
||||
|
||||
$studentName = (string) ($violation['name'] ?? '');
|
||||
$date = (string) ($violation['last_date'] ?? date('Y-m-d'));
|
||||
$date = (string) ($violation['last_date'] ?? date('Y-m-d'));
|
||||
|
||||
$body = "<p>Dear " . ($parentName ?: 'Parent/Guardian') . "</p>"
|
||||
. "<p>We'd like to inform you about your student's recent attendance:</p>"
|
||||
. "<ul>"
|
||||
. "<li><strong>Student:</strong> {$studentName}</li>"
|
||||
. "<li><strong>Issue:</strong> " . (string) ($violation['violation'] ?? '') . "</li>"
|
||||
. "<li><strong>As of:</strong> {$date}</li>"
|
||||
. "</ul>"
|
||||
. "<p>If you have any questions, please contact the school office.</p>";
|
||||
$body = '<p>Dear '.($parentName ?: 'Parent/Guardian').'</p>'
|
||||
."<p>We'd like to inform you about your student's recent attendance:</p>"
|
||||
.'<ul>'
|
||||
."<li><strong>Student:</strong> {$studentName}</li>"
|
||||
.'<li><strong>Issue:</strong> '.(string) ($violation['violation'] ?? '').'</li>'
|
||||
."<li><strong>As of:</strong> {$date}</li>"
|
||||
.'</ul>'
|
||||
.'<p>If you have any questions, please contact the school office.</p>';
|
||||
|
||||
return [$subject, $body];
|
||||
}
|
||||
@@ -134,7 +132,8 @@ class AttendanceEmailComposerService
|
||||
static function ($m) {
|
||||
$url = $m[0];
|
||||
$urlAttr = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
return '<a href="' . $urlAttr . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>';
|
||||
|
||||
return '<a href="'.$urlAttr.'" target="_blank" rel="noopener noreferrer">'.$url.'</a>';
|
||||
},
|
||||
$escaped
|
||||
);
|
||||
@@ -142,8 +141,9 @@ class AttendanceEmailComposerService
|
||||
$paragraphs = preg_split("/\n{2,}/", $escaped);
|
||||
|
||||
$paragraphs = array_map(static function ($p) {
|
||||
$p = preg_replace("/\n/", "<br>", $p);
|
||||
return '<p>' . $p . '</p>';
|
||||
$p = preg_replace("/\n/", '<br>', $p);
|
||||
|
||||
return '<p>'.$p.'</p>';
|
||||
}, $paragraphs);
|
||||
|
||||
return implode("\n", $paragraphs);
|
||||
@@ -152,10 +152,10 @@ class AttendanceEmailComposerService
|
||||
public function renderAutoEmailFor(array $violation, ?array $parent = null): ?array
|
||||
{
|
||||
$code = (string) ($violation['violation_code'] ?? $violation['code'] ?? '');
|
||||
$ctx = $this->buildTemplateContext($violation, $parent);
|
||||
$tpl = $this->renderTemplate($code, 'default', $ctx);
|
||||
$ctx = $this->buildTemplateContext($violation, $parent);
|
||||
$tpl = $this->renderTemplate($code, 'default', $ctx);
|
||||
|
||||
if (!$tpl) {
|
||||
if (! $tpl) {
|
||||
return $this->buildFallbackAutoEmail($code, $violation, (string) ($parent['parent_name'] ?? ''));
|
||||
}
|
||||
|
||||
@@ -176,21 +176,21 @@ class AttendanceEmailComposerService
|
||||
: date('Y-m-d');
|
||||
|
||||
$violation = [
|
||||
'id' => $studentId,
|
||||
'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
|
||||
'last_date' => $lastDate,
|
||||
'violation' => $code,
|
||||
'id' => $studentId,
|
||||
'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')),
|
||||
'last_date' => $lastDate,
|
||||
'violation' => $code,
|
||||
'violation_code' => $code,
|
||||
];
|
||||
|
||||
$ctx = $this->buildTemplateContext($violation, $primaryParent);
|
||||
$rendered = $this->renderTemplate($code, $variant, $ctx);
|
||||
|
||||
if (!$rendered) {
|
||||
if (! $rendered) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template not found for {$code} ({$variant}).",
|
||||
'status' => 404,
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -198,19 +198,19 @@ class AttendanceEmailComposerService
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $violation['name'],
|
||||
'parent_email' => (string) ($primaryParent['email'] ?? ''),
|
||||
'parent_name' => (string) ($primaryParent['parent_name'] ?? ''),
|
||||
'data' => [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $violation['name'],
|
||||
'parent_email' => (string) ($primaryParent['email'] ?? ''),
|
||||
'parent_name' => (string) ($primaryParent['parent_name'] ?? ''),
|
||||
'secondary_email' => (string) ($secondaryParent['email'] ?? ''),
|
||||
'secondary_name' => (string) ($secondaryParent['parent_name'] ?? ''),
|
||||
'code' => $code,
|
||||
'variant' => $variant,
|
||||
'subject' => $subject,
|
||||
'body_html' => $body,
|
||||
'incident_date' => $lastDate,
|
||||
'secondary_name' => (string) ($secondaryParent['parent_name'] ?? ''),
|
||||
'code' => $code,
|
||||
'variant' => $variant,
|
||||
'subject' => $subject,
|
||||
'body_html' => $body,
|
||||
'incident_date' => $lastDate,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ interface AttendanceMailerService
|
||||
public function send(string $to, string $subject, string $html): bool;
|
||||
|
||||
public function queueAttendanceEvent(array $payload): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ class AttendanceNotificationLogService
|
||||
{
|
||||
public function __construct(
|
||||
protected ParentNotification $notificationModel
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function notificationAlreadySent(
|
||||
int $studentId,
|
||||
@@ -31,13 +30,14 @@ class AttendanceNotificationLogService
|
||||
->where('incident_date', $ymd)
|
||||
->where('channel', $channel);
|
||||
|
||||
if (!empty($to)) {
|
||||
if (! empty($to)) {
|
||||
$query->where('to_address', $to);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
} catch (Throwable $e) {
|
||||
Log::debug('notificationAlreadySent(): ' . $e->getMessage());
|
||||
Log::debug('notificationAlreadySent(): '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -61,37 +61,37 @@ class AttendanceNotificationLogService
|
||||
->whereDate('incident_date', $ymd)
|
||||
->where('channel', $channel);
|
||||
|
||||
if (!empty($to)) {
|
||||
if (! empty($to)) {
|
||||
$existingQuery->where('to_address', $to);
|
||||
}
|
||||
|
||||
$existing = $existingQuery->orderByDesc('id')->first();
|
||||
|
||||
if ($existing && !empty($existing->id)) {
|
||||
if ($existing && ! empty($existing->id)) {
|
||||
$existing->update([
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'response' => $response,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_at' => now(),
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'response' => $response,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$this->notificationModel->query()->create([
|
||||
'student_id' => $studentId,
|
||||
'code' => $code,
|
||||
'student_id' => $studentId,
|
||||
'code' => $code,
|
||||
'incident_date' => $ymd,
|
||||
'channel' => $channel,
|
||||
'to_address' => $to,
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'response' => $response,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'channel' => $channel,
|
||||
'to_address' => $to,
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'response' => $response,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::debug('logNotification(): ' . $e->getMessage());
|
||||
Log::debug('logNotification(): '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Models\AttendanceTracking;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
@@ -15,6 +14,7 @@ use Throwable;
|
||||
class AttendanceNotificationWorkflowService
|
||||
{
|
||||
protected string $semester;
|
||||
|
||||
protected string $schoolYear;
|
||||
|
||||
public function __construct(
|
||||
@@ -29,33 +29,33 @@ class AttendanceNotificationWorkflowService
|
||||
protected AttendanceEmailComposerService $emailComposerService,
|
||||
protected AttendanceNotificationLogService $notificationLogService
|
||||
) {
|
||||
$this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
|
||||
public function record(array $data): array
|
||||
{
|
||||
$sid = (int) $data['student_id'];
|
||||
$ymd = substr((string) $data['date'], 0, 10);
|
||||
$semester = $data['semester'] ?? $this->semester;
|
||||
$sid = (int) $data['student_id'];
|
||||
$ymd = substr((string) $data['date'], 0, 10);
|
||||
$semester = $data['semester'] ?? $this->semester;
|
||||
$schoolYear = $data['school_year'] ?? $this->schoolYear;
|
||||
$subject = $data['subject_type'] ?? 'Absent';
|
||||
$subject = $data['subject_type'] ?? 'Absent';
|
||||
|
||||
$student = $this->studentModel->query()->find($sid)?->toArray() ?? [];
|
||||
$parentEmail = $data['parent_email'] ?? '';
|
||||
$parentName = $data['parent_name'] ?? '';
|
||||
$parentName = $data['parent_name'] ?? '';
|
||||
|
||||
if (!$parentEmail) {
|
||||
if (! $parentEmail) {
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($sid) ?? [];
|
||||
$parentEmail = $parent['email'] ?? '';
|
||||
$parentName = $parent['parent_name'] ?? '';
|
||||
$parentName = $parent['parent_name'] ?? '';
|
||||
}
|
||||
|
||||
if (!$parentEmail) {
|
||||
if (! $parentEmail) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No parent email found for this student.',
|
||||
'status' => 422,
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -87,37 +87,37 @@ class AttendanceNotificationWorkflowService
|
||||
: [];
|
||||
|
||||
$payload = [
|
||||
'student_id' => $sid,
|
||||
'student' => $student,
|
||||
'parent' => ['email' => $parentEmail, 'name' => $parentName],
|
||||
'semester' => $semester,
|
||||
'student_id' => $sid,
|
||||
'student' => $student,
|
||||
'parent' => ['email' => $parentEmail, 'name' => $parentName],
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'class' => $class,
|
||||
'now' => now()->toDateTimeString(),
|
||||
'date' => $ymd,
|
||||
'subject' => $subject,
|
||||
'class' => $class,
|
||||
'now' => now()->toDateTimeString(),
|
||||
'date' => $ymd,
|
||||
'subject' => $subject,
|
||||
'consequence' => $consequence ?: 'follow_up',
|
||||
];
|
||||
|
||||
try {
|
||||
$this->attendanceMailerService->queueAttendanceEvent($payload);
|
||||
|
||||
if ($row && !empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) {
|
||||
if ($row && ! empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) {
|
||||
$this->attendanceTrackingModel->markAsNotified((int) $row['id']);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Notification queued.',
|
||||
'data' => $payload,
|
||||
'data' => $payload,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Attendance record queue failed: ' . $e->getMessage());
|
||||
Log::error('Attendance record queue failed: '.$e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'status' => 500,
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -132,30 +132,32 @@ class AttendanceNotificationWorkflowService
|
||||
$details = [];
|
||||
|
||||
foreach ($toSend as $v) {
|
||||
$sid = (int) ($v['id'] ?? 0);
|
||||
$code = (string) ($v['violation_code'] ?? $v['code'] ?? '');
|
||||
$date = (string) ($v['last_date'] ?? '');
|
||||
$to = trim((string) ($v['parent_email'] ?? ''));
|
||||
$sid = (int) ($v['id'] ?? 0);
|
||||
$code = (string) ($v['violation_code'] ?? $v['code'] ?? '');
|
||||
$date = (string) ($v['last_date'] ?? '');
|
||||
$to = trim((string) ($v['parent_email'] ?? ''));
|
||||
$pname = (string) ($v['parent_name'] ?? '');
|
||||
|
||||
if ($sid <= 0 || !$code || !$date || !$to) {
|
||||
if ($sid <= 0 || ! $code || ! $date || ! $to) {
|
||||
$errors++;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'missing data'];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->notificationLogService->notificationAlreadySent($sid, $code, $date, 'email', $to)) {
|
||||
$skipped++;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate'];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($sid);
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($sid);
|
||||
$context = $this->emailComposerService->buildTemplateContext($v, $parent);
|
||||
$variant = $this->emailComposerService->pickVariant($code, $variantOverride);
|
||||
$tpl = $this->emailComposerService->renderTemplate($code, $variant, $context);
|
||||
$tpl = $this->emailComposerService->renderTemplate($code, $variant, $context);
|
||||
|
||||
if (!$tpl) {
|
||||
if (! $tpl) {
|
||||
[$subject, $body] = $this->emailComposerService->buildFallbackAutoEmail($code, $v, $pname);
|
||||
} else {
|
||||
[$subject, $body] = $tpl;
|
||||
@@ -212,7 +214,7 @@ class AttendanceNotificationWorkflowService
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: ' . $e->getMessage()];
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: '.$e->getMessage()];
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -260,7 +262,7 @@ class AttendanceNotificationWorkflowService
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: ' . $e->getMessage()];
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: '.$e->getMessage()];
|
||||
}
|
||||
|
||||
if ($anyOk) {
|
||||
@@ -277,13 +279,13 @@ class AttendanceNotificationWorkflowService
|
||||
}
|
||||
|
||||
$allDates = [];
|
||||
$absDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['absences'] ?? []));
|
||||
$absDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['absences'] ?? []));
|
||||
$lateDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['lates'] ?? []));
|
||||
|
||||
if (!empty($absDates)) {
|
||||
if (! empty($absDates)) {
|
||||
$allDates = array_merge($allDates, $absDates);
|
||||
}
|
||||
if (!empty($lateDates)) {
|
||||
if (! empty($lateDates)) {
|
||||
$allDates = array_merge($allDates, $lateDates);
|
||||
}
|
||||
if (empty($allDates)) {
|
||||
@@ -303,29 +305,29 @@ class AttendanceNotificationWorkflowService
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'sent' => $sent,
|
||||
'sent' => $sent,
|
||||
'skipped' => $skipped,
|
||||
'errors' => $errors,
|
||||
'errors' => $errors,
|
||||
'details' => $details,
|
||||
];
|
||||
}
|
||||
|
||||
public function sendEmailManual(array $data, array $violationDates = []): array
|
||||
{
|
||||
$sid = (int) $data['student_id'];
|
||||
$to = trim((string) $data['to']);
|
||||
$subject = (string) $data['subject'];
|
||||
$sid = (int) $data['student_id'];
|
||||
$to = trim((string) $data['to']);
|
||||
$subject = (string) $data['subject'];
|
||||
$bodyInput = (string) $data['body_html'];
|
||||
$code = (string) $data['code'];
|
||||
$variant = (string) ($data['variant'] ?? 'default');
|
||||
$code = (string) $data['code'];
|
||||
$variant = (string) ($data['variant'] ?? 'default');
|
||||
|
||||
$ymdRaw = (string) ($data['incident_date'] ?? '');
|
||||
$ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
|
||||
$ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
|
||||
|
||||
$safeHtmlBody = $this->emailComposerService->normalizeBodyToHtml($bodyInput);
|
||||
$wrapped = $this->emailComposerService->renderWithEmailLayout($subject, $safeHtmlBody);
|
||||
|
||||
$semester = $this->semester ?: (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$semester = $this->semester ?: (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$schoolYear = $this->schoolYear ?: (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
|
||||
try {
|
||||
@@ -354,7 +356,7 @@ class AttendanceNotificationWorkflowService
|
||||
$to2 = trim((string) ($sec['email'] ?? ''));
|
||||
|
||||
if ($to2 !== '' && strcasecmp($to2, $to) !== 0) {
|
||||
if (!$this->notificationLogService->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) {
|
||||
if (! $this->notificationLogService->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) {
|
||||
$ok2 = $this->attendanceMailerService->send($to2, $subject, $wrapped);
|
||||
|
||||
$this->notificationLogService->logNotification(
|
||||
@@ -376,11 +378,11 @@ class AttendanceNotificationWorkflowService
|
||||
}
|
||||
}
|
||||
|
||||
if (!$anyOk) {
|
||||
if (! $anyOk) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to send email.',
|
||||
'status' => 500,
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -394,7 +396,7 @@ class AttendanceNotificationWorkflowService
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($violationDates) && method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
|
||||
if (! empty($violationDates) && method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
|
||||
$this->attendanceDataModel->markReportedAndNotified(
|
||||
$sid,
|
||||
$violationDates,
|
||||
@@ -406,12 +408,12 @@ class AttendanceNotificationWorkflowService
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Email sent successfully.',
|
||||
'data' => [
|
||||
'student_id' => $sid,
|
||||
'to' => $to,
|
||||
'secondary_to' => $to2 ?? null,
|
||||
'subject' => $subject,
|
||||
'variant' => $variant,
|
||||
'data' => [
|
||||
'student_id' => $sid,
|
||||
'to' => $to,
|
||||
'secondary_to' => $to2 ?? null,
|
||||
'subject' => $subject,
|
||||
'variant' => $variant,
|
||||
'incident_date' => $ymd,
|
||||
],
|
||||
];
|
||||
@@ -431,19 +433,19 @@ class AttendanceNotificationWorkflowService
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage(),
|
||||
'status' => 500,
|
||||
'message' => 'Error: '.$e->getMessage(),
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function saveNotificationNote(array $data): array
|
||||
{
|
||||
$sid = (int) $data['student_id'];
|
||||
$code = (string) $data['code'];
|
||||
$note = trim((string) $data['note']);
|
||||
$sid = (int) $data['student_id'];
|
||||
$code = (string) $data['code'];
|
||||
$note = trim((string) $data['note']);
|
||||
$ymdRaw = (string) ($data['incident_date'] ?? '');
|
||||
$ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
|
||||
$ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
|
||||
|
||||
try {
|
||||
$day = $ymd !== '' ? $ymd : date('Y-m-d');
|
||||
@@ -455,11 +457,11 @@ class AttendanceNotificationWorkflowService
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $end)
|
||||
->where('reason', 'like', '%' . $code . '%')
|
||||
->where('reason', 'like', '%'.$code.'%')
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
$row = $this->attendanceTrackingModel->query()
|
||||
->where('student_id', $sid)
|
||||
->where('semester', $this->semester)
|
||||
@@ -470,28 +472,28 @@ class AttendanceNotificationWorkflowService
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row && !empty($row->id)) {
|
||||
if ($row && ! empty($row->id)) {
|
||||
DB::table($this->attendanceTrackingModel->getTable())
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'),
|
||||
'note' => $note,
|
||||
'is_notified' => 1,
|
||||
'updated_at' => now(),
|
||||
'note' => $note,
|
||||
'is_notified' => 1,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$this->attendanceTrackingModel->query()->create([
|
||||
'student_id' => $sid,
|
||||
'date' => $start,
|
||||
'is_reported' => 0,
|
||||
'reason' => $code,
|
||||
'is_notified' => 1,
|
||||
'student_id' => $sid,
|
||||
'date' => $start,
|
||||
'is_reported' => 0,
|
||||
'reason' => $code,
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => 1,
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
'note' => $note,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
'note' => $note,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -502,9 +504,9 @@ class AttendanceNotificationWorkflowService
|
||||
} catch (Throwable $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save note: ' . $e->getMessage(),
|
||||
'status' => 500,
|
||||
'message' => 'Failed to save note: '.$e->getMessage(),
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@ class AttendanceParentLookupService
|
||||
->where('s.id', $studentId)
|
||||
->first();
|
||||
|
||||
if (!$row || empty($row->user_id)) {
|
||||
if (! $row || empty($row->user_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'user_id' => (int) $row->user_id,
|
||||
'email' => (string) ($row->email ?? ''),
|
||||
'user_id' => (int) $row->user_id,
|
||||
'email' => (string) ($row->email ?? ''),
|
||||
'parent_name' => (string) ($row->parent_name ?? ''),
|
||||
'phone' => $row->cellphone ?? null,
|
||||
'phone' => $row->cellphone ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ class AttendanceParentLookupService
|
||||
->where('id', $studentId)
|
||||
->first();
|
||||
|
||||
if (!$stu) {
|
||||
if (! $stu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$primaryId = (int) ($stu->parent_id ?? 0);
|
||||
$primaryId = (int) ($stu->parent_id ?? 0);
|
||||
$schoolYear = (string) ($stu->school_year ?? $fallbackSchoolYear ?? '');
|
||||
|
||||
$pb = DB::table('parents')->where('firstparent_id', $primaryId);
|
||||
@@ -49,7 +49,7 @@ class AttendanceParentLookupService
|
||||
}
|
||||
|
||||
$pRow = $pb->orderByDesc('updated_at')->first();
|
||||
if (!$pRow) {
|
||||
if (! $pRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ class AttendanceParentLookupService
|
||||
->where('id', $secondId)
|
||||
->first();
|
||||
|
||||
if ($u2 && !empty($u2->email)) {
|
||||
if ($u2 && ! empty($u2->email)) {
|
||||
return [
|
||||
'user_id' => (int) $u2->id,
|
||||
'email' => (string) $u2->email,
|
||||
'parent_name' => trim(($u2->firstname ?? '') . ' ' . ($u2->lastname ?? '')),
|
||||
'phone' => (string) ($u2->cellphone ?? ''),
|
||||
'user_id' => (int) $u2->id,
|
||||
'email' => (string) $u2->email,
|
||||
'parent_name' => trim(($u2->firstname ?? '').' '.($u2->lastname ?? '')),
|
||||
'phone' => (string) ($u2->cellphone ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -73,18 +73,18 @@ class AttendanceParentLookupService
|
||||
$fallbackEmail = (string) ($pRow->secondparent_email ?? '');
|
||||
$fallbackPhone = (string) ($pRow->secondparent_phone ?? '');
|
||||
$fallbackFirst = (string) ($pRow->secondparent_firstname ?? '');
|
||||
$fallbackLast = (string) ($pRow->secondparent_lastname ?? '');
|
||||
$fallbackLast = (string) ($pRow->secondparent_lastname ?? '');
|
||||
|
||||
if ($fallbackEmail || $fallbackPhone || $fallbackFirst || $fallbackLast) {
|
||||
return [
|
||||
'user_id' => $secondId ?: null,
|
||||
'email' => $fallbackEmail,
|
||||
'parent_name' => trim($fallbackFirst . ' ' . $fallbackLast) ?: null,
|
||||
'phone' => $fallbackPhone,
|
||||
'user_id' => $secondId ?: null,
|
||||
'email' => $fallbackEmail,
|
||||
'parent_name' => trim($fallbackFirst.' '.$fallbackLast) ?: null,
|
||||
'phone' => $fallbackPhone,
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::error('getSecondaryParentForStudent() failed: ' . $e->getMessage());
|
||||
Log::error('getSecondaryParentForStudent() failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -96,7 +96,7 @@ class AttendanceParentLookupService
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Missing student_id',
|
||||
'status' => 422,
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -106,15 +106,15 @@ class AttendanceParentLookupService
|
||||
->where('id', $studentId)
|
||||
->first();
|
||||
|
||||
if (!$stu) {
|
||||
if (! $stu) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Student not found',
|
||||
'status' => 404,
|
||||
'status' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
$primaryId = (int) $stu->parent_id;
|
||||
$primaryId = (int) $stu->parent_id;
|
||||
$schoolYear = $stu->school_year ?: $currentSchoolYear;
|
||||
|
||||
$u1 = DB::table('users')
|
||||
@@ -123,39 +123,39 @@ class AttendanceParentLookupService
|
||||
->first();
|
||||
|
||||
$primary = $u1 ? [
|
||||
'id' => (int) $u1->id,
|
||||
'id' => (int) $u1->id,
|
||||
'firstname' => (string) $u1->firstname,
|
||||
'lastname' => (string) $u1->lastname,
|
||||
'email' => (string) $u1->email,
|
||||
'lastname' => (string) $u1->lastname,
|
||||
'email' => (string) $u1->email,
|
||||
'cellphone' => (string) $u1->cellphone,
|
||||
] : null;
|
||||
|
||||
$secondaryRaw = $this->getSecondaryParentForStudent($studentId, $schoolYear);
|
||||
|
||||
$secondary = $secondaryRaw ? [
|
||||
'id' => $secondaryRaw['user_id'] ?? null,
|
||||
'id' => $secondaryRaw['user_id'] ?? null,
|
||||
'firstname' => null,
|
||||
'lastname' => null,
|
||||
'email' => $secondaryRaw['email'] ?? '',
|
||||
'lastname' => null,
|
||||
'email' => $secondaryRaw['email'] ?? '',
|
||||
'cellphone' => $secondaryRaw['phone'] ?? '',
|
||||
'name' => $secondaryRaw['parent_name'] ?? '',
|
||||
'name' => $secondaryRaw['parent_name'] ?? '',
|
||||
] : null;
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'primary' => $primary,
|
||||
'data' => [
|
||||
'primary' => $primary,
|
||||
'secondary' => $secondary,
|
||||
],
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
Log::error('parentsInfo() failed: ' . $e->getMessage());
|
||||
Log::error('parentsInfo() failed: '.$e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Server error',
|
||||
'status' => 500,
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ class AttendancePendingViolationService
|
||||
protected AttendanceData $attendanceDataModel,
|
||||
protected ViolationRuleEngineService $violationRuleEngine,
|
||||
protected AttendanceViolationStudentResolverService $studentResolverService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getPendingViolations(
|
||||
string $defaultSchoolYear,
|
||||
@@ -22,52 +21,52 @@ class AttendancePendingViolationService
|
||||
?string $semesterParam = null
|
||||
): array {
|
||||
$schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear;
|
||||
$semester = filled($semesterParam) ? (string) $semesterParam : null;
|
||||
$semester = filled($semesterParam) ? (string) $semesterParam : null;
|
||||
|
||||
$debugInfo = [
|
||||
'school_year_param' => $schoolYear,
|
||||
'semester_param' => $semester,
|
||||
'class_students' => 0,
|
||||
'student_ids' => 0,
|
||||
'attendance_rows' => 0,
|
||||
'filtered_rows' => 0,
|
||||
'violations' => 0,
|
||||
'active_weeks' => 0,
|
||||
'abs_last5' => 0,
|
||||
'late_last5' => 0,
|
||||
'semester_param' => $semester,
|
||||
'class_students' => 0,
|
||||
'student_ids' => 0,
|
||||
'attendance_rows' => 0,
|
||||
'filtered_rows' => 0,
|
||||
'violations' => 0,
|
||||
'active_weeks' => 0,
|
||||
'abs_last5' => 0,
|
||||
'late_last5' => 0,
|
||||
];
|
||||
|
||||
$resolved = $this->studentResolverService->resolveForSchoolYear($schoolYear, $semester);
|
||||
|
||||
$schoolYear = (string) ($resolved['school_year'] ?? $schoolYear);
|
||||
$semester = $resolved['semester'] ?? $semester;
|
||||
$studentIds = $resolved['student_ids'] ?? [];
|
||||
$studentCodes = $resolved['student_codes'] ?? [];
|
||||
$students = $resolved['students'] ?? [];
|
||||
$schoolYear = (string) ($resolved['school_year'] ?? $schoolYear);
|
||||
$semester = $resolved['semester'] ?? $semester;
|
||||
$studentIds = $resolved['student_ids'] ?? [];
|
||||
$studentCodes = $resolved['student_codes'] ?? [];
|
||||
$students = $resolved['students'] ?? [];
|
||||
$studentCodeToId = $resolved['student_code_to_id'] ?? [];
|
||||
$existingIds = $resolved['existing_ids'] ?? [];
|
||||
$resolverDebug = $resolved['debug'] ?? [];
|
||||
$existingIds = $resolved['existing_ids'] ?? [];
|
||||
$resolverDebug = $resolved['debug'] ?? [];
|
||||
|
||||
$debugInfo['class_students'] = $resolverDebug['class_students'] ?? 0;
|
||||
$debugInfo['student_ids'] = $resolverDebug['student_ids'] ?? 0;
|
||||
$debugInfo['student_ids'] = $resolverDebug['student_ids'] ?? 0;
|
||||
|
||||
if (empty($studentIds) && empty($studentCodes)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'error' => 'No student identifiers found for the current school year.',
|
||||
'debug' => $debugInfo,
|
||||
'semester' => $semester,
|
||||
'error' => 'No student identifiers found for the current school year.',
|
||||
'debug' => $debugInfo,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($students)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'error' => 'No students found matching attendance records.',
|
||||
'debug' => $debugInfo,
|
||||
'semester' => $semester,
|
||||
'error' => 'No students found matching attendance records.',
|
||||
'debug' => $debugInfo,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,14 +90,14 @@ class AttendancePendingViolationService
|
||||
'is_reported',
|
||||
'is_notified',
|
||||
])
|
||||
->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->when(!empty($semester), fn ($q) => $q->where('semester', $semester))
|
||||
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->when(! empty($semester), fn ($q) => $q->where('semester', $semester))
|
||||
->where(function ($q) use ($studentIds, $studentCodes) {
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentIds)) {
|
||||
$q->whereIn('student_id', $studentIds);
|
||||
}
|
||||
if (!empty($studentCodes)) {
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentCodes)) {
|
||||
if (! empty($studentIds)) {
|
||||
$q->orWhereIn('student_id', $studentCodes);
|
||||
} else {
|
||||
$q->whereIn('student_id', $studentCodes);
|
||||
@@ -118,16 +117,16 @@ class AttendancePendingViolationService
|
||||
if (empty($termRows)) {
|
||||
$latestAttendanceTerm = DB::table($this->attendanceDataModel->getTable())
|
||||
->select('school_year', 'semester', 'date')
|
||||
->when(!empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds))
|
||||
->when(! empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if ($latestAttendanceTerm) {
|
||||
$schoolYear = !empty($latestAttendanceTerm->school_year)
|
||||
$schoolYear = ! empty($latestAttendanceTerm->school_year)
|
||||
? (string) $latestAttendanceTerm->school_year
|
||||
: $schoolYear;
|
||||
|
||||
$semester = !empty($latestAttendanceTerm->semester)
|
||||
$semester = ! empty($latestAttendanceTerm->semester)
|
||||
? (string) $latestAttendanceTerm->semester
|
||||
: $semester;
|
||||
|
||||
@@ -150,14 +149,14 @@ class AttendancePendingViolationService
|
||||
'is_reported',
|
||||
'is_notified',
|
||||
])
|
||||
->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->when(!empty($semester), fn ($q) => $q->where('semester', $semester))
|
||||
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->when(! empty($semester), fn ($q) => $q->where('semester', $semester))
|
||||
->where(function ($q) use ($studentIds, $studentCodes) {
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentIds)) {
|
||||
$q->whereIn('student_id', $studentIds);
|
||||
}
|
||||
if (!empty($studentCodes)) {
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentCodes)) {
|
||||
if (! empty($studentIds)) {
|
||||
$q->orWhereIn('student_id', $studentCodes);
|
||||
} else {
|
||||
$q->whereIn('student_id', $studentCodes);
|
||||
@@ -177,10 +176,10 @@ class AttendancePendingViolationService
|
||||
|
||||
if (empty($termRows)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -188,7 +187,7 @@ class AttendancePendingViolationService
|
||||
$attendanceRows = [];
|
||||
foreach ($termRows as $row) {
|
||||
$dStr = $row['date'] ?? null;
|
||||
if (!$dStr) {
|
||||
if (! $dStr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -198,7 +197,7 @@ class AttendancePendingViolationService
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($d->lt($start) || !$d->lt($endEx)) {
|
||||
if ($d->lt($start) || ! $d->lt($endEx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -222,15 +221,15 @@ class AttendancePendingViolationService
|
||||
|
||||
if (empty($attendanceRows)) {
|
||||
return [
|
||||
'students' => [],
|
||||
'students' => [],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
];
|
||||
}
|
||||
|
||||
$debugInfo['attendance_rows'] = count($termRows);
|
||||
$debugInfo['filtered_rows'] = count($attendanceRows);
|
||||
$debugInfo['filtered_rows'] = count($attendanceRows);
|
||||
|
||||
$violations = $this->violationRuleEngine->computeViolations(
|
||||
$students,
|
||||
@@ -241,17 +240,17 @@ class AttendancePendingViolationService
|
||||
);
|
||||
|
||||
$ruleDebug = $this->violationRuleEngine->getDebugCompute();
|
||||
$debugInfo['violations'] = count($violations);
|
||||
$debugInfo['violations'] = count($violations);
|
||||
$debugInfo['active_weeks'] = $ruleDebug['active_weeks'] ?? 0;
|
||||
$debugInfo['by_student'] = $ruleDebug['by_student'] ?? 0;
|
||||
$debugInfo['abs_last5'] = $ruleDebug['abs_last5'] ?? 0;
|
||||
$debugInfo['late_last5'] = $ruleDebug['late_last5'] ?? 0;
|
||||
$debugInfo['by_student'] = $ruleDebug['by_student'] ?? 0;
|
||||
$debugInfo['abs_last5'] = $ruleDebug['abs_last5'] ?? 0;
|
||||
$debugInfo['late_last5'] = $ruleDebug['late_last5'] ?? 0;
|
||||
|
||||
return [
|
||||
'students' => $violations,
|
||||
'students' => $violations,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
'semester' => $semester,
|
||||
'debug' => $debugInfo,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Services\AttendanceTracking;
|
||||
|
||||
use App\Http\Controllers\Api\AttendanceTracking\AttendanceTrackingController;
|
||||
use App\Models\Configuration;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* Facade used by {@see \App\Http\Controllers\Api\AttendanceTracking\AttendanceTrackingController}.
|
||||
* Facade used by {@see AttendanceTrackingController}.
|
||||
*/
|
||||
class AttendanceTrackingService
|
||||
{
|
||||
@@ -15,8 +17,7 @@ class AttendanceTrackingService
|
||||
protected AttendanceCaseQueryService $caseQueryService,
|
||||
protected AttendanceNotificationWorkflowService $workflowService,
|
||||
protected AttendanceCommunicationSupportService $communicationSupportService,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
protected function defaultSchoolYear(): string
|
||||
{
|
||||
@@ -152,7 +153,7 @@ class AttendanceTrackingService
|
||||
$ymd = null;
|
||||
if (! empty($data['date'])) {
|
||||
try {
|
||||
$ymd = \Carbon\Carbon::parse((string) $data['date'])->format('Y-m-d');
|
||||
$ymd = Carbon::parse((string) $data['date'])->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
$ymd = null;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ class AttendanceViolationStudentResolverService
|
||||
protected Student $studentModel,
|
||||
protected StudentClass $studentClassModel,
|
||||
protected AttendanceData $attendanceDataModel
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function resolveForSchoolYear(string $schoolYear, ?string $semester = null): array
|
||||
{
|
||||
$debug = [
|
||||
'class_students' => 0,
|
||||
'student_ids' => 0,
|
||||
'student_ids' => 0,
|
||||
];
|
||||
|
||||
$classStudentsQuery = $this->studentClassModel->query();
|
||||
@@ -41,9 +40,9 @@ class AttendanceViolationStudentResolverService
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if ($latestTerm && !empty($latestTerm->school_year)) {
|
||||
if ($latestTerm && ! empty($latestTerm->school_year)) {
|
||||
$schoolYear = (string) $latestTerm->school_year;
|
||||
if (!empty($latestTerm->semester)) {
|
||||
if (! empty($latestTerm->semester)) {
|
||||
$semester = (string) $latestTerm->semester;
|
||||
}
|
||||
|
||||
@@ -73,14 +72,14 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
if (empty($studentIds) && empty($studentCodes)) {
|
||||
return [
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'student_ids' => [],
|
||||
'student_codes' => [],
|
||||
'students' => [],
|
||||
'student_code_to_id'=> [],
|
||||
'existing_ids' => [],
|
||||
'debug' => $debug,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'student_ids' => [],
|
||||
'student_codes' => [],
|
||||
'students' => [],
|
||||
'student_code_to_id' => [],
|
||||
'existing_ids' => [],
|
||||
'debug' => $debug,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,14 +90,14 @@ class AttendanceViolationStudentResolverService
|
||||
[$studentCodeToId, $existingIds] = $this->buildLookupMaps($students);
|
||||
|
||||
return [
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'student_ids' => $studentIds,
|
||||
'student_codes' => $studentCodes,
|
||||
'students' => $students,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'student_ids' => $studentIds,
|
||||
'student_codes' => $studentCodes,
|
||||
'students' => $students,
|
||||
'student_code_to_id' => $studentCodeToId,
|
||||
'existing_ids' => $existingIds,
|
||||
'debug' => $debug,
|
||||
'existing_ids' => $existingIds,
|
||||
'debug' => $debug,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -114,12 +113,12 @@ class AttendanceViolationStudentResolverService
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($existingIds[$sid])) {
|
||||
if (! isset($existingIds[$sid])) {
|
||||
$students[] = [
|
||||
'id' => $sid,
|
||||
'id' => $sid,
|
||||
'school_id' => (string) $sid,
|
||||
'firstname' => (string) $sid,
|
||||
'lastname' => '',
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
];
|
||||
$existingIds[$sid] = true;
|
||||
@@ -139,12 +138,12 @@ class AttendanceViolationStudentResolverService
|
||||
$sid = abs(crc32($code)) ?: random_int(3000000, 3999999);
|
||||
$studentCodeToId[$code] = $sid;
|
||||
|
||||
if (!isset($existingIds[$sid])) {
|
||||
if (! isset($existingIds[$sid])) {
|
||||
$students[] = [
|
||||
'id' => $sid,
|
||||
'id' => $sid,
|
||||
'school_id' => $code,
|
||||
'firstname' => $code,
|
||||
'lastname' => '',
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
];
|
||||
$existingIds[$sid] = true;
|
||||
@@ -185,7 +184,7 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
$attendanceStudents = DB::table($this->attendanceDataModel->getTable())
|
||||
->selectRaw('DISTINCT student_id')
|
||||
->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->orderBy('student_id')
|
||||
->get();
|
||||
|
||||
@@ -207,7 +206,7 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
protected function filterInactiveIdentifiers(array $studentIds, array $studentCodes): array
|
||||
{
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentIds)) {
|
||||
$inactiveRows = $this->studentModel->query()
|
||||
->select('id')
|
||||
->whereIn('id', $studentIds)
|
||||
@@ -222,11 +221,11 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
$studentIds = array_values(array_filter(
|
||||
$studentIds,
|
||||
static fn ($id) => !isset($inactiveIdSet[$id])
|
||||
static fn ($id) => ! isset($inactiveIdSet[$id])
|
||||
));
|
||||
}
|
||||
|
||||
if (!empty($studentCodes)) {
|
||||
if (! empty($studentCodes)) {
|
||||
$inactiveCodeRows = $this->studentModel->query()
|
||||
->select('school_id')
|
||||
->whereIn('school_id', $studentCodes)
|
||||
@@ -241,7 +240,7 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
$studentCodes = array_values(array_filter(
|
||||
$studentCodes,
|
||||
static fn ($code) => $code !== '' && !isset($inactiveCodeSet[$code])
|
||||
static fn ($code) => $code !== '' && ! isset($inactiveCodeSet[$code])
|
||||
));
|
||||
}
|
||||
|
||||
@@ -252,7 +251,7 @@ class AttendanceViolationStudentResolverService
|
||||
{
|
||||
$students = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
if (! empty($studentIds)) {
|
||||
$students = $this->studentModel->query()
|
||||
->whereIn('id', $studentIds)
|
||||
->where('is_active', 1)
|
||||
@@ -260,7 +259,7 @@ class AttendanceViolationStudentResolverService
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if (!empty($studentCodes)) {
|
||||
if (! empty($studentCodes)) {
|
||||
$byCode = $this->studentModel->query()
|
||||
->whereIn('school_id', $studentCodes)
|
||||
->where('is_active', 1)
|
||||
@@ -271,7 +270,7 @@ class AttendanceViolationStudentResolverService
|
||||
|
||||
foreach ($byCode as $s) {
|
||||
$sid = (int) ($s['id'] ?? 0);
|
||||
if ($sid > 0 && !isset($seen[$sid])) {
|
||||
if ($sid > 0 && ! isset($seen[$sid])) {
|
||||
$students[] = $s;
|
||||
$seen[$sid] = true;
|
||||
}
|
||||
@@ -293,14 +292,14 @@ class AttendanceViolationStudentResolverService
|
||||
}
|
||||
|
||||
foreach ($studentCodes as $code) {
|
||||
if (!isset($knownCodes[$code])) {
|
||||
if (! isset($knownCodes[$code])) {
|
||||
$virtualId = abs(crc32($code)) ?: random_int(1000000, 1999999);
|
||||
$students[] = [
|
||||
'id' => $virtualId,
|
||||
'school_id' => $code,
|
||||
'firstname' => $code,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'id' => $virtualId,
|
||||
'school_id' => $code,
|
||||
'firstname' => $code,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'class_name' => '',
|
||||
];
|
||||
}
|
||||
@@ -317,13 +316,13 @@ class AttendanceViolationStudentResolverService
|
||||
if (is_numeric($sidOrCode)) {
|
||||
$sid = (int) $sidOrCode;
|
||||
|
||||
if ($sid > 0 && !isset($existingIds[$sid])) {
|
||||
if ($sid > 0 && ! isset($existingIds[$sid])) {
|
||||
$students[] = [
|
||||
'id' => $sid,
|
||||
'school_id' => (string) $sid,
|
||||
'firstname' => (string) $sid,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'id' => $sid,
|
||||
'school_id' => (string) $sid,
|
||||
'firstname' => (string) $sid,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'class_name' => '',
|
||||
];
|
||||
$existingIds[$sid] = true;
|
||||
@@ -331,14 +330,14 @@ class AttendanceViolationStudentResolverService
|
||||
} else {
|
||||
$code = (string) $sidOrCode;
|
||||
|
||||
if ($code !== '' && !isset($studentCodeToId[$code])) {
|
||||
if ($code !== '' && ! isset($studentCodeToId[$code])) {
|
||||
$virtualId = abs(crc32($code)) ?: random_int(2000000, 2999999);
|
||||
$students[] = [
|
||||
'id' => $virtualId,
|
||||
'school_id' => $code,
|
||||
'firstname' => $code,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'id' => $virtualId,
|
||||
'school_id' => $code,
|
||||
'firstname' => $code,
|
||||
'lastname' => '',
|
||||
'parent_id' => 0,
|
||||
'class_name' => '',
|
||||
];
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ class DefaultAttendanceMailerService implements AttendanceMailerService
|
||||
public function __construct(
|
||||
protected EmailService $emailService,
|
||||
protected AttendanceEmailComposerService $emailComposerService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function send(string $to, string $subject, string $html): bool
|
||||
{
|
||||
@@ -26,24 +25,24 @@ class DefaultAttendanceMailerService implements AttendanceMailerService
|
||||
}
|
||||
|
||||
$student = (array) ($payload['student'] ?? []);
|
||||
$studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
|
||||
$studentName = trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? ''));
|
||||
$subjectType = (string) ($payload['subject'] ?? 'Attendance');
|
||||
$date = substr((string) ($payload['date'] ?? now()->toDateString()), 0, 10);
|
||||
|
||||
$subject = "Attendance Notice: {$subjectType}";
|
||||
$body = '<p>Dear ' . e((string) data_get($payload, 'parent.name', 'Parent/Guardian')) . ',</p>'
|
||||
. '<p>This is an attendance notification for your student.</p>'
|
||||
. '<ul>'
|
||||
. '<li><strong>Student:</strong> ' . e($studentName !== '' ? $studentName : 'Student') . '</li>'
|
||||
. '<li><strong>Date:</strong> ' . e($date) . '</li>'
|
||||
. '<li><strong>Attendance:</strong> ' . e($subjectType) . '</li>'
|
||||
. '</ul>'
|
||||
. '<p>If you have any questions, please contact the school office.</p>';
|
||||
$body = '<p>Dear '.e((string) data_get($payload, 'parent.name', 'Parent/Guardian')).',</p>'
|
||||
.'<p>This is an attendance notification for your student.</p>'
|
||||
.'<ul>'
|
||||
.'<li><strong>Student:</strong> '.e($studentName !== '' ? $studentName : 'Student').'</li>'
|
||||
.'<li><strong>Date:</strong> '.e($date).'</li>'
|
||||
.'<li><strong>Attendance:</strong> '.e($subjectType).'</li>'
|
||||
.'</ul>'
|
||||
.'<p>If you have any questions, please contact the school office.</p>';
|
||||
|
||||
$html = $this->emailComposerService->renderWithEmailLayout($subject, $body);
|
||||
|
||||
if (!$this->send($to, $subject, $html)) {
|
||||
Log::error('Attendance notification email failed for ' . $to);
|
||||
if (! $this->send($to, $subject, $html)) {
|
||||
Log::error('Attendance notification email failed for '.$to);
|
||||
throw new \RuntimeException('Failed to send attendance notification email.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ use Throwable;
|
||||
class ViolationRuleEngineService
|
||||
{
|
||||
protected array $debugCompute = [];
|
||||
|
||||
protected ?array $attendanceReportedColumns = null;
|
||||
|
||||
public function __construct(
|
||||
protected AttendanceTracking $attendanceTrackingModel,
|
||||
protected AttendanceParentLookupService $parentLookupService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getDebugCompute(): array
|
||||
{
|
||||
@@ -54,10 +54,10 @@ class ViolationRuleEngineService
|
||||
continue;
|
||||
}
|
||||
|
||||
$ymd = substr((string) ($r['date'] ?? ''), 0, 10);
|
||||
$ymd = substr((string) ($r['date'] ?? ''), 0, 10);
|
||||
$stat = strtolower(trim((string) ($r['status'] ?? '')));
|
||||
|
||||
if (!in_array($stat, ['absent', 'late'], true)) {
|
||||
if (! in_array($stat, ['absent', 'late'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ class ViolationRuleEngineService
|
||||
[$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear);
|
||||
|
||||
$weekRowsSource = null;
|
||||
if (!empty($weekRowsFallback)) {
|
||||
if (! empty($weekRowsFallback)) {
|
||||
foreach ($weekRowsFallback as $r) {
|
||||
$st = strtolower(trim((string) ($r['status'] ?? '')));
|
||||
if ($st !== '' && !in_array($st, ['absent', 'late'], true)) {
|
||||
if ($st !== '' && ! in_array($st, ['absent', 'late'], true)) {
|
||||
$weekRowsSource = $weekRowsFallback;
|
||||
break;
|
||||
}
|
||||
@@ -86,9 +86,9 @@ class ViolationRuleEngineService
|
||||
return [];
|
||||
}
|
||||
|
||||
$weekIndex = array_flip($activeWeekKeys);
|
||||
$currentWeekIdx = count($activeWeekKeys) - 1;
|
||||
$windowStartIdx = max(0, $currentWeekIdx - 4);
|
||||
$weekIndex = array_flip($activeWeekKeys);
|
||||
$currentWeekIdx = count($activeWeekKeys) - 1;
|
||||
$windowStartIdx = max(0, $currentWeekIdx - 4);
|
||||
$requiredWeeksCount = $currentWeekIdx - $windowStartIdx + 1;
|
||||
|
||||
$keepLastWeeks = function (array $dates) use ($weekIndex, $windowStartIdx, $currentWeekIdx): array {
|
||||
@@ -105,17 +105,18 @@ class ViolationRuleEngineService
|
||||
}
|
||||
$out = array_values(array_unique($out));
|
||||
rsort($out);
|
||||
|
||||
return $out;
|
||||
};
|
||||
|
||||
$classByStudent = [];
|
||||
try {
|
||||
$studentIdsForClass = array_values(array_unique(array_map(
|
||||
static fn($s) => (int) ($s['id'] ?? 0),
|
||||
static fn ($s) => (int) ($s['id'] ?? 0),
|
||||
$students
|
||||
)));
|
||||
|
||||
if (!empty($studentIdsForClass)) {
|
||||
if (! empty($studentIdsForClass)) {
|
||||
$rows = DB::table('student_class as sc')
|
||||
->select([
|
||||
'sc.student_id',
|
||||
@@ -137,18 +138,18 @@ class ViolationRuleEngineService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($classByStudent[$sid])) {
|
||||
if (! isset($classByStudent[$sid])) {
|
||||
$classByStudent[$sid] = [
|
||||
'class_section_id' => $row->class_section_id ?? null,
|
||||
'class_section_id' => $row->class_section_id ?? null,
|
||||
'class_section_name' => (string) ($row->class_section_name ?? ''),
|
||||
'class_id' => $row->class_id ?? null,
|
||||
'class_name' => (string) ($row->class_name ?? ''),
|
||||
'class_id' => $row->class_id ?? null,
|
||||
'class_name' => (string) ($row->class_name ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::debug('computeViolations(): class prefetch failed: ' . $e->getMessage());
|
||||
Log::debug('computeViolations(): class prefetch failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$out = [];
|
||||
@@ -156,26 +157,26 @@ class ViolationRuleEngineService
|
||||
$lateCountLast5 = 0;
|
||||
|
||||
foreach ($students as $stu) {
|
||||
$sid = (int) ($stu['id'] ?? 0);
|
||||
$name = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? ''));
|
||||
$sid = (int) ($stu['id'] ?? 0);
|
||||
$name = trim(($stu['firstname'] ?? '').' '.($stu['lastname'] ?? ''));
|
||||
|
||||
$absDatesAll = array_values(array_unique($byStudent[$sid]['absent'] ?? []));
|
||||
$absDatesAll = array_values(array_unique($byStudent[$sid]['absent'] ?? []));
|
||||
$lateDatesAll = array_values(array_unique($byStudent[$sid]['late'] ?? []));
|
||||
|
||||
$absDates = $keepLastWeeks($absDatesAll);
|
||||
$absDates = $keepLastWeeks($absDatesAll);
|
||||
$lateDates = $keepLastWeeks($lateDatesAll);
|
||||
|
||||
if (empty($absDates) && empty($lateDates)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$absWeekIdx = $this->datesToWeekIndices($absDates, $weekIndex);
|
||||
$absWeekIdx = $this->datesToWeekIndices($absDates, $weekIndex);
|
||||
$lateWeekIdx = $this->datesToWeekIndices($lateDates, $weekIndex);
|
||||
$absCountLast5 += count($absDates);
|
||||
$absCountLast5 += count($absDates);
|
||||
$lateCountLast5 += count($lateDates);
|
||||
|
||||
$absenceViolation = null;
|
||||
if (!empty($absDates)) {
|
||||
if (! empty($absDates)) {
|
||||
$lastAbsDate = $absDates[0];
|
||||
|
||||
$A2row = $this->hasNConsecutiveItems($absDates, 2, 7);
|
||||
@@ -188,47 +189,47 @@ class ViolationRuleEngineService
|
||||
|
||||
if ($A4row || $A_in5w4) {
|
||||
$absenceViolation = [
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_4',
|
||||
'severity' => 4,
|
||||
'action' => 'expel_notify',
|
||||
'color' => '#ff4d4f',
|
||||
'title' => '4 Absences (in a row or within last 5 active weeks)',
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_4',
|
||||
'severity' => 4,
|
||||
'action' => 'expel_notify',
|
||||
'color' => '#ff4d4f',
|
||||
'title' => '4 Absences (in a row or within last 5 active weeks)',
|
||||
'description' => 'Notify team to inform parent about expulsion and send email.',
|
||||
'last_date' => $lastAbsDate,
|
||||
'last_date' => $lastAbsDate,
|
||||
];
|
||||
} elseif ($A3row || $A_in4w3) {
|
||||
$absenceViolation = [
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_3',
|
||||
'severity' => 3,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#fa8c16',
|
||||
'title' => '3 Absences (in a row or within last 4 active weeks)',
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_3',
|
||||
'severity' => 3,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#fa8c16',
|
||||
'title' => '3 Absences (in a row or within last 4 active weeks)',
|
||||
'description' => 'Team calls parent and sends email.',
|
||||
'last_date' => $lastAbsDate,
|
||||
'last_date' => $lastAbsDate,
|
||||
];
|
||||
} elseif ($A2row || $A_in3w2) {
|
||||
$absenceViolation = [
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_2',
|
||||
'severity' => 2,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#fadb14',
|
||||
'title' => '2 Absences (in a row or within last 3 active weeks)',
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_2',
|
||||
'severity' => 2,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#fadb14',
|
||||
'title' => '2 Absences (in a row or within last 3 active weeks)',
|
||||
'description' => 'Team calls parent and sends email.',
|
||||
'last_date' => $lastAbsDate,
|
||||
'last_date' => $lastAbsDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$lateViolation = null;
|
||||
if (!empty($lateDates)) {
|
||||
if (! empty($lateDates)) {
|
||||
$lastLateDate = $lateDates[0];
|
||||
|
||||
$L2row = $this->hasNConsecutiveItems($lateDates, 2, 7);
|
||||
$L3row = $this->hasNConsecutiveItems($lateDates, 3, 7);
|
||||
$L4row = $this->hasNConsecutiveItems($lateDates, 4, 7);
|
||||
$L2row = $this->hasNConsecutiveItems($lateDates, 2, 7);
|
||||
$L3row = $this->hasNConsecutiveItems($lateDates, 3, 7);
|
||||
$L4row = $this->hasNConsecutiveItems($lateDates, 4, 7);
|
||||
|
||||
$L_in3w2 = $this->hasNInWActiveWeeks($lateWeekIdx, 2, 3);
|
||||
$L_in4w3 = $this->hasNInWActiveWeeks($lateWeekIdx, 3, 4);
|
||||
@@ -238,7 +239,7 @@ class ViolationRuleEngineService
|
||||
$lateWeeksSet = array_flip($lateWeekIdx);
|
||||
$lateCoversAllWindowWeeks = true;
|
||||
for ($i = $windowStartIdx; $i <= $currentWeekIdx; $i++) {
|
||||
if (!isset($lateWeeksSet[$i])) {
|
||||
if (! isset($lateWeeksSet[$i])) {
|
||||
$lateCoversAllWindowWeeks = false;
|
||||
break;
|
||||
}
|
||||
@@ -247,41 +248,41 @@ class ViolationRuleEngineService
|
||||
|
||||
if ($L4row || $lateCoversAllWindowWeeks) {
|
||||
$lateViolation = [
|
||||
'type' => 'late',
|
||||
'code' => 'LATE_4',
|
||||
'severity' => 4,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#ff4d4f',
|
||||
'title' => '4 Lates in a row OR in each of the last 4 active weeks',
|
||||
'type' => 'late',
|
||||
'code' => 'LATE_4',
|
||||
'severity' => 4,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#ff4d4f',
|
||||
'title' => '4 Lates in a row OR in each of the last 4 active weeks',
|
||||
'description' => 'Team calls parent and sends email.',
|
||||
'last_date' => $lastLateDate,
|
||||
'last_date' => $lastLateDate,
|
||||
];
|
||||
} else {
|
||||
$twoLatesOneAbsWithin4 = $this->twoLatesOneAbsInWWeeks($lateWeekIdx, $absWeekIdx, 4);
|
||||
|
||||
if ($L3row || $L_in4w3 || $twoLatesOneAbsWithin4) {
|
||||
$lateViolation = [
|
||||
'type' => $twoLatesOneAbsWithin4 ? 'mix' : 'late',
|
||||
'code' => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3',
|
||||
'severity' => 3,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#002766',
|
||||
'title' => $twoLatesOneAbsWithin4
|
||||
'type' => $twoLatesOneAbsWithin4 ? 'mix' : 'late',
|
||||
'code' => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3',
|
||||
'severity' => 3,
|
||||
'action' => 'team_notify',
|
||||
'color' => '#002766',
|
||||
'title' => $twoLatesOneAbsWithin4
|
||||
? '2 Lates + 1 Absence (within last 4 active weeks)'
|
||||
: '3 Lates (in a row or within last 4 active weeks)',
|
||||
'description' => 'Team calls parent and sends email.',
|
||||
'last_date' => $lastLateDate,
|
||||
'last_date' => $lastLateDate,
|
||||
];
|
||||
} elseif ($L2row || $L_in3w2) {
|
||||
$lateViolation = [
|
||||
'type' => 'late',
|
||||
'code' => 'LATE_2',
|
||||
'severity' => 1,
|
||||
'action' => 'auto_email',
|
||||
'color' => '#bae7ff',
|
||||
'title' => '2 Lates (in a row or within last 3 active weeks)',
|
||||
'type' => 'late',
|
||||
'code' => 'LATE_2',
|
||||
'severity' => 1,
|
||||
'action' => 'auto_email',
|
||||
'color' => '#bae7ff',
|
||||
'title' => '2 Lates (in a row or within last 3 active weeks)',
|
||||
'description' => 'Send automated email to parent.',
|
||||
'last_date' => $lastLateDate,
|
||||
'last_date' => $lastLateDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -289,20 +290,20 @@ class ViolationRuleEngineService
|
||||
|
||||
$chosen = $this->chooseHigherSeverity($absenceViolation, $lateViolation);
|
||||
|
||||
if (!$chosen && count($absDates) === 1) {
|
||||
if (! $chosen && count($absDates) === 1) {
|
||||
$chosen = [
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_1',
|
||||
'severity' => 1,
|
||||
'action' => 'auto_email',
|
||||
'color' => '#e6f7ff',
|
||||
'title' => '1 Unreported Absence',
|
||||
'type' => 'absence',
|
||||
'code' => 'ABS_1',
|
||||
'severity' => 1,
|
||||
'action' => 'auto_email',
|
||||
'color' => '#e6f7ff',
|
||||
'title' => '1 Unreported Absence',
|
||||
'description' => 'Send automated email to parent.',
|
||||
'last_date' => $absDates[0],
|
||||
'last_date' => $absDates[0],
|
||||
];
|
||||
}
|
||||
|
||||
if (!$chosen) {
|
||||
if (! $chosen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -315,51 +316,51 @@ class ViolationRuleEngineService
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $end)
|
||||
->where('reason', 'like', '%' . $chosen['code'] . '%');
|
||||
->where('reason', 'like', '%'.$chosen['code'].'%');
|
||||
|
||||
if ($semester !== null) {
|
||||
$trackingQ->where('semester', $semester);
|
||||
}
|
||||
|
||||
$tracking = $trackingQ->orderByDesc('date')->first();
|
||||
$tracking = $tracking?->toArray() ?? [];
|
||||
$tracking = $trackingQ->orderByDesc('date')->first();
|
||||
$tracking = $tracking?->toArray() ?? [];
|
||||
$isNotified = (bool) ($tracking['is_notified'] ?? 0);
|
||||
$notifCnt = (int) ($tracking['notif_counter'] ?? 0);
|
||||
$notifCnt = (int) ($tracking['notif_counter'] ?? 0);
|
||||
|
||||
$cls = $classByStudent[$sid] ?? [];
|
||||
$classLabel = (string) ($cls['class_section_name'] ?? '');
|
||||
if ($classLabel === '' && !empty($cls['class_name'])) {
|
||||
if ($classLabel === '' && ! empty($cls['class_name'])) {
|
||||
$classLabel = (string) $cls['class_name'];
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'id' => $sid,
|
||||
'name' => $name,
|
||||
'class_name' => $classLabel,
|
||||
'id' => $sid,
|
||||
'name' => $name,
|
||||
'class_name' => $classLabel,
|
||||
'class_section_name' => (string) ($cls['class_section_name'] ?? ''),
|
||||
'className' => (string) ($cls['class_name'] ?? ''),
|
||||
'class_id' => $cls['class_id'] ?? null,
|
||||
'className' => (string) ($cls['class_name'] ?? ''),
|
||||
'class_id' => $cls['class_id'] ?? null,
|
||||
'class_section_id' => $cls['class_section_id'] ?? null,
|
||||
'parent_email' => $parent['email'] ?? '',
|
||||
'parent_name' => $parent['parent_name'] ?? '',
|
||||
'parent_phone' => $parent['phone'] ?? null,
|
||||
'violation' => $chosen['title'],
|
||||
'violation_code' => $chosen['code'],
|
||||
'type' => $chosen['type'],
|
||||
'severity' => $chosen['severity'],
|
||||
'action' => $chosen['action'],
|
||||
'color' => $chosen['color'],
|
||||
'last_absence' => $absDates[0] ?? null,
|
||||
'last_late' => $lateDates[0] ?? null,
|
||||
'last_date' => $chosen['last_date'],
|
||||
'is_notified' => $isNotified,
|
||||
'notif_counter' => $notifCnt,
|
||||
'absences' => $absDates,
|
||||
'lates' => $lateDates,
|
||||
'parent_email' => $parent['email'] ?? '',
|
||||
'parent_name' => $parent['parent_name'] ?? '',
|
||||
'parent_phone' => $parent['phone'] ?? null,
|
||||
'violation' => $chosen['title'],
|
||||
'violation_code' => $chosen['code'],
|
||||
'type' => $chosen['type'],
|
||||
'severity' => $chosen['severity'],
|
||||
'action' => $chosen['action'],
|
||||
'color' => $chosen['color'],
|
||||
'last_absence' => $absDates[0] ?? null,
|
||||
'last_late' => $lateDates[0] ?? null,
|
||||
'last_date' => $chosen['last_date'],
|
||||
'is_notified' => $isNotified,
|
||||
'notif_counter' => $notifCnt,
|
||||
'absences' => $absDates,
|
||||
'lates' => $lateDates,
|
||||
];
|
||||
}
|
||||
|
||||
$this->debugCompute['abs_last5'] = $absCountLast5;
|
||||
$this->debugCompute['abs_last5'] = $absCountLast5;
|
||||
$this->debugCompute['late_last5'] = $lateCountLast5;
|
||||
|
||||
return $out;
|
||||
@@ -370,8 +371,10 @@ class ViolationRuleEngineService
|
||||
if ($a && $b) {
|
||||
if ($a['severity'] === $b['severity']) {
|
||||
$prio = ['expel_notify' => 3, 'team_notify' => 2, 'auto_email' => 1];
|
||||
|
||||
return ($prio[$a['action']] ?? 0) >= ($prio[$b['action']] ?? 0) ? $a : $b;
|
||||
}
|
||||
|
||||
return $a['severity'] > $b['severity'] ? $a : $b;
|
||||
}
|
||||
|
||||
@@ -386,7 +389,7 @@ class ViolationRuleEngineService
|
||||
|
||||
$this->attendanceReportedColumns = [
|
||||
'is_reported' => Schema::hasColumn($table, 'is_reported'),
|
||||
'reported' => Schema::hasColumn($table, 'reported'),
|
||||
'reported' => Schema::hasColumn($table, 'reported'),
|
||||
];
|
||||
|
||||
return $this->attendanceReportedColumns;
|
||||
@@ -396,15 +399,15 @@ class ViolationRuleEngineService
|
||||
{
|
||||
$cols = $this->attendanceReportedColumns($table);
|
||||
|
||||
if (!empty($cols['is_reported'])) {
|
||||
if (! empty($cols['is_reported'])) {
|
||||
$qb->whereRaw("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))");
|
||||
}
|
||||
if (!empty($cols['reported'])) {
|
||||
if (! empty($cols['reported'])) {
|
||||
$qb->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))");
|
||||
}
|
||||
|
||||
$qb->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent-reported%')")
|
||||
->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')");
|
||||
->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')");
|
||||
|
||||
if (Schema::hasTable('parent_attendance_reports')) {
|
||||
$qb->whereRaw("NOT EXISTS (
|
||||
@@ -418,14 +421,16 @@ class ViolationRuleEngineService
|
||||
|
||||
public function deriveSchoolYearBounds(string $schoolYear): array
|
||||
{
|
||||
if (!preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) {
|
||||
$y = (int) date('Y');
|
||||
if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) {
|
||||
$y = (int) date('Y');
|
||||
$startY = (int) (date('n') >= 8 ? $y : $y - 1);
|
||||
|
||||
return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $startY + 1)];
|
||||
}
|
||||
|
||||
$startY = (int) $m[1];
|
||||
$endY = (int) $m[2];
|
||||
$endY = (int) $m[2];
|
||||
|
||||
return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $endY)];
|
||||
}
|
||||
|
||||
@@ -446,9 +451,10 @@ class ViolationRuleEngineService
|
||||
public function dayBounds(string $ymd): array
|
||||
{
|
||||
$d = substr($ymd, 0, 10);
|
||||
|
||||
return [
|
||||
$d . ' 00:00:00',
|
||||
date('Y-m-d', strtotime($d . ' +1 day')) . ' 00:00:00',
|
||||
$d.' 00:00:00',
|
||||
date('Y-m-d', strtotime($d.' +1 day')).' 00:00:00',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -461,10 +467,10 @@ class ViolationRuleEngineService
|
||||
): array {
|
||||
$weeks = [];
|
||||
|
||||
if (!empty($weekRowsFallback)) {
|
||||
if (! empty($weekRowsFallback)) {
|
||||
foreach ($weekRowsFallback as $r) {
|
||||
$d = $r['date'] ?? null;
|
||||
if (!$d) {
|
||||
if (! $d) {
|
||||
continue;
|
||||
}
|
||||
$ts = strtotime($d);
|
||||
@@ -490,10 +496,10 @@ class ViolationRuleEngineService
|
||||
$q->whereNull('reason')
|
||||
->orWhere(function ($q2) {
|
||||
$q2->where('reason', 'not like', '%break%')
|
||||
->where('reason', 'not like', '%cancel%');
|
||||
->where('reason', 'not like', '%cancel%');
|
||||
});
|
||||
})
|
||||
->when($semester !== null && $semester !== '', fn($q) => $q->where('semester', $semester))
|
||||
->when($semester !== null && $semester !== '', fn ($q) => $q->where('semester', $semester))
|
||||
->groupBy(DB::raw('DATE(date)'))
|
||||
->orderBy('ymd')
|
||||
->get();
|
||||
@@ -521,6 +527,7 @@ class ViolationRuleEngineService
|
||||
}
|
||||
$idx = array_keys($set);
|
||||
sort($idx);
|
||||
|
||||
return $idx;
|
||||
}
|
||||
|
||||
@@ -542,14 +549,16 @@ class ViolationRuleEngineService
|
||||
if (Str::startsWith($code, 'ABS_4')) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
public function isoWeekStartYmd(string $weekKey): string
|
||||
{
|
||||
if (preg_match('/^(\d{4})-W(\d{2})$/', $weekKey, $m)) {
|
||||
return date('Y-m-d', strtotime($m[1] . '-W' . $m[2] . '-1'));
|
||||
return date('Y-m-d', strtotime($m[1].'-W'.$m[2].'-1'));
|
||||
}
|
||||
|
||||
return date('Y-m-d');
|
||||
}
|
||||
|
||||
@@ -571,14 +580,15 @@ class ViolationRuleEngineService
|
||||
|
||||
if (empty($activeWeekKeys)) {
|
||||
$days = ($windowWeeks * 7) - 1;
|
||||
return date('Y-m-d', strtotime($endBound . ' -' . $days . ' days'));
|
||||
|
||||
return date('Y-m-d', strtotime($endBound.' -'.$days.' days'));
|
||||
}
|
||||
|
||||
$activeWeekKeys = array_values($activeWeekKeys);
|
||||
$weekIndex = array_flip($activeWeekKeys);
|
||||
$endWeekKey = date('o-\WW', strtotime($endBound));
|
||||
|
||||
if (!isset($weekIndex[$endWeekKey])) {
|
||||
if (! isset($weekIndex[$endWeekKey])) {
|
||||
$endWeekKey = null;
|
||||
for ($i = count($activeWeekKeys) - 1; $i >= 0; $i--) {
|
||||
$wkStart = $this->isoWeekStartYmd($activeWeekKeys[$i]);
|
||||
@@ -662,11 +672,12 @@ class ViolationRuleEngineService
|
||||
|
||||
while ($iL2 < count($L)) {
|
||||
$windowStart = $L[$iL1];
|
||||
$windowEnd = $windowStart + ($w - 1);
|
||||
$windowEnd = $windowStart + ($w - 1);
|
||||
|
||||
if ($L[$iL2] > $windowEnd) {
|
||||
$iL1++;
|
||||
$iL2 = $iL1 + 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -687,4 +698,4 @@ class ViolationRuleEngineService
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\IpAttempt;
|
||||
use App\Models\LoginActivity;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -22,7 +22,7 @@ class ApiLoginSecurityService
|
||||
public function isIpBlocked(string $ip): bool
|
||||
{
|
||||
$row = IpAttempt::query()->where('ip_address', $ip)->first();
|
||||
if (!$row || !$row->blocked_until) {
|
||||
if (! $row || ! $row->blocked_until) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Models\LoginActivity;
|
||||
use App\Models\Preferences;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -21,8 +20,7 @@ class AuthSessionService
|
||||
|
||||
public function __construct(
|
||||
private ApplicationUrlService $urls,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sanitizes login redirects to same-host relative paths only.
|
||||
|
||||
@@ -12,7 +12,7 @@ class PermissionCheckService
|
||||
->where('name', $permissionName)
|
||||
->value('id');
|
||||
|
||||
if (!$permissionId) {
|
||||
if (! $permissionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ class RegistrationCaptchaService
|
||||
$ip = (string) (request()?->ip() ?? 'unknown');
|
||||
$ua = (string) (request()?->header('User-Agent') ?? '');
|
||||
$hash = substr(sha1($ua), 0, 12);
|
||||
return 'captcha:' . $ip . ':' . $hash;
|
||||
|
||||
return 'captcha:'.$ip.':'.$hash;
|
||||
}
|
||||
|
||||
public function generate(?int $length = null): string
|
||||
|
||||
@@ -6,9 +6,7 @@ use App\Services\PhoneFormatterService;
|
||||
|
||||
class RegistrationFormatterService
|
||||
{
|
||||
public function __construct(private PhoneFormatterService $phoneFormatter)
|
||||
{
|
||||
}
|
||||
public function __construct(private PhoneFormatterService $phoneFormatter) {}
|
||||
|
||||
public function format(array $payload): array
|
||||
{
|
||||
@@ -30,7 +28,7 @@ class RegistrationFormatterService
|
||||
'accept_school_policy' => (int) ($payload['accept_school_policy'] ?? 0),
|
||||
];
|
||||
|
||||
$noSecondInfo = !empty($payload['no_second_parent_info']);
|
||||
$noSecondInfo = ! empty($payload['no_second_parent_info']);
|
||||
|
||||
$secondFirstname = $this->formatName($g('second_firstname'));
|
||||
$secondLastname = $this->formatName($g('second_lastname'));
|
||||
@@ -38,7 +36,7 @@ class RegistrationFormatterService
|
||||
$secondEmail = $this->formatEmail($g('second_email'));
|
||||
$secondCellphone = $this->phoneFormatter->formatPhoneNumber($g('second_cellphone'));
|
||||
|
||||
$secondProvided = !$noSecondInfo && (
|
||||
$secondProvided = ! $noSecondInfo && (
|
||||
$secondFirstname !== '' ||
|
||||
$secondLastname !== '' ||
|
||||
$secondGender !== '' ||
|
||||
@@ -64,7 +62,7 @@ class RegistrationFormatterService
|
||||
return '';
|
||||
}
|
||||
|
||||
return ucwords(strtolower($name), " -");
|
||||
return ucwords(strtolower($name), ' -');
|
||||
}
|
||||
|
||||
private function formatEmail(string $email): string
|
||||
|
||||
@@ -20,13 +20,12 @@ class RegistrationService
|
||||
private RegistrationFormatterService $formatter,
|
||||
private RegistrationCaptchaService $captchaService,
|
||||
private ApplicationUrlService $urls,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function register(array $payload): array
|
||||
{
|
||||
$captcha = (string) ($payload['captcha'] ?? '');
|
||||
if (!$this->captchaService->verify($captcha)) {
|
||||
if (! $this->captchaService->verify($captcha)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'code' => 'captcha_invalid',
|
||||
@@ -34,15 +33,15 @@ class RegistrationService
|
||||
];
|
||||
}
|
||||
|
||||
$isParent = !empty($payload['is_parent']);
|
||||
$noSecondInfo = !empty($payload['no_second_parent_info']);
|
||||
$isParent = ! empty($payload['is_parent']);
|
||||
$noSecondInfo = ! empty($payload['no_second_parent_info']);
|
||||
|
||||
$formatted = $this->formatter->format($payload);
|
||||
$email = (string) ($formatted['email'] ?? '');
|
||||
|
||||
$existing = User::query()->where('email', $email)->first();
|
||||
if ($existing) {
|
||||
$pending = !empty($existing->token) && (int) ($existing->is_verified ?? 0) === 0;
|
||||
$pending = ! empty($existing->token) && (int) ($existing->is_verified ?? 0) === 0;
|
||||
if ($pending) {
|
||||
return [
|
||||
'ok' => false,
|
||||
@@ -60,7 +59,7 @@ class RegistrationService
|
||||
|
||||
$roleName = $isParent ? 'parent' : 'guest';
|
||||
$role = Role::query()->where('name', $roleName)->first();
|
||||
if (!$role) {
|
||||
if (! $role) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'code' => 'role_missing',
|
||||
@@ -113,7 +112,7 @@ class RegistrationService
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
if ($isParent && !$noSecondInfo && !empty($formatted['second_firstname'])) {
|
||||
if ($isParent && ! $noSecondInfo && ! empty($formatted['second_firstname'])) {
|
||||
ParentModel::query()->create([
|
||||
'secondparent_firstname' => $formatted['second_firstname'] ?? null,
|
||||
'secondparent_lastname' => $formatted['second_lastname'] ?? null,
|
||||
@@ -128,12 +127,12 @@ class RegistrationService
|
||||
});
|
||||
|
||||
$activationLink = $this->urls->spaRegistrationConfirmUrl($token);
|
||||
$recipientName = trim((string) ($formatted['firstname'] ?? '') . ' ' . (string) ($formatted['lastname'] ?? ''));
|
||||
$recipientName = trim((string) ($formatted['firstname'] ?? '').' '.(string) ($formatted['lastname'] ?? ''));
|
||||
|
||||
$html = '<p>Hello ' . e($recipientName !== '' ? $recipientName : 'there') . ',</p>'
|
||||
. '<p>Please confirm your email by clicking the link below:</p>'
|
||||
. '<p><a href="' . e($activationLink) . '">Activate your account</a></p>'
|
||||
. '<p>Thank you.</p>';
|
||||
$html = '<p>Hello '.e($recipientName !== '' ? $recipientName : 'there').',</p>'
|
||||
.'<p>Please confirm your email by clicking the link below:</p>'
|
||||
.'<p><a href="'.e($activationLink).'">Activate your account</a></p>'
|
||||
.'<p>Thank you.</p>';
|
||||
|
||||
$sent = $this->emailService->send($email, 'Email Confirmation', $html, 'general');
|
||||
$this->captchaService->clear();
|
||||
|
||||
@@ -35,16 +35,16 @@ class UserRoleService
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($crudAction === 'create' && !empty($row->can_create)) {
|
||||
if ($crudAction === 'create' && ! empty($row->can_create)) {
|
||||
return true;
|
||||
}
|
||||
if ($crudAction === 'read' && !empty($row->can_read)) {
|
||||
if ($crudAction === 'read' && ! empty($row->can_read)) {
|
||||
return true;
|
||||
}
|
||||
if ($crudAction === 'update' && !empty($row->can_update)) {
|
||||
if ($crudAction === 'update' && ! empty($row->can_update)) {
|
||||
return true;
|
||||
}
|
||||
if ($crudAction === 'delete' && !empty($row->can_delete)) {
|
||||
if ($crudAction === 'delete' && ! empty($row->can_delete)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Services\BadgeScan;
|
||||
|
||||
use App\Controllers\View\RFIDController;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* legacy {@see \App\Controllers\View\RFIDController} parity (badge scan + log listing).
|
||||
* legacy {@see RFIDController} parity (badge scan + log listing).
|
||||
*/
|
||||
class BadgeScanService
|
||||
{
|
||||
|
||||
@@ -8,8 +8,7 @@ class BadgeFormDataService
|
||||
protected BadgeUserLookupService $lookupService,
|
||||
protected BadgeStudentLookupService $studentLookupService,
|
||||
protected BadgeTextFormatter $formatter
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function build(
|
||||
?string $schoolYear,
|
||||
@@ -24,7 +23,7 @@ class BadgeFormDataService
|
||||
$students = $this->studentLookupService->fetchStudentList($schoolYear, $selectedStudentIds);
|
||||
|
||||
foreach ($users as &$u) {
|
||||
if (!isset($u['user_id']) && isset($u['id'])) {
|
||||
if (! isset($u['user_id']) && isset($u['id'])) {
|
||||
$u['user_id'] = (int) $u['id'];
|
||||
}
|
||||
|
||||
@@ -46,7 +45,7 @@ class BadgeFormDataService
|
||||
$u['class_section_id'] = $u['class_section_id'] ?? null;
|
||||
$u['class_section_name'] = $u['class_section_name'] ?? null;
|
||||
$u['entity_type'] = 'user';
|
||||
$u['row_id'] = 'user:' . (int) ($u['user_id'] ?? 0);
|
||||
$u['row_id'] = 'user:'.(int) ($u['user_id'] ?? 0);
|
||||
$u['role_group'] = $this->detectRoleGroup(
|
||||
strtolower((string) ($u['roles_raw'] ?? ($u['role_name_raw'] ?? '')))
|
||||
);
|
||||
@@ -57,7 +56,7 @@ class BadgeFormDataService
|
||||
$rolesDetect = strtolower((string) ($u['roles_raw'] ?? ($u['role_name_raw'] ?? '')));
|
||||
$isTeacherish = str_contains($rolesDetect, 'teacher') || preg_match('/\bta\b/', $rolesDetect);
|
||||
|
||||
if ($isTeacherish && !empty($u['user_id'])) {
|
||||
if ($isTeacherish && ! empty($u['user_id'])) {
|
||||
$assign = $this->lookupService->getLatestClassForUser((int) $u['user_id'], $schoolYear);
|
||||
if ($assign) {
|
||||
$u['class_section_id'] = $assign['class_section_id'] ?? null;
|
||||
@@ -69,7 +68,7 @@ class BadgeFormDataService
|
||||
|
||||
foreach ($students as &$student) {
|
||||
$student['entity_type'] = 'student';
|
||||
$student['row_id'] = 'student:' . (int) ($student['student_id'] ?? 0);
|
||||
$student['row_id'] = 'student:'.(int) ($student['student_id'] ?? 0);
|
||||
$student['role_group'] = 'student';
|
||||
$student['class_section_id'] = null;
|
||||
|
||||
@@ -84,17 +83,17 @@ class BadgeFormDataService
|
||||
$rows = array_merge($users, $students);
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
return strcasecmp(
|
||||
trim((string) ($a['lastname'] ?? '')) . ' ' . trim((string) ($a['firstname'] ?? '')),
|
||||
trim((string) ($b['lastname'] ?? '')) . ' ' . trim((string) ($b['firstname'] ?? ''))
|
||||
trim((string) ($a['lastname'] ?? '')).' '.trim((string) ($a['firstname'] ?? '')),
|
||||
trim((string) ($b['lastname'] ?? '')).' '.trim((string) ($b['firstname'] ?? ''))
|
||||
);
|
||||
});
|
||||
|
||||
$rolesTabs = [
|
||||
'all' => 'All',
|
||||
'all' => 'All',
|
||||
'teacher' => 'Teachers',
|
||||
'ta' => 'Teacher Assistants',
|
||||
'admin' => 'Admins',
|
||||
'staff' => 'Staff',
|
||||
'ta' => 'Teacher Assistants',
|
||||
'admin' => 'Admins',
|
||||
'staff' => 'Staff',
|
||||
'student' => 'Students',
|
||||
];
|
||||
|
||||
@@ -103,13 +102,13 @@ class BadgeFormDataService
|
||||
: 'all';
|
||||
|
||||
return [
|
||||
'users' => $rows,
|
||||
'schoolYears' => $this->lookupService->getAvailableSchoolYears(),
|
||||
'selectedYear' => $schoolYear,
|
||||
'selectedUserIds' => $selectedUserIds,
|
||||
'users' => $rows,
|
||||
'schoolYears' => $this->lookupService->getAvailableSchoolYears(),
|
||||
'selectedYear' => $schoolYear,
|
||||
'selectedUserIds' => $selectedUserIds,
|
||||
'selectedStudentIds' => $selectedStudentIds,
|
||||
'rolesTabs' => $rolesTabs,
|
||||
'active_role' => $activeRole,
|
||||
'rolesTabs' => $rolesTabs,
|
||||
'active_role' => $activeRole,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,13 @@ class BadgePdfService
|
||||
protected BadgeUserLookupService $userLookupService,
|
||||
protected BadgePrintLogService $printLogService,
|
||||
protected BadgeTextFormatter $formatter,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Letter landscape PDF, 8 badges per page — students and/or staff (FPDF + QR PNG).
|
||||
*
|
||||
* @param int[] $studentIds Primary keys on `students.id`
|
||||
* @param int[] $userIds Staff/admin/teacher keys on `users.id`
|
||||
* @param int[] $userIds Staff/admin/teacher keys on `users.id`
|
||||
*/
|
||||
public function generate(
|
||||
array $studentIds,
|
||||
@@ -86,7 +85,7 @@ class BadgePdfService
|
||||
|
||||
try {
|
||||
$qrPayload = (string) ($staffRow['user_id_for_qr'] ?? $staffRow['user_id'] ?? '');
|
||||
if ($qrPayload === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $qrPayload)) {
|
||||
if ($qrPayload === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $qrPayload)) {
|
||||
$qrPayload = (string) (int) ($staffRow['user_id'] ?? 0);
|
||||
}
|
||||
|
||||
@@ -144,7 +143,7 @@ class BadgePdfService
|
||||
): string {
|
||||
$html = $this->buildHtmlDocument($rows, $schoolName, $motto, $footerBlock);
|
||||
|
||||
$options = new Options();
|
||||
$options = new Options;
|
||||
$options->set('isHtml5ParserEnabled', true);
|
||||
$options->set('isRemoteEnabled', true);
|
||||
$options->set('defaultFont', 'DejaVu Sans');
|
||||
@@ -168,7 +167,7 @@ class BadgePdfService
|
||||
$this->tempQrFiles = [];
|
||||
|
||||
try {
|
||||
if (!class_exists('FPDF', false)) {
|
||||
if (! class_exists('FPDF', false)) {
|
||||
require_once base_path('app/ThirdParty/fpdf/fpdf.php');
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ class BadgePdfService
|
||||
for ($gridCol = 0; $gridCol < 4; $gridCol++) {
|
||||
$idx = ($gridRow * 4) + $gridCol;
|
||||
$row = $slots[$idx];
|
||||
if (!is_array($row)) {
|
||||
if (! is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -262,7 +261,7 @@ class BadgePdfService
|
||||
array $classesMap
|
||||
): ?array {
|
||||
$info = $this->userLookupService->getUserInfoById($userId, $schoolYear);
|
||||
if (!$info || !is_array($info)) {
|
||||
if (! $info || ! is_array($info)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -276,7 +275,7 @@ class BadgePdfService
|
||||
$roleResolved = $this->formatter->formatRole($roleResolved);
|
||||
$info['role_resolved'] = $roleResolved;
|
||||
|
||||
if (!empty($classesMap[$resolvedId])) {
|
||||
if (! empty($classesMap[$resolvedId])) {
|
||||
$info['class_section_name'] = (string) $classesMap[$resolvedId];
|
||||
}
|
||||
|
||||
@@ -291,11 +290,11 @@ class BadgePdfService
|
||||
|
||||
if ($isTeacherish && $class !== '') {
|
||||
if (strtolower($class) === 'youth') {
|
||||
$display = 'Youth ' . $roleResolved;
|
||||
$display = 'Youth '.$roleResolved;
|
||||
} elseif ($class === 'KG') {
|
||||
$display = 'KG ' . $roleResolved;
|
||||
$display = 'KG '.$roleResolved;
|
||||
} else {
|
||||
$display = 'Grade ' . $class . ' ' . $roleResolved;
|
||||
$display = 'Grade '.$class.' '.$roleResolved;
|
||||
}
|
||||
} elseif ($roleResolved !== '') {
|
||||
$display = $roleResolved;
|
||||
@@ -309,7 +308,7 @@ class BadgePdfService
|
||||
$year = $this->formatter->norm((string) ($info['school_year'] ?? ($schoolYear ?? '')));
|
||||
$schoolId = trim((string) ($info['school_id'] ?? ''));
|
||||
|
||||
if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -364,10 +363,10 @@ class BadgePdfService
|
||||
}
|
||||
|
||||
return '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Badges PDF</title><style>'
|
||||
. $this->badgeCss()
|
||||
. '</style></head><body>'
|
||||
. $badgesHtml
|
||||
. '</body></html>';
|
||||
.$this->badgeCss()
|
||||
.'</style></head><body>'
|
||||
.$badgesHtml
|
||||
.'</body></html>';
|
||||
}
|
||||
|
||||
protected function buildBadgeHtml(
|
||||
@@ -376,8 +375,7 @@ class BadgePdfService
|
||||
string $motto,
|
||||
string $footerBlock,
|
||||
?string $logoDataUri
|
||||
): string
|
||||
{
|
||||
): string {
|
||||
$kind = (string) ($row['_badge_kind'] ?? 'student');
|
||||
$idLabel = $kind === 'staff' ? 'User ID' : 'School ID';
|
||||
$title = $this->escapeHtml((string) ($row['fullname'] ?? ''));
|
||||
@@ -404,8 +402,8 @@ class BadgePdfService
|
||||
<div class="info-row">
|
||||
<span class="icon-badge">G</span>
|
||||
<span class="info-copy">
|
||||
<span class="label">' . $gradeLabel . '</span>
|
||||
<span class="value">' . $grade . '</span>
|
||||
<span class="label">'.$gradeLabel.'</span>
|
||||
<span class="value">'.$grade.'</span>
|
||||
</span>
|
||||
</div>';
|
||||
}
|
||||
@@ -415,15 +413,15 @@ class BadgePdfService
|
||||
<span class="icon-badge">Y</span>
|
||||
<span class="info-copy">
|
||||
<span class="label">Academic Year</span>
|
||||
<span class="value">' . $year . '</span>
|
||||
<span class="value">'.$year.'</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="icon-badge">ID</span>
|
||||
<span class="info-copy">
|
||||
<span class="label">' . $this->escapeHtml($idLabel) . '</span>
|
||||
<span class="value">' . $schoolId . '</span>
|
||||
<span class="label">'.$this->escapeHtml($idLabel).'</span>
|
||||
<span class="value">'.$schoolId.'</span>
|
||||
</span>
|
||||
</div>';
|
||||
|
||||
@@ -432,18 +430,18 @@ class BadgePdfService
|
||||
<div class="header-wrap">
|
||||
<div class="header">
|
||||
<div class="header-mark">'
|
||||
. ($logoDataUri !== null
|
||||
? '<img src="' . $logoDataUri . '" alt="School Logo">'
|
||||
: '<span>AS</span>') .
|
||||
.($logoDataUri !== null
|
||||
? '<img src="'.$logoDataUri.'" alt="School Logo">'
|
||||
: '<span>AS</span>').
|
||||
'</div>
|
||||
<div class="header-copy">
|
||||
<div class="school-name">' . $this->escapeHtml($headerMainTitle) . '</div>
|
||||
' . ($schoolLevel !== ''
|
||||
? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>'
|
||||
: '') . '
|
||||
' . ($headerMotto !== ''
|
||||
? '<div class="motto">' . $this->escapeHtml($headerMotto) . '</div>'
|
||||
: '') . '
|
||||
<div class="school-name">'.$this->escapeHtml($headerMainTitle).'</div>
|
||||
'.($schoolLevel !== ''
|
||||
? '<div class="sub-school">'.$this->escapeHtml($schoolLevel).'</div>'
|
||||
: '').'
|
||||
'.($headerMotto !== ''
|
||||
? '<div class="motto">'.$this->escapeHtml($headerMotto).'</div>'
|
||||
: '').'
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-accent"></div>
|
||||
@@ -451,25 +449,25 @@ class BadgePdfService
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="student-name">' . mb_strtoupper($title, 'UTF-8') . '</div>
|
||||
<div class="student-name">'.mb_strtoupper($title, 'UTF-8').'</div>
|
||||
<div class="name-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-dot"></span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
<div class="role">' . mb_strtoupper($role, 'UTF-8') . '</div>
|
||||
<div class="role">'.mb_strtoupper($role, 'UTF-8').'</div>
|
||||
|
||||
<table class="content">
|
||||
<tr>
|
||||
<td class="info">
|
||||
' . $infoRows . '
|
||||
'.$infoRows.'
|
||||
</td>
|
||||
|
||||
<td class="qr-col">
|
||||
<div class="qr-box">'
|
||||
. ($qrSrc !== ''
|
||||
? '<img src="' . $qrSrc . '" alt="QR Code">'
|
||||
: '') .
|
||||
.($qrSrc !== ''
|
||||
? '<img src="'.$qrSrc.'" alt="QR Code">'
|
||||
: '').
|
||||
'</div>
|
||||
<div class="scan-pill">Scan to verify</div>
|
||||
</td>
|
||||
@@ -477,18 +475,18 @@ class BadgePdfService
|
||||
</table>
|
||||
|
||||
<div class="barcode-wrap">
|
||||
<div class="barcode-bars">' . $this->buildBarcodeHtml($barcodeValue) . '</div>
|
||||
<div class="barcode-text">' . $barcodeText . '</div>
|
||||
<div class="barcode-bars">'.$this->buildBarcodeHtml($barcodeValue).'</div>
|
||||
<div class="barcode-text">'.$barcodeText.'</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span class="footer-mark">'
|
||||
. ($logoDataUri !== null
|
||||
? '<img src="' . $logoDataUri . '" alt="School Logo">'
|
||||
: '<span>S</span>') .
|
||||
.($logoDataUri !== null
|
||||
? '<img src="'.$logoDataUri.'" alt="School Logo">'
|
||||
: '<span>S</span>').
|
||||
'</span>
|
||||
<span class="footer-copy">' . $footer . '</span>
|
||||
<span class="footer-copy">'.$footer.'</span>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
@@ -899,7 +897,7 @@ CSS;
|
||||
default => '4px',
|
||||
};
|
||||
|
||||
$bars[] = '<span class="barcode-bar" style="width:' . $width . '"></span>';
|
||||
$bars[] = '<span class="barcode-bar" style="width:'.$width.'"></span>';
|
||||
$bars[] = '<span class="barcode-bar" style="width:1px"></span>';
|
||||
}
|
||||
|
||||
@@ -917,7 +915,7 @@ CSS;
|
||||
return null;
|
||||
}
|
||||
|
||||
$tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_qr_' . bin2hex(random_bytes(8)) . '.png';
|
||||
$tmpPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'badge_qr_'.bin2hex(random_bytes(8)).'.png';
|
||||
if (@file_put_contents($tmpPath, $png) === false) {
|
||||
return null;
|
||||
}
|
||||
@@ -1002,11 +1000,11 @@ CSS;
|
||||
{
|
||||
return $this->formatter->firstExisting([
|
||||
public_path($filename),
|
||||
public_path('badges/' . $filename),
|
||||
public_path('assets/images/' . $filename),
|
||||
public_path('assets/images/badges/' . $filename),
|
||||
storage_path('app/public/' . $filename),
|
||||
storage_path('app/public/badges/' . $filename),
|
||||
public_path('badges/'.$filename),
|
||||
public_path('assets/images/'.$filename),
|
||||
public_path('assets/images/badges/'.$filename),
|
||||
storage_path('app/public/'.$filename),
|
||||
storage_path('app/public/badges/'.$filename),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1087,7 +1085,7 @@ CSS;
|
||||
/**
|
||||
* Shrink text until it fits in the available width. FPDF, bravely, does not do this for us.
|
||||
*
|
||||
* @param array{0:int,1:int,2:int} $rgb
|
||||
* @param array{0:int,1:int,2:int} $rgb
|
||||
*/
|
||||
protected function fitCenteredText(
|
||||
\FPDF $pdf,
|
||||
@@ -1149,7 +1147,7 @@ CSS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{0:int,1:int,2:int} $rgb
|
||||
* @param array{0:int,1:int,2:int} $rgb
|
||||
*/
|
||||
protected function fitLeftText(
|
||||
\FPDF $pdf,
|
||||
@@ -1447,6 +1445,7 @@ CSS;
|
||||
foreach ($segments as [$type, $segmentWidth]) {
|
||||
if ($type === 'gap') {
|
||||
$cursor += $segmentWidth;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1510,7 +1509,7 @@ CSS;
|
||||
protected function preparedLogoPath(): ?string
|
||||
{
|
||||
$path = $this->logoFilePath();
|
||||
if ($path === null || !is_readable($path)) {
|
||||
if ($path === null || ! is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1519,7 +1518,7 @@ CSS;
|
||||
return $path;
|
||||
}
|
||||
|
||||
$tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_logo_' . bin2hex(random_bytes(8)) . '.png';
|
||||
$tmpPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'badge_logo_'.bin2hex(random_bytes(8)).'.png';
|
||||
if (@file_put_contents($tmpPath, $png) === false) {
|
||||
return $path;
|
||||
}
|
||||
@@ -1532,7 +1531,7 @@ CSS;
|
||||
protected function logoDataUri(): ?string
|
||||
{
|
||||
$path = $this->logoFilePath();
|
||||
if ($path === null || !is_readable($path)) {
|
||||
if ($path === null || ! is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1544,10 +1543,9 @@ CSS;
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'data:image/png;base64,' . base64_encode($bytes);
|
||||
return 'data:image/png;base64,'.base64_encode($bytes);
|
||||
}
|
||||
|
||||
|
||||
protected function headerMainTitle(string $schoolName): string
|
||||
{
|
||||
$name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School';
|
||||
@@ -1579,7 +1577,7 @@ CSS;
|
||||
{
|
||||
$address = trim((string) config('badges.footer_address', ''));
|
||||
if ($address !== '') {
|
||||
return $schoolName . "\n" . $address;
|
||||
return $schoolName."\n".$address;
|
||||
}
|
||||
|
||||
return $schoolName;
|
||||
@@ -1587,7 +1585,7 @@ CSS;
|
||||
|
||||
protected function makeCircularPngBytesFromPath(string $path): ?string
|
||||
{
|
||||
if (!function_exists('imagecreatetruecolor') || !function_exists('imagepng')) {
|
||||
if (! function_exists('imagecreatetruecolor') || ! function_exists('imagepng')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1626,6 +1624,7 @@ CSS;
|
||||
$circle = imagecreatetruecolor($size, $size);
|
||||
if ($circle === false) {
|
||||
imagedestroy($square);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ class BadgePrintLogService
|
||||
public function __construct(
|
||||
protected BadgePrintLog $badgePrintLogModel,
|
||||
protected BadgeTextFormatter $formatter
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getStatus(array $userIds, ?string $schoolYear): array
|
||||
{
|
||||
@@ -51,7 +50,7 @@ class BadgePrintLogService
|
||||
try {
|
||||
$this->log($userIds, $actorId, $schoolYear, $rolesMap, $classesMap, $copies);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to log badge prints: ' . $e->getMessage());
|
||||
Log::error('Failed to log badge prints: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ class BadgeService
|
||||
protected TeacherClass $teacherClassModel,
|
||||
protected ClassSection $classSectionModel,
|
||||
protected BadgePrintLog $badgePrintLogModel
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function generateBadgePdf(
|
||||
array $userIds,
|
||||
@@ -55,7 +54,7 @@ class BadgeService
|
||||
foreach ($userIds as $uid) {
|
||||
$info = $this->getUserInfoById($uid, $schoolYear);
|
||||
|
||||
if (!$info || !is_array($info)) {
|
||||
if (! $info || ! is_array($info)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -70,13 +69,13 @@ class BadgeService
|
||||
$roleResolved = $this->formatRole($roleResolved);
|
||||
$info['role_resolved'] = $roleResolved;
|
||||
|
||||
if (!empty($classesMap[$userId])) {
|
||||
if (! empty($classesMap[$userId])) {
|
||||
$info['class_section_name'] = (string) $classesMap[$userId];
|
||||
}
|
||||
|
||||
$class = $this->norm($info['class_section_name'] ?? '');
|
||||
|
||||
$badgeKey = strtolower(trim($userId . '|' . $name . '|' . $roleResolved . '|' . $class));
|
||||
$badgeKey = strtolower(trim($userId.'|'.$name.'|'.$roleResolved.'|'.$class));
|
||||
if (isset($seen[$badgeKey])) {
|
||||
continue;
|
||||
}
|
||||
@@ -117,7 +116,7 @@ class BadgeService
|
||||
1
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to log badge prints: ' . $e->getMessage());
|
||||
Log::error('Failed to log badge prints: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return response($pdfString, 200, [
|
||||
@@ -159,7 +158,7 @@ class BadgeService
|
||||
$users = $this->fetchStaffList($schoolYear, $selectedUserIds);
|
||||
|
||||
foreach ($users as &$u) {
|
||||
if (!isset($u['user_id'])) {
|
||||
if (! isset($u['user_id'])) {
|
||||
if (isset($u['id'])) {
|
||||
$u['user_id'] = (int) $u['id'];
|
||||
}
|
||||
@@ -190,7 +189,7 @@ class BadgeService
|
||||
$rolesDetect = strtolower($u['roles_raw'] ?? ($u['role_name_raw'] ?? ''));
|
||||
$isTeacherish = str_contains($rolesDetect, 'teacher') || preg_match('/\bta\b/', $rolesDetect);
|
||||
|
||||
if ($isTeacherish && !empty($u['user_id'])) {
|
||||
if ($isTeacherish && ! empty($u['user_id'])) {
|
||||
$assign = $this->getLatestClassForUser((int) $u['user_id'], $schoolYear);
|
||||
if ($assign) {
|
||||
$u['class_section_id'] = $assign['class_section_id'] ?? null;
|
||||
@@ -234,11 +233,11 @@ class BadgeService
|
||||
])
|
||||
->groupBy('users.id', 'users.firstname', 'users.lastname');
|
||||
|
||||
if (!empty($selectedUserIds)) {
|
||||
if (! empty($selectedUserIds)) {
|
||||
$query->whereIn('users.id', $selectedUserIds);
|
||||
}
|
||||
|
||||
if (!empty($schoolYear) && Schema::hasColumn('user_roles', 'school_year')) {
|
||||
if (! empty($schoolYear) && Schema::hasColumn('user_roles', 'school_year')) {
|
||||
$query->where('user_roles.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
@@ -255,7 +254,7 @@ class BadgeService
|
||||
])
|
||||
->where('tc.teacher_id', $userId);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
if (! empty($schoolYear)) {
|
||||
$query->where('tc.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
@@ -265,7 +264,7 @@ class BadgeService
|
||||
->orderByDesc('tc.id')
|
||||
->first();
|
||||
|
||||
if (!$row || empty($row->class_section_id)) {
|
||||
if (! $row || empty($row->class_section_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -282,12 +281,12 @@ class BadgeService
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$u) {
|
||||
if (! $u) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = (int) $u->user_id;
|
||||
$fullname = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? ''));
|
||||
$fullname = trim(($u->firstname ?? '').' '.($u->lastname ?? ''));
|
||||
|
||||
$rolesRows = DB::table('user_roles as ur')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
@@ -310,7 +309,7 @@ class BadgeService
|
||||
->select(['class_section_id', 'updated_at', 'id', 'position', 'school_year'])
|
||||
->where('teacher_id', $userId);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
if (! empty($schoolYear)) {
|
||||
$tcQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
@@ -322,7 +321,7 @@ class BadgeService
|
||||
$position = strtolower(trim((string) ($assignment->position ?? '')));
|
||||
$className = null;
|
||||
|
||||
if (!empty($classId)) {
|
||||
if (! empty($classId)) {
|
||||
$className = $this->resolveClassName((int) $classId);
|
||||
}
|
||||
|
||||
@@ -330,7 +329,7 @@ class BadgeService
|
||||
$roleLabel = 'Teacher Assistant';
|
||||
} elseif ($position === 'teacher' || $position === 'main') {
|
||||
$roleLabel = 'Teacher';
|
||||
} elseif (!empty($allRoles)) {
|
||||
} elseif (! empty($allRoles)) {
|
||||
$roleLabel = $allRoles[0];
|
||||
} else {
|
||||
$roleLabel = 'Staff';
|
||||
@@ -347,11 +346,11 @@ class BadgeService
|
||||
]);
|
||||
|
||||
$jobTitle = '';
|
||||
if (!empty($roleLabel) && !empty($className)) {
|
||||
if (! empty($roleLabel) && ! empty($className)) {
|
||||
$jobTitle = "{$roleLabel} - {$className}";
|
||||
} elseif (!empty($roleLabel)) {
|
||||
} elseif (! empty($roleLabel)) {
|
||||
$jobTitle = $roleLabel;
|
||||
} elseif (!empty($className)) {
|
||||
} elseif (! empty($className)) {
|
||||
$jobTitle = $className;
|
||||
}
|
||||
|
||||
@@ -379,10 +378,10 @@ class BadgeService
|
||||
$pdf->Rect($x, $y, $w, $h);
|
||||
|
||||
$logoSize = 6;
|
||||
if (!empty($data['school_logo']) && file_exists($data['school_logo'])) {
|
||||
if (! empty($data['school_logo']) && file_exists($data['school_logo'])) {
|
||||
$pdf->Image($data['school_logo'], $x + $w - 6 - $logoSize, $y + 4, $logoSize + 3, $logoSize - 1);
|
||||
}
|
||||
if (!empty($data['isgl_logo']) && file_exists($data['isgl_logo'])) {
|
||||
if (! empty($data['isgl_logo']) && file_exists($data['isgl_logo'])) {
|
||||
$pdf->Image($data['isgl_logo'], $x + 4, $y + 4, $logoSize + 3, $logoSize - 1);
|
||||
}
|
||||
|
||||
@@ -418,11 +417,11 @@ class BadgeService
|
||||
|
||||
if ($isTeacherish && $class !== '') {
|
||||
if (strtolower($class) === 'youth') {
|
||||
$display = 'Youth ' . $roleResolved;
|
||||
$display = 'Youth '.$roleResolved;
|
||||
} elseif ($class === 'KG') {
|
||||
$display = 'KG ' . $roleResolved;
|
||||
$display = 'KG '.$roleResolved;
|
||||
} else {
|
||||
$display = 'Grade ' . $class . ' ' . $roleResolved;
|
||||
$display = 'Grade '.$class.' '.$roleResolved;
|
||||
}
|
||||
} elseif ($roleResolved !== '') {
|
||||
$display = $roleResolved;
|
||||
@@ -461,7 +460,7 @@ class BadgeService
|
||||
'role',
|
||||
'active_role',
|
||||
'role_name',
|
||||
fn ($r) => !empty($r['roles'])
|
||||
fn ($r) => ! empty($r['roles'])
|
||||
? (array_filter(array_map('trim', explode(',', (string) $r['roles'])))[0] ?? '')
|
||||
: '',
|
||||
'job_title',
|
||||
@@ -479,7 +478,7 @@ class BadgeService
|
||||
return $this->norm($v);
|
||||
}
|
||||
} else {
|
||||
if (!empty($info[$key])) {
|
||||
if (! empty($info[$key])) {
|
||||
$v = $this->norm((string) $info[$key]);
|
||||
if ($v !== '') {
|
||||
return $v;
|
||||
@@ -579,6 +578,7 @@ class BadgeService
|
||||
|
||||
if ($start === false) {
|
||||
$out[] = ucfirst(mb_strtolower($tok, 'UTF-8'));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -594,12 +594,12 @@ class BadgeService
|
||||
$coreFmt = preg_replace_callback(
|
||||
'/\p{L}+/u',
|
||||
static fn ($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8')
|
||||
. mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'),
|
||||
.mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'),
|
||||
mb_strtolower($core, 'UTF-8')
|
||||
);
|
||||
}
|
||||
|
||||
$out[] = $pre . $coreFmt . $suf;
|
||||
$out[] = $pre.$coreFmt.$suf;
|
||||
}
|
||||
|
||||
return implode(' ', $out);
|
||||
@@ -640,7 +640,7 @@ class BadgeService
|
||||
protected function firstExisting(array $paths): ?string
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
if (!empty($path) && file_exists($path)) {
|
||||
if (! empty($path) && file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -697,4 +697,4 @@ class BadgeService
|
||||
|
||||
return $schoolYears;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ class BadgeStudentLookupService
|
||||
{
|
||||
public function __construct(
|
||||
protected BadgeTextFormatter $formatter
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Active student roster for the badge picker.
|
||||
@@ -29,7 +28,7 @@ class BadgeStudentLookupService
|
||||
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
|
||||
$join->on('sc.student_id', '=', 'students.id');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
if (! empty($schoolYear)) {
|
||||
$join->where('sc.school_year', '=', $schoolYear);
|
||||
}
|
||||
})
|
||||
@@ -55,9 +54,9 @@ class BadgeStudentLookupService
|
||||
->orderBy('students.lastname')
|
||||
->orderBy('students.firstname');
|
||||
|
||||
if (!empty($selectedStudentIds)) {
|
||||
if (! empty($selectedStudentIds)) {
|
||||
$query->whereIn('students.id', $selectedStudentIds);
|
||||
} elseif (!empty($schoolYear)) {
|
||||
} elseif (! empty($schoolYear)) {
|
||||
$query->where(function ($q) use ($schoolYear) {
|
||||
$q->where('students.school_year', $schoolYear)
|
||||
->orWhereNotNull('sc.student_id');
|
||||
@@ -74,7 +73,7 @@ class BadgeStudentLookupService
|
||||
}
|
||||
|
||||
$schoolId = trim((string) ($row['school_id'] ?? ''));
|
||||
if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ class BadgeStudentLookupService
|
||||
$out = [];
|
||||
foreach ($rows as $student) {
|
||||
$schoolId = trim((string) ($student->school_id ?? ''));
|
||||
if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class BadgeTextFormatter
|
||||
}
|
||||
|
||||
$special = [
|
||||
'of' => 'of',
|
||||
'of' => 'of',
|
||||
'head' => 'Head',
|
||||
];
|
||||
|
||||
@@ -95,6 +95,7 @@ class BadgeTextFormatter
|
||||
|
||||
if ($start === false) {
|
||||
$out[] = ucfirst(mb_strtolower($tok, 'UTF-8'));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -110,12 +111,12 @@ class BadgeTextFormatter
|
||||
$coreFmt = preg_replace_callback(
|
||||
'/\p{L}+/u',
|
||||
static fn ($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8')
|
||||
. mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'),
|
||||
.mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'),
|
||||
mb_strtolower($core, 'UTF-8')
|
||||
);
|
||||
}
|
||||
|
||||
$out[] = $pre . $coreFmt . $suf;
|
||||
$out[] = $pre.$coreFmt.$suf;
|
||||
}
|
||||
|
||||
return implode(' ', $out);
|
||||
@@ -163,7 +164,7 @@ class BadgeTextFormatter
|
||||
'role',
|
||||
'active_role',
|
||||
'role_name',
|
||||
fn ($r) => !empty($r['roles'])
|
||||
fn ($r) => ! empty($r['roles'])
|
||||
? (array_values(array_filter(array_map('trim', explode(',', (string) $r['roles']))))[0] ?? '')
|
||||
: '',
|
||||
'job_title',
|
||||
@@ -181,7 +182,7 @@ class BadgeTextFormatter
|
||||
return $this->norm($v);
|
||||
}
|
||||
} else {
|
||||
if (!empty($info[$key])) {
|
||||
if (! empty($info[$key])) {
|
||||
$v = $this->norm((string) $info[$key]);
|
||||
if ($v !== '') {
|
||||
return $v;
|
||||
@@ -196,7 +197,7 @@ class BadgeTextFormatter
|
||||
public function firstExisting(array $paths): ?string
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
if (!empty($path) && file_exists($path)) {
|
||||
if (! empty($path) && file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ class BadgeUserLookupService
|
||||
{
|
||||
public function __construct(
|
||||
protected BadgeTextFormatter $formatter
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function fetchStaffList(?string $schoolYear, array $selectedUserIds = []): array
|
||||
{
|
||||
@@ -25,7 +24,7 @@ class BadgeUserLookupService
|
||||
])
|
||||
->groupBy('users.id', 'users.firstname', 'users.lastname');
|
||||
|
||||
if (!empty($selectedUserIds)) {
|
||||
if (! empty($selectedUserIds)) {
|
||||
$query->whereIn('users.id', $selectedUserIds);
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ class BadgeUserLookupService
|
||||
])
|
||||
->where('tc.teacher_id', $userId);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
if (! empty($schoolYear)) {
|
||||
$query->where('tc.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
@@ -75,12 +74,12 @@ class BadgeUserLookupService
|
||||
->orderByDesc('tc.id')
|
||||
->first();
|
||||
|
||||
if (!$row || empty($row->class_section_id)) {
|
||||
if (! $row || empty($row->class_section_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'class_section_id' => (int) $row->class_section_id,
|
||||
'class_section_id' => (int) $row->class_section_id,
|
||||
'class_section_name' => $row->class_section_name ?? null,
|
||||
];
|
||||
}
|
||||
@@ -92,12 +91,12 @@ class BadgeUserLookupService
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$u) {
|
||||
if (! $u) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = (int) $u->user_id;
|
||||
$fullname = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? ''));
|
||||
$fullname = trim(($u->firstname ?? '').' '.($u->lastname ?? ''));
|
||||
$schoolId = trim((string) ($u->school_id ?? ''));
|
||||
if ($schoolId === '') {
|
||||
$schoolId = trim((string) ($u->account_id ?? ''));
|
||||
@@ -124,7 +123,7 @@ class BadgeUserLookupService
|
||||
->select(['class_section_id', 'updated_at', 'id', 'position', 'school_year'])
|
||||
->where('teacher_id', $userId);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
if (! empty($schoolYear)) {
|
||||
$tcQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
@@ -136,7 +135,7 @@ class BadgeUserLookupService
|
||||
$position = strtolower(trim((string) ($assignment->position ?? '')));
|
||||
$className = null;
|
||||
|
||||
if (!empty($classId)) {
|
||||
if (! empty($classId)) {
|
||||
$className = $this->resolveClassName((int) $classId);
|
||||
}
|
||||
|
||||
@@ -144,7 +143,7 @@ class BadgeUserLookupService
|
||||
$roleLabel = 'Teacher Assistant';
|
||||
} elseif ($position === 'teacher' || $position === 'main') {
|
||||
$roleLabel = 'Teacher';
|
||||
} elseif (!empty($allRoles)) {
|
||||
} elseif (! empty($allRoles)) {
|
||||
$roleLabel = $allRoles[0];
|
||||
} else {
|
||||
$roleLabel = 'Staff';
|
||||
@@ -161,27 +160,27 @@ class BadgeUserLookupService
|
||||
]);
|
||||
|
||||
$jobTitle = '';
|
||||
if (!empty($roleLabel) && !empty($className)) {
|
||||
if (! empty($roleLabel) && ! empty($className)) {
|
||||
$jobTitle = "{$roleLabel} - {$className}";
|
||||
} elseif (!empty($roleLabel)) {
|
||||
} elseif (! empty($roleLabel)) {
|
||||
$jobTitle = $roleLabel;
|
||||
} elseif (!empty($className)) {
|
||||
} elseif (! empty($className)) {
|
||||
$jobTitle = $className;
|
||||
}
|
||||
|
||||
return [
|
||||
'user_id' => $userId,
|
||||
'name' => $fullname !== '' ? $fullname : 'STAFF',
|
||||
'role' => $roleLabel,
|
||||
'roles' => $rolesCsv,
|
||||
'class_section_id' => $classId ? (int) $classId : null,
|
||||
'class_section_name' => $className,
|
||||
'job_title' => $jobTitle,
|
||||
'school_name' => 'Al Rahma Sunday School',
|
||||
'school_id' => $schoolId,
|
||||
'school_logo' => $schoolLogo,
|
||||
'isgl_logo' => $isglLogo,
|
||||
'school_year' => $assignment->school_year ?? $schoolYear,
|
||||
'user_id' => $userId,
|
||||
'name' => $fullname !== '' ? $fullname : 'STAFF',
|
||||
'role' => $roleLabel,
|
||||
'roles' => $rolesCsv,
|
||||
'class_section_id' => $classId ? (int) $classId : null,
|
||||
'class_section_name' => $className,
|
||||
'job_title' => $jobTitle,
|
||||
'school_name' => 'Al Rahma Sunday School',
|
||||
'school_id' => $schoolId,
|
||||
'school_logo' => $schoolLogo,
|
||||
'isgl_logo' => $isglLogo,
|
||||
'school_year' => $assignment->school_year ?? $schoolYear,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,10 @@ class BalanceCalculationService
|
||||
private FeeConfigService $config,
|
||||
private TuitionCalculationService $tuition,
|
||||
private BillingTotalsService $billingTotals
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $students
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $students
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function calculateFamilyAccount(array $students, int $parentId, ?string $schoolYear = null): array
|
||||
@@ -98,9 +96,8 @@ class BalanceCalculationService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $tuitionStudents
|
||||
* @param array<int, float> $eventByStudent
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $tuitionStudents
|
||||
* @param array<int, float> $eventByStudent
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function buildStudentBalances(
|
||||
|
||||
@@ -96,7 +96,7 @@ class BillingTotalsService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{charge_type?: string, amount?: float|int|string}> $rows
|
||||
* @param array<int, array{charge_type?: string, amount?: float|int|string}> $rows
|
||||
*/
|
||||
private function sumAdditionalChargeRows(array $rows): float
|
||||
{
|
||||
|
||||
@@ -26,8 +26,7 @@ class ChargeService
|
||||
private ExtraChargesInvoiceService $invoices,
|
||||
private ExtraChargesContextService $context,
|
||||
private InvoiceGenerationService $invoiceGeneration
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function createExtraCharge(array $payload): array
|
||||
{
|
||||
@@ -60,7 +59,7 @@ class ChargeService
|
||||
'semester' => $payload['semester'] ?? $this->context->getSemester(),
|
||||
]));
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
if (! ($result['ok'] ?? false)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -70,14 +69,14 @@ class ChargeService
|
||||
'id' => (int) ($result['id'] ?? 0),
|
||||
'parent_id' => $parentId,
|
||||
'invoice_id' => $result['invoice_id'] ?? null,
|
||||
'status' => !empty($payload['invoice_id']) ? 'applied' : 'pending',
|
||||
'status' => ! empty($payload['invoice_id']) ? 'applied' : 'pending',
|
||||
];
|
||||
}
|
||||
|
||||
public function cancelExtraCharge(int $chargeId): array
|
||||
{
|
||||
$charge = AdditionalCharge::query()->find($chargeId);
|
||||
if (!$charge) {
|
||||
if (! $charge) {
|
||||
return ['ok' => false, 'message' => 'Extra charge not found.'];
|
||||
}
|
||||
|
||||
@@ -99,7 +98,7 @@ class ChargeService
|
||||
public function applyExtraCharge(int $chargeId, int $invoiceId): array
|
||||
{
|
||||
$charge = AdditionalCharge::query()->find($chargeId);
|
||||
if (!$charge) {
|
||||
if (! $charge) {
|
||||
return ['ok' => false, 'message' => 'Extra charge not found.'];
|
||||
}
|
||||
|
||||
@@ -165,7 +164,7 @@ class ChargeService
|
||||
$amount = isset($payload['amount']) ? (float) $payload['amount'] : null;
|
||||
if ($eventId > 0) {
|
||||
$event = Event::getEvent($eventId, $schoolYear);
|
||||
if (!$event) {
|
||||
if (! $event) {
|
||||
return ['ok' => false, 'message' => 'Event not found.'];
|
||||
}
|
||||
$eventName = (string) ($event->event_name ?? $eventName);
|
||||
@@ -225,7 +224,7 @@ class ChargeService
|
||||
public function cancelEventCharge(int $chargeId, bool $issueCredit = true): array
|
||||
{
|
||||
$charge = EventCharges::query()->find($chargeId);
|
||||
if (!$charge) {
|
||||
if (! $charge) {
|
||||
return ['ok' => false, 'message' => 'Event charge not found.'];
|
||||
}
|
||||
|
||||
@@ -252,8 +251,8 @@ class ChargeService
|
||||
if ($issueCredit && $amount > 0 && $parentId > 0) {
|
||||
$credit = $this->createExtraCharge([
|
||||
'parent_id' => $parentId,
|
||||
'title' => 'Event credit: ' . $eventName,
|
||||
'description' => 'Account credit for canceled event charge #' . $chargeId,
|
||||
'title' => 'Event credit: '.$eventName,
|
||||
'description' => 'Account credit for canceled event charge #'.$chargeId,
|
||||
'amount' => $amount,
|
||||
'charge_type' => 'deduct',
|
||||
'school_year' => $schoolYear,
|
||||
@@ -287,7 +286,7 @@ class ChargeService
|
||||
public function markEventChargePaid(int $chargeId, bool $paid, ?int $paymentId = null): array
|
||||
{
|
||||
$charge = EventCharges::query()->find($chargeId);
|
||||
if (!$charge) {
|
||||
if (! $charge) {
|
||||
return ['ok' => false, 'message' => 'Event charge not found.'];
|
||||
}
|
||||
|
||||
@@ -389,7 +388,7 @@ class ChargeService
|
||||
|
||||
private function resolveEventName(EventCharges $charge): string
|
||||
{
|
||||
if (!empty($charge->event_name)) {
|
||||
if (! empty($charge->event_name)) {
|
||||
return (string) $charge->event_name;
|
||||
}
|
||||
|
||||
@@ -426,7 +425,7 @@ class ChargeService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function formatEventCharge(array $row): array
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ class BroadcastEmailComposerService
|
||||
$html = preg_replace('#<(script|iframe)[^>]*>.*?</\\1>#is', '', $html);
|
||||
$html = preg_replace('/\\son\\w+="[^"]*"/i', '', $html);
|
||||
$html = preg_replace("/\\son\\w+='[^']*'/i", '', $html);
|
||||
|
||||
return $html ?? '';
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ class BroadcastEmailComposerService
|
||||
): string {
|
||||
$content = $personalize ? str_replace('{{name}}', $recipientName, $body) : $body;
|
||||
|
||||
if (!$wrapLayout) {
|
||||
if (! $wrapLayout) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Services\EmailService;
|
||||
class BroadcastEmailDispatchService
|
||||
{
|
||||
private EmailService $mailer;
|
||||
|
||||
private BroadcastEmailComposerService $composer;
|
||||
|
||||
public function __construct(EmailService $mailer, BroadcastEmailComposerService $composer)
|
||||
@@ -30,7 +31,7 @@ class BroadcastEmailDispatchService
|
||||
|
||||
$ok = $this->mailer->send(
|
||||
$payload['test_email'],
|
||||
'[TEST] ' . $payload['subject'],
|
||||
'[TEST] '.$payload['subject'],
|
||||
$html,
|
||||
$payload['from_key']
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ class BroadcastEmailImageService
|
||||
}
|
||||
|
||||
$mime = strtolower((string) $file->getMimeType());
|
||||
if (!isset($this->allowedTypes[$mime])) {
|
||||
if (! isset($this->allowedTypes[$mime])) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 415,
|
||||
@@ -43,17 +43,17 @@ class BroadcastEmailImageService
|
||||
}
|
||||
|
||||
$targetDir = public_path('uploads/email');
|
||||
if (!is_dir($targetDir)) {
|
||||
if (! is_dir($targetDir)) {
|
||||
@mkdir($targetDir, 0755, true);
|
||||
}
|
||||
|
||||
$ext = $this->allowedTypes[$mime];
|
||||
$newName = 'em_' . bin2hex(random_bytes(16)) . '.' . $ext;
|
||||
$newName = 'em_'.bin2hex(random_bytes(16)).'.'.$ext;
|
||||
$file->move($targetDir, $newName);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'url' => asset('uploads/email/' . $newName),
|
||||
'url' => asset('uploads/email/'.$newName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class BroadcastEmailRecipientService
|
||||
|
||||
return array_values(array_filter($parents, static function ($parent) {
|
||||
$email = $parent['email'] ?? '';
|
||||
|
||||
return is_string($email) && trim($email) !== '';
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ class BroadcastEmailSenderOptionsService
|
||||
$json = getenv('MAIL_SENDERS') ?: env('MAIL_SENDERS', '{}');
|
||||
$arr = json_decode($json, true);
|
||||
|
||||
if (!is_array($arr) || empty($arr)) {
|
||||
if (! is_array($arr) || empty($arr)) {
|
||||
$smtpUser = getenv('SMTP_USER') ?: '';
|
||||
$name = 'Al Rahma Sunday School';
|
||||
|
||||
return [[
|
||||
'key' => 'general',
|
||||
'label' => $name . ($smtpUser ? " <{$smtpUser}>" : ''),
|
||||
'label' => $name.($smtpUser ? " <{$smtpUser}>" : ''),
|
||||
]];
|
||||
}
|
||||
|
||||
@@ -24,7 +25,7 @@ class BroadcastEmailSenderOptionsService
|
||||
$email = $info['email'] ?? '';
|
||||
$out[] = [
|
||||
'key' => (string) $key,
|
||||
'label' => trim($name . ($email ? " <{$email}>" : '')),
|
||||
'label' => trim($name.($email ? " <{$email}>" : '')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Certificates;
|
||||
|
||||
use App\Models\CertificateRecord;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\StudentDecision;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -641,6 +640,7 @@ class CertificateAdminService
|
||||
|
||||
if (preg_match('/grade\s*(\d+)/i', $name, $matches) === 1) {
|
||||
$grade = (int) $matches[1];
|
||||
|
||||
return [
|
||||
'Grade '.$grade,
|
||||
'1_'.str_pad((string) $grade, 3, '0', STR_PAD_LEFT),
|
||||
|
||||
@@ -7,12 +7,13 @@ namespace App\Services\Certificates;
|
||||
class CertificatePdfService
|
||||
{
|
||||
private string $fontDir;
|
||||
|
||||
private string $imgDir;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->fontDir = public_path('assets/certificates/fonts') . DIRECTORY_SEPARATOR;
|
||||
$this->imgDir = public_path('assets/certificates/images') . DIRECTORY_SEPARATOR;
|
||||
$this->fontDir = public_path('assets/certificates/fonts').DIRECTORY_SEPARATOR;
|
||||
$this->imgDir = public_path('assets/certificates/images').DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,9 +21,9 @@ class CertificatePdfService
|
||||
*/
|
||||
public function generate(array $students, string $certDate): string
|
||||
{
|
||||
$edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir.'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir.'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir.'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
|
||||
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
|
||||
$pdf->SetCreator('Al Rahma Sunday School');
|
||||
@@ -39,7 +40,7 @@ class CertificatePdfService
|
||||
|
||||
foreach ($students as $student) {
|
||||
$pdf->AddPage();
|
||||
$name = $student['firstname'] . ' ' . $student['lastname'];
|
||||
$name = $student['firstname'].' '.$student['lastname'];
|
||||
$grade = $this->formatGrade((string) ($student['grade'] ?? ''));
|
||||
$certNumber = (string) ($student['cert_number'] ?? '');
|
||||
$verifyUrl = is_string($student['verify_url'] ?? null) ? $student['verify_url'] : null;
|
||||
@@ -64,8 +65,8 @@ class CertificatePdfService
|
||||
|
||||
private function drawCertificate(
|
||||
\TCPDF $pdf,
|
||||
float $W,
|
||||
float $H,
|
||||
float $W,
|
||||
float $H,
|
||||
string $name,
|
||||
string $grade,
|
||||
string $certDate,
|
||||
@@ -75,9 +76,9 @@ class CertificatePdfService
|
||||
string $garamondBold,
|
||||
string $ebGaramond
|
||||
): void {
|
||||
$pdf->Image($this->imgDir . 'title.png', 126, 0, 600);
|
||||
$pdf->Image($this->imgDir . 'background.png', 280, 176, 291, 340);
|
||||
$pdf->Image($this->imgDir . 'signature.png', 140, 410, 90, 80);
|
||||
$pdf->Image($this->imgDir.'title.png', 126, 0, 600);
|
||||
$pdf->Image($this->imgDir.'background.png', 280, 176, 291, 340);
|
||||
$pdf->Image($this->imgDir.'signature.png', 140, 410, 90, 80);
|
||||
|
||||
$pdf->SetFont('times', 'B', 24);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
@@ -135,7 +136,7 @@ class CertificatePdfService
|
||||
$pdf->SetFont('helvetica', '', 8);
|
||||
$pdf->SetTextColor(150, 150, 150);
|
||||
$pdf->SetXY(70.86, $H - 39.68);
|
||||
$pdf->Cell(200, 12, 'Certificate No. ' . $certNumber, 0, 0, 'L');
|
||||
$pdf->Cell(200, 12, 'Certificate No. '.$certNumber, 0, 0, 'L');
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ class CertificatePdfService
|
||||
$lower = strtolower($clean);
|
||||
|
||||
if (preg_match('/^\d+$/', $clean) && (int) $clean >= 1 && (int) $clean <= 9) {
|
||||
return 'Grade ' . $clean;
|
||||
return 'Grade '.$clean;
|
||||
}
|
||||
if ($lower === 'youth') {
|
||||
return 'Youth';
|
||||
|
||||
@@ -16,7 +16,7 @@ class ClassPreparationAdjustmentWriterService
|
||||
|
||||
foreach ($adjustments as $itemName => $adjustment) {
|
||||
$item = (string) $itemName;
|
||||
if ($item === '' || !isset($allowed[$item])) {
|
||||
if ($item === '' || ! isset($allowed[$item])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class ClassPreparationAdjustmentWriterService
|
||||
'adjustable' => 1,
|
||||
]);
|
||||
$count++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class ClassPreparationCalculatorService
|
||||
|
||||
if ($numValue > 0 && $numValue < 30) {
|
||||
$firstDigit = (int) substr((string) $numValue, 0, 1);
|
||||
|
||||
return $firstDigit === 1 ? 1 : ($firstDigit === 2 ? 2 : 3);
|
||||
}
|
||||
|
||||
@@ -69,10 +70,12 @@ class ClassPreparationCalculatorService
|
||||
|
||||
if ($gradeMin !== null && $classLevel < (int) $gradeMin) {
|
||||
$items[$name] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($gradeMax !== null && $classLevel > (int) $gradeMax) {
|
||||
$items[$name] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class ClassPreparationContextService
|
||||
->count();
|
||||
|
||||
$this->rosterPresenceCache[$key] = $cnt > 0;
|
||||
|
||||
return $this->rosterPresenceCache[$key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class ClassPreparationInventoryService
|
||||
}
|
||||
|
||||
$needsFallback = array_filter($inventoryMap, static fn ($v) => (int) $v === 0);
|
||||
if (!empty($needsFallback)) {
|
||||
if (! empty($needsFallback)) {
|
||||
$fallbackRows = DB::table('inventory_items')
|
||||
->select('name AS item_name, COALESCE(SUM(CASE WHEN good_qty IS NOT NULL AND good_qty > 0 THEN good_qty WHEN `condition`=\"good\" THEN quantity ELSE quantity END),0) AS available')
|
||||
->where('type', 'classroom')
|
||||
@@ -79,6 +79,7 @@ class ClassPreparationInventoryService
|
||||
|
||||
if ($goodQty > 0) {
|
||||
$inventoryMap[$name] = $goodQty;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class ClassPreparationLogService
|
||||
'prep_data' => json_encode($items),
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
|
||||
@@ -13,8 +13,7 @@ class ClassPreparationPrintService
|
||||
private ClassPreparationCalculatorService $calculator,
|
||||
private ClassPreparationAdjustmentService $adjustments,
|
||||
private ClassPreparationLogService $logs
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function printPrep(string $classSectionId, string $schoolYear, ?string $semester = null): array
|
||||
{
|
||||
@@ -31,7 +30,7 @@ class ClassPreparationPrintService
|
||||
$printedAt = $this->utcNow();
|
||||
$ok = $this->logs->createLog($classSectionId, $className, $schoolYear, $items, $printedAt);
|
||||
|
||||
if (!$ok) {
|
||||
if (! $ok) {
|
||||
Log::warning('Failed to store class preparation log.', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => $schoolYear,
|
||||
@@ -55,6 +54,7 @@ class ClassPreparationPrintService
|
||||
if (function_exists('utc_now')) {
|
||||
return (string) utc_now();
|
||||
}
|
||||
|
||||
return now('UTC')->toDateTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class ClassPreparationRosterService
|
||||
}
|
||||
|
||||
$row = $query->first();
|
||||
|
||||
return (int) ($row->cnt ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,19 @@ use App\Models\ClassSection;
|
||||
class ClassPreparationService
|
||||
{
|
||||
private ClassPreparationContextService $context;
|
||||
|
||||
private ClassPreparationRosterService $roster;
|
||||
|
||||
private ClassPreparationInventoryService $inventory;
|
||||
|
||||
private ClassPreparationCalculatorService $calculator;
|
||||
|
||||
private ClassPreparationAdjustmentService $adjustments;
|
||||
|
||||
private ClassPreparationLogService $logs;
|
||||
|
||||
private ClassPreparationAdjustmentWriterService $adjustmentWriter;
|
||||
|
||||
private ClassPreparationPrintService $printService;
|
||||
|
||||
public function __construct(
|
||||
@@ -66,14 +73,14 @@ class ClassPreparationService
|
||||
$hasChanged = $this->logs->hasPrepChanged($baseItems, $oldPrep);
|
||||
|
||||
$results[] = [
|
||||
'class_section' => $className,
|
||||
'class_section' => $className,
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_count' => $studentCount,
|
||||
'class_level' => $classLevel,
|
||||
'prep_items' => $baseItems,
|
||||
'needs_print' => $hasChanged,
|
||||
'last_printed_at' => $oldSnap?->created_at,
|
||||
'adjustments' => $adjMap,
|
||||
'student_count' => $studentCount,
|
||||
'class_level' => $classLevel,
|
||||
'prep_items' => $baseItems,
|
||||
'needs_print' => $hasChanged,
|
||||
'last_printed_at' => $oldSnap?->created_at,
|
||||
'adjustments' => $adjMap,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -139,6 +146,7 @@ class ClassPreparationService
|
||||
if (function_exists('utc_now')) {
|
||||
return (string) utc_now();
|
||||
}
|
||||
|
||||
return now('UTC')->toDateTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Services\ClassProgress;
|
||||
use App\Models\ClassProgressAttachment;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ClassProgressAttachmentService
|
||||
{
|
||||
@@ -17,7 +16,7 @@ class ClassProgressAttachmentService
|
||||
|
||||
$stored = [];
|
||||
foreach ($files as $file) {
|
||||
if (!$file instanceof UploadedFile || !$file->isValid()) {
|
||||
if (! $file instanceof UploadedFile || ! $file->isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -49,17 +48,17 @@ class ClassProgressAttachmentService
|
||||
$candidates = [];
|
||||
if (str_starts_with($path, 'storage/')) {
|
||||
$relative = ltrim(substr($path, strlen('storage/')), '/');
|
||||
$candidates[] = storage_path('app/public/' . $relative);
|
||||
$candidates[] = storage_path('app/public/'.$relative);
|
||||
}
|
||||
|
||||
if (str_starts_with($path, 'writable/uploads/')) {
|
||||
$relative = ltrim(substr($path, strlen('writable/uploads/')), '/');
|
||||
$candidates[] = storage_path('app/' . $relative);
|
||||
$candidates[] = storage_path('app/public/' . $relative);
|
||||
$candidates[] = storage_path('app/'.$relative);
|
||||
$candidates[] = storage_path('app/public/'.$relative);
|
||||
}
|
||||
|
||||
$candidates[] = storage_path('app/' . ltrim($path, '/'));
|
||||
$candidates[] = storage_path('app/public/' . ltrim($path, '/'));
|
||||
$candidates[] = storage_path('app/'.ltrim($path, '/'));
|
||||
$candidates[] = storage_path('app/public/'.ltrim($path, '/'));
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
@@ -84,6 +83,6 @@ class ClassProgressAttachmentService
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 'storage/' . ltrim($path, '/');
|
||||
return 'storage/'.ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class ClassProgressMetaService
|
||||
|
||||
public function curriculumOptions(?int $classId, ?string $schoolYear = null): array
|
||||
{
|
||||
if (!$classId) {
|
||||
if (! $classId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ use App\Models\ClassProgressReport;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use App\Services\SchoolYears\SchoolYearContextService;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ClassProgressMutationService
|
||||
{
|
||||
@@ -34,11 +35,11 @@ class ClassProgressMutationService
|
||||
$weekStart = (string) ($payload['week_start'] ?? '');
|
||||
$weekEnd = $this->rules->ensureWeekEnd($weekStart, $payload['week_end'] ?? null);
|
||||
|
||||
if (!$this->rules->isWeekEndValid($weekStart, $weekEnd)) {
|
||||
if (! $this->rules->isWeekEndValid($weekStart, $weekEnd)) {
|
||||
throw new \InvalidArgumentException('Week end must be the same as or after the week start.');
|
||||
}
|
||||
|
||||
if (!$this->rules->hasIslamicUnitSelection($payload)) {
|
||||
if (! $this->rules->hasIslamicUnitSelection($payload)) {
|
||||
throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ class ClassProgressMutationService
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($existingIds !== [] && !$confirmOverwrite) {
|
||||
if ($existingIds !== [] && ! $confirmOverwrite) {
|
||||
throw new \InvalidArgumentException('CONFIRM_OVERWRITE');
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ class ClassProgressMutationService
|
||||
throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
$originalWeekStart = $anchor->week_start instanceof \Carbon\CarbonInterface
|
||||
$originalWeekStart = $anchor->week_start instanceof CarbonInterface
|
||||
? $anchor->week_start->format('Y-m-d')
|
||||
: (string) $anchor->week_start;
|
||||
|
||||
@@ -295,7 +296,7 @@ class ClassProgressMutationService
|
||||
return DB::transaction(function () use ($report, $payload, $attachments) {
|
||||
$weekStart = $payload['week_start'] ?? $report->week_start?->format('Y-m-d');
|
||||
$weekEnd = $this->rules->ensureWeekEnd((string) $weekStart, $payload['week_end'] ?? null);
|
||||
if ($weekStart && $weekEnd && !$this->rules->isWeekEndValid((string) $weekStart, (string) $weekEnd)) {
|
||||
if ($weekStart && $weekEnd && ! $this->rules->isWeekEndValid((string) $weekStart, (string) $weekEnd)) {
|
||||
throw new \InvalidArgumentException('Week end must be the same as or after the week start.');
|
||||
}
|
||||
|
||||
@@ -364,7 +365,7 @@ class ClassProgressMutationService
|
||||
->where('class_section_id', $classSectionId)
|
||||
->exists();
|
||||
|
||||
if (!$assigned) {
|
||||
if (! $assigned) {
|
||||
throw new \InvalidArgumentException('No class assignment found for this report.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use App\Models\Configuration;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use App\Services\SchoolYears\SchoolYearContextService;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ClassProgressQueryService
|
||||
{
|
||||
@@ -28,7 +28,7 @@ class ClassProgressQueryService
|
||||
->with(['classSection', 'teacher'])
|
||||
->orderBy($filters['sort_by'] ?? 'week_start', $filters['sort_dir'] ?? 'desc');
|
||||
|
||||
if (!$this->isAdmin($user)) {
|
||||
if (! $this->isAdmin($user)) {
|
||||
$relaxedSectionIds = TeacherClass::distinctSectionIdsForTeacher((int) $user->id);
|
||||
|
||||
if (! empty($filters['class_section_id'])) {
|
||||
@@ -50,31 +50,31 @@ class ClassProgressQueryService
|
||||
$query->where('teacher_id', (int) $filters['teacher_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['class_section_id'])) {
|
||||
if (! empty($filters['class_section_id'])) {
|
||||
$query->where('class_section_id', (int) $filters['class_section_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['subject'])) {
|
||||
if (! empty($filters['subject'])) {
|
||||
$query->where('subject', (string) $filters['subject']);
|
||||
}
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
if (! empty($filters['status'])) {
|
||||
$query->where('status', (string) $filters['status']);
|
||||
}
|
||||
|
||||
if (!empty($filters['school_year']) && Schema::hasColumn('class_progress_reports', 'school_year')) {
|
||||
if (! empty($filters['school_year']) && Schema::hasColumn('class_progress_reports', 'school_year')) {
|
||||
$query->where('school_year', (string) $filters['school_year']);
|
||||
}
|
||||
|
||||
if (!empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) {
|
||||
if (! empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) {
|
||||
$query->where('semester', (string) $filters['semester']);
|
||||
}
|
||||
|
||||
if (!empty($filters['week_start'])) {
|
||||
if (! empty($filters['week_start'])) {
|
||||
$query->whereDate('week_start', '>=', $filters['week_start']);
|
||||
}
|
||||
|
||||
if (!empty($filters['week_end'])) {
|
||||
if (! empty($filters['week_end'])) {
|
||||
$query->whereDate('week_end', '<=', $filters['week_end']);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ class ClassProgressQueryService
|
||||
return $rows->map(function (ClassProgressReport $row) use ($attachments) {
|
||||
$row->setAttribute('status_label', $this->statusLabel($row->status));
|
||||
$row->setAttribute('attachments', $attachments[$row->id] ?? []);
|
||||
|
||||
return $row;
|
||||
})->all();
|
||||
}
|
||||
@@ -159,6 +160,7 @@ class ClassProgressQueryService
|
||||
private function statusLabel(?string $status): string
|
||||
{
|
||||
$options = (array) config('progress.status_options', []);
|
||||
|
||||
return $options[$status] ?? 'Unknown';
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class ClassProgressRuleService
|
||||
public function normalizeFlags($flags): ?array
|
||||
{
|
||||
$values = array_values(array_filter((array) $flags, static fn ($item) => $item !== '' && $item !== null));
|
||||
|
||||
return $values === [] ? null : $values;
|
||||
}
|
||||
|
||||
@@ -80,6 +81,7 @@ class ClassProgressRuleService
|
||||
try {
|
||||
$dt = new \DateTime($weekStart);
|
||||
$dt->modify('+6 days');
|
||||
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return $weekStart;
|
||||
|
||||
@@ -15,7 +15,7 @@ class ClassAttendanceService
|
||||
$semester = $semester ?: (string) (Configuration::getConfig('semester') ?? '');
|
||||
|
||||
$teacher = User::query()->find($teacherId);
|
||||
$teacherName = trim((string) ($teacher?->firstname ?? '') . ' ' . (string) ($teacher?->lastname ?? ''));
|
||||
$teacherName = trim((string) ($teacher?->firstname ?? '').' '.(string) ($teacher?->lastname ?? ''));
|
||||
|
||||
$className = ClassSection::getClassSectionNameBySectionId($classSectionId) ?? (string) $classSectionId;
|
||||
$students = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, $teacherId);
|
||||
|
||||
@@ -13,27 +13,27 @@ class ClassSectionQueryService
|
||||
->leftJoin('classes', 'classSection.class_id', '=', 'classes.id')
|
||||
->select('classSection.*', 'classes.class_name');
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$term = '%' . strtolower((string) $filters['search']) . '%';
|
||||
if (! empty($filters['search'])) {
|
||||
$term = '%'.strtolower((string) $filters['search']).'%';
|
||||
$query->where(function ($q) use ($term) {
|
||||
$q->whereRaw('LOWER(classSection.class_section_name) LIKE ?', [$term])
|
||||
->orWhereRaw('LOWER(classes.class_name) LIKE ?', [$term]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($filters['class_id'])) {
|
||||
if (! empty($filters['class_id'])) {
|
||||
$query->where('classSection.class_id', (int) $filters['class_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['school_year'])) {
|
||||
if (! empty($filters['school_year'])) {
|
||||
$query->where('classSection.school_year', (string) $filters['school_year']);
|
||||
}
|
||||
|
||||
if (!empty($filters['semester'])) {
|
||||
if (! empty($filters['semester'])) {
|
||||
$query->where('classSection.semester', (string) $filters['semester']);
|
||||
}
|
||||
|
||||
if (!empty($filters['with_students'])) {
|
||||
if (! empty($filters['with_students'])) {
|
||||
$query->whereExists(function ($sub) use ($filters) {
|
||||
$sub->selectRaw('1')
|
||||
->from('student_class as sc')
|
||||
@@ -41,7 +41,7 @@ class ClassSectionQueryService
|
||||
->whereColumn('sc.class_section_id', 'classSection.class_section_id')
|
||||
->where('s.is_active', 1);
|
||||
|
||||
if (!empty($filters['school_year'])) {
|
||||
if (! empty($filters['school_year'])) {
|
||||
$sub->where('sc.school_year', (string) $filters['school_year']);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ class CommunicationFamilyService
|
||||
}
|
||||
|
||||
$names = array_map(static function ($row) {
|
||||
return trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));
|
||||
return trim(($row['firstname'] ?? '').' '.($row['lastname'] ?? ''));
|
||||
}, $rows);
|
||||
|
||||
return implode(' & ', $names);
|
||||
|
||||
@@ -9,7 +9,7 @@ class CommunicationLogService
|
||||
{
|
||||
public function log(array $payload): void
|
||||
{
|
||||
if (!Schema::hasTable('communication_logs')) {
|
||||
if (! Schema::hasTable('communication_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Services\Communication;
|
||||
class CommunicationPreviewService
|
||||
{
|
||||
private CommunicationTemplateService $templates;
|
||||
|
||||
private CommunicationStudentService $students;
|
||||
|
||||
private CommunicationFamilyService $families;
|
||||
|
||||
public function __construct(
|
||||
@@ -26,7 +28,7 @@ class CommunicationPreviewService
|
||||
string $teacherName
|
||||
): array {
|
||||
$template = $this->templates->findTemplateByKey($templateKey);
|
||||
if (!$template) {
|
||||
if (! $template) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 404,
|
||||
@@ -35,7 +37,7 @@ class CommunicationPreviewService
|
||||
}
|
||||
|
||||
$student = $this->students->getStudentBasic($studentId);
|
||||
if (!$student) {
|
||||
if (! $student) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 404,
|
||||
@@ -46,7 +48,7 @@ class CommunicationPreviewService
|
||||
$salutation = $this->families->guardianSalutation($familyId);
|
||||
|
||||
$autoVars = [
|
||||
'student_fullname' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
|
||||
'student_fullname' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')),
|
||||
'student_grade' => $student['grade'] ?? '',
|
||||
'parent_salutation' => $salutation,
|
||||
'date' => $this->formatLocalDate('Y-m-d'),
|
||||
@@ -70,6 +72,7 @@ class CommunicationPreviewService
|
||||
{
|
||||
return (string) preg_replace_callback('/\\{\\{\\s*([a-zA-Z0-9_\\.]+)\\s*\\}\\}/', function ($m) use ($vars) {
|
||||
$key = $m[1];
|
||||
|
||||
return htmlspecialchars((string) ($vars[$key] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
}, $template);
|
||||
}
|
||||
@@ -79,6 +82,7 @@ class CommunicationPreviewService
|
||||
if (function_exists('local_date') && function_exists('utc_now')) {
|
||||
return (string) local_date(utc_now(), $format);
|
||||
}
|
||||
|
||||
return now()->format($format);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Services\EmailService;
|
||||
class CommunicationSendService
|
||||
{
|
||||
private EmailService $mailer;
|
||||
|
||||
private CommunicationLogService $logService;
|
||||
|
||||
public function __construct(EmailService $mailer, CommunicationLogService $logService)
|
||||
@@ -61,7 +62,7 @@ class CommunicationSendService
|
||||
$sendOk = false;
|
||||
}
|
||||
|
||||
if (!$sendOk && $error === null) {
|
||||
if (! $sendOk && $error === null) {
|
||||
$error = 'Mailer returned false';
|
||||
}
|
||||
|
||||
@@ -90,6 +91,7 @@ class CommunicationSendService
|
||||
private function normalizeEmails(array $values): array
|
||||
{
|
||||
$out = array_values(array_unique(array_filter(array_map('strval', $values))));
|
||||
|
||||
return array_values(array_filter($out, static function ($email) {
|
||||
return $email !== '';
|
||||
}));
|
||||
|
||||
@@ -33,13 +33,13 @@ class CommunicationStudentService
|
||||
->orderBy('sc.id', 'desc')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = (array) $row;
|
||||
$grade = $row['registration_grade'] ?? '';
|
||||
if (!$grade && !empty($row['class_section_name'])) {
|
||||
if (! $grade && ! empty($row['class_section_name'])) {
|
||||
$grade = $row['class_section_name'];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class CommunicationTemplateService
|
||||
->get()
|
||||
->map(function ($row) use ($keyColumn, $bodyColumn) {
|
||||
$row = (array) $row;
|
||||
|
||||
return [
|
||||
'template_key' => (string) ($row[$keyColumn] ?? ''),
|
||||
'subject' => (string) ($row['subject'] ?? ''),
|
||||
@@ -35,7 +36,7 @@ class CommunicationTemplateService
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ class CommunicationTemplateService
|
||||
{
|
||||
$keyColumn = Schema::hasColumn('email_templates', 'template_key') ? 'template_key' : 'code';
|
||||
$bodyColumn = Schema::hasColumn('email_templates', 'body') ? 'body' : 'body_html';
|
||||
|
||||
return [$keyColumn, $bodyColumn];
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user