add notifications logic and add support of both JWT and Sanctum
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceAutoPublishJobService
|
||||
{
|
||||
public function __construct(private AttendanceAutoPublishService $autoPublish)
|
||||
{
|
||||
}
|
||||
|
||||
public function run(?DateTimeImmutable $now = null): array
|
||||
{
|
||||
$zone = $this->autoPublish->timezone();
|
||||
$now = $now ? $now->setTimezone($zone) : new DateTimeImmutable('now', $zone);
|
||||
$nowStr = $now->format('Y-m-d H:i:s');
|
||||
|
||||
$cutoffDate = $this->autoPublish->secondSundayBackwardDate($now);
|
||||
|
||||
$query = DB::table('attendance_day')
|
||||
->where('status', 'submitted')
|
||||
->where(function ($q) use ($nowStr, $cutoffDate) {
|
||||
$q->where('auto_publish_at', '<=', $nowStr)
|
||||
->orWhere(function ($sub) use ($cutoffDate) {
|
||||
$sub->whereNull('auto_publish_at')
|
||||
->where('date', '<=', $cutoffDate);
|
||||
});
|
||||
});
|
||||
|
||||
$count = (clone $query)->count();
|
||||
if ($count > 0) {
|
||||
$query->update([
|
||||
'status' => 'published',
|
||||
'published_by' => 0,
|
||||
'published_at' => $nowStr,
|
||||
'updated_at' => $nowStr,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'published' => $count,
|
||||
'checked_at' => $nowStr,
|
||||
'timezone' => $zone->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
class AttendanceAutoPublishService
|
||||
{
|
||||
public function timezone(?string $tzFromCfg = null): DateTimeZone
|
||||
{
|
||||
$tz = $tzFromCfg ?: (class_exists(\Config\School::class)
|
||||
? (config('School')->attendance['timezone'] ?? null)
|
||||
: null);
|
||||
|
||||
return new DateTimeZone($tz ?: date_default_timezone_get());
|
||||
}
|
||||
|
||||
public function secondSundayAfterEndOfDay(string $ymd, ?string $tz = null): string
|
||||
{
|
||||
$zone = $this->timezone($tz);
|
||||
$day = new DateTimeImmutable($ymd . ' 00:00:00', $zone);
|
||||
|
||||
$weekday = (int) $day->format('w');
|
||||
$daysToNextSunday = (7 - $weekday) % 7;
|
||||
if ($daysToNextSunday === 0) {
|
||||
$daysToNextSunday = 7;
|
||||
}
|
||||
|
||||
$firstSunday = $day->modify("+{$daysToNextSunday} days");
|
||||
$secondSunday = $firstSunday->modify('+7 days');
|
||||
|
||||
return $secondSunday->setTime(23, 59, 59)->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function secondSundayBackwardDate(DateTimeImmutable $now, ?string $tz = null): string
|
||||
{
|
||||
$zone = $this->timezone($tz);
|
||||
$inZone = $now->setTimezone($zone);
|
||||
$weekday = (int) $inZone->format('w');
|
||||
$thisSunday = $inZone->setTime(0, 0, 0)->modify("-{$weekday} days");
|
||||
$twoBack = $thisSunday->modify('-14 days');
|
||||
|
||||
return $twoBack->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceTracking;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\Notifications\UserNotificationDispatchService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AttendanceConsequenceService
|
||||
{
|
||||
public function __construct(
|
||||
private EmailService $emailService,
|
||||
private UserNotificationDispatchService $notifier
|
||||
) {
|
||||
}
|
||||
|
||||
public function sendFollowUp(array $payload): array
|
||||
{
|
||||
return $this->send(
|
||||
'follow_up',
|
||||
$payload,
|
||||
'Attendance Follow Up',
|
||||
'emails/follow_up',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
public function sendFinalWarning(array $payload): array
|
||||
{
|
||||
return $this->send(
|
||||
'final_warning',
|
||||
$payload,
|
||||
'Final Warning: Attendance Issue',
|
||||
'emails/final_warning',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
public function sendDismissal(array $payload): array
|
||||
{
|
||||
return $this->send(
|
||||
'dismissal',
|
||||
$payload,
|
||||
'Notice of Dismissal',
|
||||
'emails/dismissal',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
private function send(
|
||||
string $type,
|
||||
array $payload,
|
||||
string $defaultSubject,
|
||||
string $viewName,
|
||||
string $logLevel
|
||||
): array {
|
||||
$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'] ?? ''));
|
||||
$className = (string) ($payload['class']['class_section_name'] ?? '');
|
||||
$semester = (string) ($payload['semester'] ?? '');
|
||||
$schoolYear = (string) ($payload['school_year'] ?? '');
|
||||
$dateYmd = (string) ($payload['date'] ?? '');
|
||||
$dateFmt = $dateYmd !== '' ? date('m-d-Y', strtotime($dateYmd)) : '';
|
||||
$sentAt = $payload['now'] ?? utc_now();
|
||||
|
||||
if ($parentEmail === '') {
|
||||
Log::warning("Attendance {$type}: missing parent email", [
|
||||
'student_id' => $payload['student_id'] ?? null,
|
||||
]);
|
||||
return ['ok' => false, 'message' => 'Missing parent email.'];
|
||||
}
|
||||
|
||||
$subject = (string) ($payload['subject'] ?? $this->buildSubject($defaultSubject, $studentName, $dateFmt));
|
||||
|
||||
$emailData = [
|
||||
'title' => $subject,
|
||||
'student_name' => $studentName,
|
||||
'parent_name' => $parentName,
|
||||
'teacher_name' => $teacherName,
|
||||
'class_section_name' => $className,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date_fmt' => $dateFmt,
|
||||
'sent_at' => $sentAt,
|
||||
];
|
||||
|
||||
$html = trim((string) ($payload['html'] ?? ''));
|
||||
if ($html === '') {
|
||||
try {
|
||||
$html = view($viewName, $emailData, ['saveData' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
$html = '<p>' . e($subject) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
$ok = false;
|
||||
try {
|
||||
$ok = $this->emailService->send($parentEmail, $subject, $html, 'attendance');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Attendance email send failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$summary = $this->buildInAppSummary($type, $studentName, $dateFmt);
|
||||
$parentUserId = (int) ($payload['parent']['user_id'] ?? 0);
|
||||
if ($parentUserId > 0) {
|
||||
$this->notifier->notifyUser(
|
||||
$parentUserId,
|
||||
$subject,
|
||||
$summary,
|
||||
['in_app'],
|
||||
'attendance',
|
||||
'parent'
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($payload['tracking_id'])) {
|
||||
AttendanceTracking::markAsNotified((int) $payload['tracking_id']);
|
||||
}
|
||||
|
||||
Log::log($logLevel, "Attendance {$type} email " . ($ok ? 'sent' : 'failed'), [
|
||||
'student_id' => $payload['student_id'] ?? null,
|
||||
'recipient' => $parentEmail,
|
||||
]);
|
||||
|
||||
return ['ok' => $ok];
|
||||
}
|
||||
|
||||
private function buildSubject(string $base, string $studentName, string $dateFmt): string
|
||||
{
|
||||
$name = $studentName !== '' ? " — {$studentName}" : '';
|
||||
$date = $dateFmt !== '' ? " ({$dateFmt})" : '';
|
||||
return $base . $name . $date;
|
||||
}
|
||||
|
||||
private function buildInAppSummary(string $type, string $studentName, string $dateFmt): string
|
||||
{
|
||||
$who = $studentName !== '' ? $studentName : 'a student';
|
||||
$when = $dateFmt !== '' ? " on {$dateFmt}" : '';
|
||||
|
||||
return match ($type) {
|
||||
'dismissal' => "Dismissal notice sent for {$who}{$when}.",
|
||||
'final_warning' => "Final warning sent for {$who}{$when}.",
|
||||
default => "Follow-up notice sent for {$who}{$when}.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Services\EmailService;
|
||||
use App\Services\Notifications\UserNotificationDispatchService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
private function fetchTodaySummary(string $type): array
|
||||
{
|
||||
$start = now()->startOfDay();
|
||||
$endEx = now()->addDay()->startOfDay();
|
||||
|
||||
$query = DB::table('attendance_data as ad')
|
||||
->join('students as s', 's.id', '=', 'ad.student_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 's.parent_id')
|
||||
->select(
|
||||
'ad.date',
|
||||
's.id as student_id',
|
||||
's.firstname as student_firstname',
|
||||
's.lastname as student_lastname',
|
||||
'u.id as parent_id',
|
||||
'u.email as parent_email',
|
||||
'u.firstname as parent_firstname',
|
||||
'u.lastname as parent_lastname'
|
||||
)
|
||||
->where('ad.date', '>=', $start)
|
||||
->where('ad.date', '<', $endEx);
|
||||
|
||||
if ($type === 'absent') {
|
||||
$query->where(function ($w) {
|
||||
$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'])
|
||||
->orWhereRaw("UPPER(TRIM(ad.status)) LIKE 'LATE%'");
|
||||
});
|
||||
}
|
||||
|
||||
return $query->get()->map(fn ($row) => (array) $row)->all();
|
||||
}
|
||||
|
||||
private function notifyParents(array $rows, string $type): int
|
||||
{
|
||||
$sent = 0;
|
||||
foreach ($rows as $row) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentName = trim((string) ($row['student_firstname'] ?? '') . ' ' . (string) ($row['student_lastname'] ?? ''));
|
||||
$date = substr((string) ($row['date'] ?? ''), 0, 10);
|
||||
|
||||
$message = sprintf(
|
||||
'Daily Attendance Update: %s was marked %s on %s.',
|
||||
$studentName !== '' ? $studentName : 'Your student',
|
||||
$type,
|
||||
$date
|
||||
);
|
||||
|
||||
$this->notifier->notifyUser($parentId, 'Daily Attendance Update', $message, ['in_app'], 'attendance', 'parent');
|
||||
|
||||
$email = (string) ($row['parent_email'] ?? '');
|
||||
if ($email !== '') {
|
||||
$body = '<p>' . e($message) . '</p>';
|
||||
$this->emailService->send($email, 'Daily Attendance Update', $body, 'attendance');
|
||||
}
|
||||
|
||||
$sent++;
|
||||
}
|
||||
|
||||
return $sent;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class AttendancePolicyService
|
||||
@@ -42,24 +43,14 @@ class AttendancePolicyService
|
||||
public function isAdminLike(array $roles, array $permissions = []): bool
|
||||
{
|
||||
$roles = array_map([$this, 'normalizeRole'], $roles);
|
||||
$adminTokens = $this->adminRoleTokens();
|
||||
|
||||
$excluded = [
|
||||
'parent',
|
||||
'guest',
|
||||
'teacher',
|
||||
'teacher_assistant',
|
||||
'assistant_teacher',
|
||||
'ta',
|
||||
];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if ($role !== '' && !in_array($role, $excluded, true)) {
|
||||
return true;
|
||||
}
|
||||
if (!empty($adminTokens) && count(array_intersect($roles, $adminTokens)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = strtolower(trim((string)$permission));
|
||||
$permission = strtolower(trim((string) $permission));
|
||||
if (str_contains($permission, 'attendance.manage') || str_contains($permission, 'attendance.admin')) {
|
||||
return true;
|
||||
}
|
||||
@@ -72,4 +63,66 @@ class AttendancePolicyService
|
||||
{
|
||||
return str_replace([' ', '-'], '_', strtolower(trim((string)$role)));
|
||||
}
|
||||
}
|
||||
|
||||
public function canManageEarlyDismissals(array $userRoles): bool
|
||||
{
|
||||
$normalized = array_map([$this, 'normalizeRole'], $userRoles);
|
||||
$adminTokens = $this->adminRoleTokens();
|
||||
$isAdminLike = count(array_intersect($normalized, $adminTokens)) > 0;
|
||||
$isTeacher = in_array('teacher', $normalized, true) || in_array('teacher_assistant', $normalized, true);
|
||||
|
||||
return $isAdminLike || $isTeacher;
|
||||
}
|
||||
|
||||
private function adminRoleTokens(): array
|
||||
{
|
||||
static $cache = null;
|
||||
if (is_array($cache)) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$tokens = [];
|
||||
$normalize = fn (string $value): string => $this->normalizeRole($value);
|
||||
|
||||
try {
|
||||
$rows = Role::query()
|
||||
->where('is_active', 1)
|
||||
->get(['name', 'slug']);
|
||||
|
||||
$excluded = array_map($normalize, [
|
||||
'guest',
|
||||
'parent',
|
||||
'student',
|
||||
'teacher',
|
||||
'teacher_assistant',
|
||||
'ta',
|
||||
]);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$name = $normalize((string) ($row->name ?? ''));
|
||||
$slug = $normalize((string) ($row->slug ?? $name));
|
||||
if ($name === '' && $slug === '') {
|
||||
continue;
|
||||
}
|
||||
if (in_array($slug, $excluded, true) || in_array($name, $excluded, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($name !== '') {
|
||||
$tokens[$name] = true;
|
||||
}
|
||||
if ($slug !== '') {
|
||||
$tokens[$slug] = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// ignore and fall back to defaults
|
||||
}
|
||||
|
||||
foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $fallback) {
|
||||
$tokens[$normalize($fallback)] = true;
|
||||
}
|
||||
|
||||
$cache = array_keys($tokens);
|
||||
return $cache;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceSummaryRebuildService
|
||||
{
|
||||
public function rebuild(): array
|
||||
{
|
||||
DB::table('attendance_record')->truncate();
|
||||
|
||||
$rows = DB::table('attendance_data')
|
||||
->orderBy('student_id')
|
||||
->orderBy('school_year')
|
||||
->orderBy('semester')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
if (empty($rows)) {
|
||||
return ['inserted' => 0];
|
||||
}
|
||||
|
||||
$summary = [];
|
||||
foreach ($rows as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$schoolYear = (string) ($row['school_year'] ?? '');
|
||||
$semester = (string) ($row['semester'] ?? '');
|
||||
$status = strtolower((string) ($row['status'] ?? ''));
|
||||
|
||||
$key = $studentId . '-' . $schoolYear . '-' . $semester;
|
||||
if (!isset($summary[$key])) {
|
||||
$summary[$key] = [
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'class_section_id' => $row['class_section_id'] ?? null,
|
||||
'school_id' => $row['school_id'] ?? null,
|
||||
'total_presence' => 0,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 0,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
}
|
||||
|
||||
$summary[$key]['total_attendance']++;
|
||||
if ($status === 'present') {
|
||||
$summary[$key]['total_presence']++;
|
||||
} elseif ($status === 'absent') {
|
||||
$summary[$key]['total_absence']++;
|
||||
} elseif ($status === 'late') {
|
||||
$summary[$key]['total_late']++;
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('attendance_record')->insert(array_values($summary));
|
||||
|
||||
return ['inserted' => count($summary)];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Libraries\AttendanceAutoPublish;
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\Student;
|
||||
@@ -21,6 +20,7 @@ class TeacherAttendanceSubmissionService
|
||||
protected Student $student,
|
||||
protected AttendancePolicyService $attendancePolicyService,
|
||||
protected StudentAttendanceWriterService $studentAttendanceWriterService,
|
||||
protected AttendanceAutoPublishService $attendanceAutoPublishService,
|
||||
) {}
|
||||
|
||||
public function submit(
|
||||
@@ -201,7 +201,7 @@ class TeacherAttendanceSubmissionService
|
||||
]);
|
||||
}
|
||||
|
||||
$autoPublishAt = AttendanceAutoPublish::secondSundayAfterEndOfDay($attendDate);
|
||||
$autoPublishAt = $this->attendanceAutoPublishService->secondSundayAfterEndOfDay($attendDate);
|
||||
|
||||
$dayRow->update([
|
||||
'status' => 'submitted',
|
||||
@@ -217,4 +217,4 @@ class TeacherAttendanceSubmissionService
|
||||
'message' => 'Attendance submitted successfully.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user