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, ]; } } }