reconstruction of the project
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
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;
|
||||
|
||||
class AttendanceNotificationWorkflowService
|
||||
{
|
||||
protected string $semester;
|
||||
protected string $schoolYear;
|
||||
|
||||
public function __construct(
|
||||
protected Student $studentModel,
|
||||
protected StudentClass $studentClassModel,
|
||||
protected AttendanceData $attendanceDataModel,
|
||||
protected AttendanceTracking $attendanceTrackingModel,
|
||||
protected Configuration $configModel,
|
||||
protected AttendanceMailerService $attendanceMailerService,
|
||||
protected ViolationRuleEngineService $violationRuleEngine,
|
||||
protected AttendanceParentLookupService $parentLookupService,
|
||||
protected AttendanceEmailComposerService $emailComposerService,
|
||||
protected AttendanceNotificationLogService $notificationLogService
|
||||
) {
|
||||
$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;
|
||||
$schoolYear = $data['school_year'] ?? $this->schoolYear;
|
||||
$subject = $data['subject_type'] ?? 'Absent';
|
||||
|
||||
$student = $this->studentModel->query()->find($sid)?->toArray() ?? [];
|
||||
$parentEmail = $data['parent_email'] ?? '';
|
||||
$parentName = $data['parent_name'] ?? '';
|
||||
|
||||
if (!$parentEmail) {
|
||||
$parent = $this->parentLookupService->getPrimaryParentForStudent($sid) ?? [];
|
||||
$parentEmail = $parent['email'] ?? '';
|
||||
$parentName = $parent['parent_name'] ?? '';
|
||||
}
|
||||
|
||||
if (!$parentEmail) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No parent email found for this student.',
|
||||
'status' => 422,
|
||||
];
|
||||
}
|
||||
|
||||
[$start, $end] = $this->violationRuleEngine->dayBounds($ymd);
|
||||
|
||||
$row = $this->attendanceTrackingModel->query()
|
||||
->where('student_id', $sid)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $end)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$row = $row?->toArray();
|
||||
|
||||
$consequence = null;
|
||||
$reasonTxt = (string) ($row['reason'] ?? '');
|
||||
if (preg_match('/\bABS_4\b|dismissal/i', $reasonTxt)) {
|
||||
$consequence = 'dismissal';
|
||||
} elseif (preg_match('/\bABS_3\b|final_warning/i', $reasonTxt)) {
|
||||
$consequence = 'final_warning';
|
||||
} elseif (preg_match('/\bABS_2\b|follow_up/i', $reasonTxt)) {
|
||||
$consequence = 'follow_up';
|
||||
}
|
||||
|
||||
$class = method_exists($this->studentClassModel, 'getClassSectionsByStudentId')
|
||||
? $this->studentClassModel->getClassSectionsByStudentId($sid, $this->schoolYear)
|
||||
: [];
|
||||
|
||||
$payload = [
|
||||
'student_id' => $sid,
|
||||
'student' => $student,
|
||||
'parent' => ['email' => $parentEmail, 'name' => $parentName],
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'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')) {
|
||||
$this->attendanceTrackingModel->markAsNotified((int) $row['id']);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Notification queued.',
|
||||
'data' => $payload,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Attendance record queue failed: ' . $e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function sendAutoEmails(array $violations, ?string $variantOverride = null): array
|
||||
{
|
||||
$toSend = array_values(array_filter($violations, fn ($v) => ($v['action'] ?? '') === 'auto_email'));
|
||||
|
||||
$sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
$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'] ?? ''));
|
||||
$pname = (string) ($v['parent_name'] ?? '');
|
||||
|
||||
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);
|
||||
$context = $this->emailComposerService->buildTemplateContext($v, $parent);
|
||||
$variant = $this->emailComposerService->pickVariant($code, $variantOverride);
|
||||
$tpl = $this->emailComposerService->renderTemplate($code, $variant, $context);
|
||||
|
||||
if (!$tpl) {
|
||||
[$subject, $body] = $this->emailComposerService->buildFallbackAutoEmail($code, $v, $pname);
|
||||
} else {
|
||||
[$subject, $body] = $tpl;
|
||||
}
|
||||
|
||||
$wrapped = $this->emailComposerService->renderWithEmailLayout($subject, $body);
|
||||
$anyOk = false;
|
||||
|
||||
try {
|
||||
$ok = $this->attendanceMailerService->send($to, $subject, $wrapped);
|
||||
|
||||
if ($ok) {
|
||||
$anyOk = true;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'sent'];
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
'email',
|
||||
$to,
|
||||
$subject,
|
||||
'sent',
|
||||
'OK',
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
} else {
|
||||
$errors++;
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
'email',
|
||||
$to,
|
||||
$subject,
|
||||
'failed',
|
||||
'send returned false',
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'failed'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
'email',
|
||||
$to,
|
||||
$subject,
|
||||
'failed',
|
||||
$e->getMessage(),
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: ' . $e->getMessage()];
|
||||
}
|
||||
|
||||
try {
|
||||
$sec = $this->parentLookupService->getSecondaryParentForStudent($sid, $this->schoolYear);
|
||||
$to2 = trim((string) ($sec['email'] ?? ''));
|
||||
|
||||
if ($to2 !== '' && strcasecmp($to2, $to) !== 0) {
|
||||
if ($this->notificationLogService->notificationAlreadySent($sid, $code, $date, 'email', $to2)) {
|
||||
$skipped++;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate_cc'];
|
||||
} else {
|
||||
$ok2 = $this->attendanceMailerService->send($to2, $subject, $wrapped);
|
||||
if ($ok2) {
|
||||
$anyOk = true;
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
'email',
|
||||
$to2,
|
||||
$subject,
|
||||
'sent',
|
||||
'OK',
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_sent'];
|
||||
} else {
|
||||
$errors++;
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
'email',
|
||||
$to2,
|
||||
$subject,
|
||||
'failed',
|
||||
'send returned false',
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_failed'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
$details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: ' . $e->getMessage()];
|
||||
}
|
||||
|
||||
if ($anyOk) {
|
||||
$sent++;
|
||||
|
||||
if (method_exists($this->attendanceTrackingModel, 'markRuleAsNotified')) {
|
||||
$this->attendanceTrackingModel->markRuleAsNotified(
|
||||
$sid,
|
||||
$code,
|
||||
$date,
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
}
|
||||
|
||||
$allDates = [];
|
||||
$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)) {
|
||||
$allDates = array_merge($allDates, $absDates);
|
||||
}
|
||||
if (!empty($lateDates)) {
|
||||
$allDates = array_merge($allDates, $lateDates);
|
||||
}
|
||||
if (empty($allDates)) {
|
||||
$allDates[] = substr($date, 0, 10);
|
||||
}
|
||||
|
||||
if (method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
|
||||
$this->attendanceDataModel->markReportedAndNotified(
|
||||
$sid,
|
||||
$allDates,
|
||||
$this->semester,
|
||||
$this->schoolYear
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'sent' => $sent,
|
||||
'skipped' => $skipped,
|
||||
'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'];
|
||||
$bodyInput = (string) $data['body_html'];
|
||||
$code = (string) $data['code'];
|
||||
$variant = (string) ($data['variant'] ?? 'default');
|
||||
|
||||
$ymdRaw = (string) ($data['incident_date'] ?? '');
|
||||
$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') ?? '');
|
||||
$schoolYear = $this->schoolYear ?: (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
|
||||
try {
|
||||
$anyOk = false;
|
||||
|
||||
$ok = $this->attendanceMailerService->send($to, $subject, $wrapped);
|
||||
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$ymd,
|
||||
'email',
|
||||
$to,
|
||||
$subject,
|
||||
$ok ? 'sent' : 'failed',
|
||||
$ok ? 'OK' : 'send returned false',
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
$anyOk = true;
|
||||
}
|
||||
|
||||
$sec = $this->parentLookupService->getSecondaryParentForStudent($sid, $schoolYear);
|
||||
$to2 = trim((string) ($sec['email'] ?? ''));
|
||||
|
||||
if ($to2 !== '' && strcasecmp($to2, $to) !== 0) {
|
||||
if (!$this->notificationLogService->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) {
|
||||
$ok2 = $this->attendanceMailerService->send($to2, $subject, $wrapped);
|
||||
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$ymd,
|
||||
'email',
|
||||
$to2,
|
||||
$subject,
|
||||
$ok2 ? 'sent' : 'failed',
|
||||
$ok2 ? 'OK' : 'send returned false',
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
|
||||
if ($ok2) {
|
||||
$anyOk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$anyOk) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to send email.',
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
|
||||
if (method_exists($this->attendanceTrackingModel, 'markRuleAsNotified')) {
|
||||
$this->attendanceTrackingModel->markRuleAsNotified(
|
||||
$sid,
|
||||
$code,
|
||||
$ymd !== '' ? $ymd : date('Y-m-d'),
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($violationDates) && method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
|
||||
$this->attendanceDataModel->markReportedAndNotified(
|
||||
$sid,
|
||||
$violationDates,
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Email sent successfully.',
|
||||
'data' => [
|
||||
'student_id' => $sid,
|
||||
'to' => $to,
|
||||
'secondary_to' => $to2 ?? null,
|
||||
'subject' => $subject,
|
||||
'variant' => $variant,
|
||||
'incident_date' => $ymd,
|
||||
],
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
$this->notificationLogService->logNotification(
|
||||
$sid,
|
||||
$code,
|
||||
$ymd,
|
||||
'email',
|
||||
$to,
|
||||
$subject,
|
||||
'failed',
|
||||
$e->getMessage(),
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'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']);
|
||||
$ymdRaw = (string) ($data['incident_date'] ?? '');
|
||||
$ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
|
||||
|
||||
try {
|
||||
$day = $ymd !== '' ? $ymd : date('Y-m-d');
|
||||
[$start, $end] = $this->violationRuleEngine->dayBounds($day);
|
||||
|
||||
$row = $this->attendanceTrackingModel->query()
|
||||
->where('student_id', $sid)
|
||||
->where('semester', $this->semester)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $end)
|
||||
->where('reason', 'like', '%' . $code . '%')
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
$row = $this->attendanceTrackingModel->query()
|
||||
->where('student_id', $sid)
|
||||
->where('semester', $this->semester)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $end)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
}
|
||||
|
||||
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(),
|
||||
]);
|
||||
} else {
|
||||
$this->attendanceTrackingModel->query()->create([
|
||||
'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(),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Note saved.',
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save note: ' . $e->getMessage(),
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user