add projet
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
class AdminNotificationService
|
||||
{
|
||||
public function alertsPayload(): array
|
||||
{
|
||||
$admins = $this->fetchAdminNotificationUsers();
|
||||
$subjects = $this->notificationSubjectOptions();
|
||||
$assignedSubjects = [];
|
||||
$tableReady = $this->hasTable('admin_notification_subjects');
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = DB::table('admin_notification_subjects')
|
||||
->select('id', 'admin_id', 'subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
$subject = (string) ($row->subject ?? '');
|
||||
|
||||
if ($adminId <= 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assignedSubjects[$adminId] ??= [];
|
||||
$assignedSubjects[$adminId][$subject] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'subjects' => $subjects,
|
||||
'assigned_subjects' => $assignedSubjects,
|
||||
'table_ready' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Input shape (API):
|
||||
* [
|
||||
* 'subjects' => [
|
||||
* 12 => ['finance' => true, 'attendance' => true],
|
||||
* 15 => ['general', 'events']
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
public function saveSubjects(array $posted): array
|
||||
{
|
||||
if (!$this->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
'updated' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$subjectsMap = $this->notificationSubjectOptions();
|
||||
$allowed = array_keys($subjectsMap);
|
||||
|
||||
$admins = $this->fetchAdminNotificationUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
if (empty($adminIds)) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'No admins found to update.',
|
||||
'updated' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$existing = DB::table('admin_notification_subjects')
|
||||
->select('id', 'admin_id', 'subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
$existingMap = [];
|
||||
foreach ($existing as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
$subject = (string) ($row->subject ?? '');
|
||||
if ($adminId <= 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
$existingMap[$adminId] ??= [];
|
||||
$existingMap[$adminId][$subject] = (int) ($row->id ?? 0);
|
||||
}
|
||||
|
||||
$updates = 0;
|
||||
|
||||
DB::transaction(function () use ($posted, $adminIds, $allowed, $existingMap, &$updates) {
|
||||
foreach ($adminIds as $adminId) {
|
||||
$subjectRaw = $posted[$adminId] ?? [];
|
||||
|
||||
$selected = [];
|
||||
if (is_array($subjectRaw)) {
|
||||
foreach ($subjectRaw as $key => $value) {
|
||||
$candidate = is_string($key) ? $key : $value;
|
||||
if (!is_string($candidate)) {
|
||||
continue;
|
||||
}
|
||||
$candidate = trim($candidate);
|
||||
if ($candidate !== '') {
|
||||
$selected[] = $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selected = array_values(array_unique(array_filter(
|
||||
$selected,
|
||||
fn ($value) => in_array($value, $allowed, true)
|
||||
)));
|
||||
|
||||
$current = array_keys($existingMap[$adminId] ?? []);
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
DB::table('admin_notification_subjects')
|
||||
->where('admin_id', $adminId)
|
||||
->whereIn('subject', $toDelete)
|
||||
->delete();
|
||||
|
||||
$updates += count($toDelete);
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $subject) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => $subject,
|
||||
];
|
||||
}
|
||||
|
||||
DB::table('admin_notification_subjects')->insert($batch);
|
||||
$updates += count($toInsert);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.',
|
||||
'updated' => $updates,
|
||||
];
|
||||
}
|
||||
|
||||
public function printRecipientsPayload(): array
|
||||
{
|
||||
$admins = $this->fetchAdminNotificationUsers();
|
||||
$tableReady = $this->hasTable('admin_notification_subjects');
|
||||
$assigned = [];
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
$rows = DB::table('admin_notification_subjects')
|
||||
->select('admin_id')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row->admin_id ?? 0);
|
||||
if ($adminId > 0) {
|
||||
$assigned[$adminId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'assigned' => $assigned,
|
||||
'table_ready' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Input shape (API):
|
||||
* ['notify' => [12 => true, 15 => true]]
|
||||
*/
|
||||
public function savePrintRecipients(array $postedNotify): array
|
||||
{
|
||||
if (!$this->hasTable('admin_notification_subjects')) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Notification subject storage is missing. Run migrations first.',
|
||||
'updated' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$admins = $this->fetchAdminNotificationUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
|
||||
if (empty($adminIds)) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'No admins found to update.',
|
||||
'updated' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$selected = [];
|
||||
foreach ((array) $postedNotify as $key => $value) {
|
||||
$adminId = (int) $key;
|
||||
if ($adminId <= 0) continue;
|
||||
if (!in_array($adminId, $adminIds, true)) continue;
|
||||
$selected[] = $adminId;
|
||||
}
|
||||
$selected = array_values(array_unique($selected));
|
||||
|
||||
$existingRows = DB::table('admin_notification_subjects')
|
||||
->select('admin_id')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->get();
|
||||
|
||||
$current = array_values(array_unique(array_map(
|
||||
fn ($row) => (int) ($row->admin_id ?? 0),
|
||||
$existingRows->all()
|
||||
)));
|
||||
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
DB::transaction(function () use ($toDelete, $toInsert) {
|
||||
if (!empty($toDelete)) {
|
||||
DB::table('admin_notification_subjects')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $toDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $adminId) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => 'print_requests',
|
||||
];
|
||||
}
|
||||
DB::table('admin_notification_subjects')->insert($batch);
|
||||
}
|
||||
});
|
||||
|
||||
$changes = count($toDelete) + count($toInsert);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.',
|
||||
'updated' => $changes,
|
||||
'selected_admin_ids' => $selected,
|
||||
];
|
||||
}
|
||||
|
||||
public function notificationSubjectOptions(): array
|
||||
{
|
||||
return [
|
||||
'academics' => 'Academics',
|
||||
'attendance' => 'Attendance',
|
||||
'events' => 'Events',
|
||||
'finance' => 'Finance',
|
||||
'general' => 'General',
|
||||
'print_requests' => 'Print Requests',
|
||||
];
|
||||
}
|
||||
|
||||
private function getAdminNotificationExcludedRoles(): array
|
||||
{
|
||||
return [
|
||||
'parent',
|
||||
'student',
|
||||
'guest',
|
||||
'teacher',
|
||||
'assistant teacher',
|
||||
'teacher assistant',
|
||||
'teacher_assistant',
|
||||
'assistant_teacher',
|
||||
'ta',
|
||||
'authorized_user',
|
||||
];
|
||||
}
|
||||
|
||||
public function fetchAdminNotificationUsers(): array
|
||||
{
|
||||
$excluded = array_map(
|
||||
fn ($value) => strtolower(trim((string) $value)),
|
||||
$this->getAdminNotificationExcludedRoles()
|
||||
);
|
||||
|
||||
// Safer than raw NOT IN string: use bindings + lower(name)
|
||||
$rows = DB::table('users as u')
|
||||
->select('u.id', 'u.firstname', 'u.lastname', 'u.email')
|
||||
->join('user_roles as ur', 'ur.user_id', '=', 'u.id')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->whereNull('ur.deleted_at')
|
||||
->whereNotNull('r.name')
|
||||
->whereNotIn(DB::raw('LOWER(r.name)'), $excluded)
|
||||
->groupBy('u.id', 'u.firstname', 'u.lastname', 'u.email')
|
||||
->orderBy('u.lastname')
|
||||
->orderBy('u.firstname')
|
||||
->get();
|
||||
|
||||
return $rows->map(function ($row) {
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'firstname' => (string) ($row->firstname ?? ''),
|
||||
'lastname' => (string) ($row->lastname ?? ''),
|
||||
'email' => (string) ($row->email ?? ''),
|
||||
'full_name' => trim((string)($row->firstname ?? '') . ' ' . (string)($row->lastname ?? '')),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
try {
|
||||
return DB::getSchemaBuilder()->hasTable($table);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class AdminProgressQueryService
|
||||
{
|
||||
public function baseIndexQuery(): Builder
|
||||
{
|
||||
return ClassProgressReport::query()
|
||||
->from('class_progress_reports')
|
||||
->select([
|
||||
'class_progress_reports.*',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->selectRaw("CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS teacher_name")
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'class_progress_reports.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'class_progress_reports.teacher_id');
|
||||
}
|
||||
|
||||
public function applyFilters(Builder $query, array $filters): Builder
|
||||
{
|
||||
if (!empty($filters['from'])) {
|
||||
$query->where('week_start', '>=', $filters['from']);
|
||||
}
|
||||
|
||||
if (!empty($filters['to'])) {
|
||||
$query->where('week_end', '<=', $filters['to']);
|
||||
}
|
||||
|
||||
if (!empty($filters['class_section_id'])) {
|
||||
$query->where('class_progress_reports.class_section_id', (int) $filters['class_section_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$query->where('class_progress_reports.status', $filters['status']);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function baseShowQuery(): Builder
|
||||
{
|
||||
return ClassProgressReport::query()
|
||||
->from('class_progress_reports')
|
||||
->select([
|
||||
'class_progress_reports.*',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->selectRaw("CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.lastname, '')) AS teacher_name")
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'class_progress_reports.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'class_progress_reports.teacher_id');
|
||||
}
|
||||
|
||||
public function groupReportsByWeekAndSection(array $rows): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? '');
|
||||
if ($key === '_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($groups[$key])) {
|
||||
$groups[$key] = [
|
||||
'week_start' => $row['week_start'] ?? null,
|
||||
'week_end' => $row['week_end'] ?? null,
|
||||
'class_section_id' => $row['class_section_id'] ?? null,
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'reports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$subject = (string) ($row['subject'] ?? 'unknown');
|
||||
$groups[$key]['reports'][$subject] = $row;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
class TeacherSubmissionService
|
||||
{
|
||||
public function __construct(
|
||||
private AdminContextService $context
|
||||
) {}
|
||||
|
||||
public function report(): array
|
||||
{
|
||||
$cfg = $this->context->currentConfig();
|
||||
$semester = (string) ($cfg['semester'] ?? '');
|
||||
$schoolYear = (string) ($cfg['school_year'] ?? '');
|
||||
|
||||
$assignmentRows = DB::table('teacher_class as tc')
|
||||
->select([
|
||||
'tc.class_section_id',
|
||||
'cs.class_section_name',
|
||||
'tc.teacher_id',
|
||||
'u.firstname',
|
||||
'u.lastname',
|
||||
'tc.position',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'tc.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'tc.teacher_id')
|
||||
->where('tc.school_year', $schoolYear)
|
||||
->where('tc.semester', $semester)
|
||||
->orderBy('cs.class_section_name')
|
||||
->get();
|
||||
|
||||
$teachersBySection = [];
|
||||
foreach ($assignmentRows as $assignment) {
|
||||
$sectionId = (int) ($assignment->class_section_id ?? 0);
|
||||
if ($sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$positionKey = strtolower(trim((string) ($assignment->position ?? '')));
|
||||
$roleKey = $positionKey !== '' ? $positionKey : 'teacher';
|
||||
|
||||
$positionLabel = match ($roleKey) {
|
||||
'ta' => 'TA',
|
||||
'main' => 'Main',
|
||||
default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher',
|
||||
};
|
||||
|
||||
$teacherFullName = trim(
|
||||
(string)($assignment->firstname ?? '') . ' ' . (string)($assignment->lastname ?? '')
|
||||
);
|
||||
$teacherId = (int) ($assignment->teacher_id ?? 0);
|
||||
|
||||
if ($teacherId <= 0 || $teacherFullName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($teachersBySection[$sectionId])) {
|
||||
$teachersBySection[$sectionId] = [
|
||||
'class_section' => (string) ($assignment->class_section_name ?? "Section {$sectionId}"),
|
||||
'teachers' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$teachersBySection[$sectionId]['teachers'][] = [
|
||||
'id' => $teacherId,
|
||||
'label' => "{$positionLabel}: {$teacherFullName}",
|
||||
'role_key' => $roleKey,
|
||||
];
|
||||
}
|
||||
|
||||
$today = Carbon::now()->format('Y-m-d');
|
||||
|
||||
$rows = [];
|
||||
$totalStatuses = 0;
|
||||
$missingItemCount = 0;
|
||||
$allTeacherIds = [];
|
||||
$allClassSectionIds = [];
|
||||
|
||||
foreach ($teachersBySection as $classSectionId => $section) {
|
||||
$classSectionId = (int) $classSectionId;
|
||||
if ($classSectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentIds = DB::table('student_class')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->pluck('student_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->filter(fn ($id) => $id > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$expected = count($studentIds);
|
||||
|
||||
// semester_scores
|
||||
$midtermStudents = [];
|
||||
$participationStudents = [];
|
||||
|
||||
$scoreRecords = DB::table('semester_scores')
|
||||
->select('student_id', 'midterm_exam_score', 'participation_score')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
foreach ($scoreRecords as $score) {
|
||||
$sid = (int) ($score->student_id ?? 0);
|
||||
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$midtermValue = trim((string) ($score->midterm_exam_score ?? ''));
|
||||
if ($midtermValue !== '') {
|
||||
$midtermStudents[$sid] = true;
|
||||
}
|
||||
|
||||
$participationValue = trim((string) ($score->participation_score ?? ''));
|
||||
if ($participationValue !== '') {
|
||||
$participationStudents[$sid] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// score_comments
|
||||
$midtermCommentStudents = [];
|
||||
$ptapCommentStudents = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$comments = DB::table('score_comments')
|
||||
->select('student_id', 'score_type', 'comment')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('score_type', ['midterm', 'ptap'])
|
||||
->get();
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
$sid = (int) ($comment->student_id ?? 0);
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$text = trim((string) ($comment->comment ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = strtolower(trim((string) ($comment->score_type ?? '')));
|
||||
if ($type === 'midterm') {
|
||||
$midtermCommentStudents[$sid] = true;
|
||||
}
|
||||
if ($type === 'ptap') {
|
||||
$ptapCommentStudents[$sid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attendance_days (today)
|
||||
$attendanceRow = DB::table('attendance_days')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', $today)
|
||||
->first();
|
||||
|
||||
$attendanceSubmitted = $attendanceRow
|
||||
&& in_array(
|
||||
strtolower((string) ($attendanceRow->status ?? '')),
|
||||
['submitted', 'published', 'finalized'],
|
||||
true
|
||||
);
|
||||
|
||||
$teacherList = $section['teachers'] ?? [];
|
||||
if (!empty($teacherList)) {
|
||||
usort($teacherList, function (array $a, array $b) {
|
||||
return $this->teacherRolePriority($a['role_key'] ?? 'teacher')
|
||||
<=> $this->teacherRolePriority($b['role_key'] ?? 'teacher');
|
||||
});
|
||||
$teacherList = array_values($teacherList);
|
||||
}
|
||||
|
||||
foreach ($teacherList as $teacherEntry) {
|
||||
if (!empty($teacherEntry['id'])) {
|
||||
$allTeacherIds[] = (int) $teacherEntry['id'];
|
||||
}
|
||||
}
|
||||
$allClassSectionIds[] = $classSectionId;
|
||||
|
||||
$midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected);
|
||||
$midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected);
|
||||
$participationStatus = $this->submissionStatus(count($participationStudents), $expected);
|
||||
$ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected);
|
||||
$attendanceStatus = $this->attendanceStatus($attendanceSubmitted);
|
||||
|
||||
// CI original intentionally only counted score/comment statuses in summary (not attendance)
|
||||
$statusDetails = [
|
||||
'midterm_score_status' => $midtermScoreStatus,
|
||||
'midterm_comment_status' => $midtermCommentStatus,
|
||||
'participation_status' => $participationStatus,
|
||||
'ptap_comment_status' => $ptapCommentStatus,
|
||||
];
|
||||
|
||||
$missingItemsForSection = $this->buildMissingItems($statusDetails);
|
||||
$missingItemCount += count($missingItemsForSection);
|
||||
$totalStatuses += count($statusDetails);
|
||||
|
||||
$rows[] = [
|
||||
'class_section' => $section['class_section'] ?? "Section {$classSectionId}",
|
||||
'class_section_id' => $classSectionId,
|
||||
'teachers' => $teacherList,
|
||||
'midterm_score_status' => $midtermScoreStatus,
|
||||
'midterm_comment_status' => $midtermCommentStatus,
|
||||
'participation_status' => $participationStatus,
|
||||
'ptap_comment_status' => $ptapCommentStatus,
|
||||
'attendance_status' => $attendanceStatus,
|
||||
'missing_items' => $missingItemsForSection,
|
||||
'student_count' => $expected,
|
||||
];
|
||||
}
|
||||
|
||||
$historyMap = [];
|
||||
$teacherIds = array_values(array_unique($allTeacherIds));
|
||||
$classSectionIds = array_values(array_unique($allClassSectionIds));
|
||||
|
||||
if (!empty($teacherIds) && !empty($classSectionIds) && $this->hasTable('teacher_submission_notification_history')) {
|
||||
$historyRecords = DB::table('teacher_submission_notification_history as h')
|
||||
->select([
|
||||
'h.*',
|
||||
'u.firstname',
|
||||
'u.lastname',
|
||||
])
|
||||
->leftJoin('users as u', 'u.id', '=', 'h.admin_id')
|
||||
->where('h.notification_category', 'teacher_submissions')
|
||||
->whereIn('h.teacher_id', $teacherIds)
|
||||
->whereIn('h.class_section_id', $classSectionIds)
|
||||
->orderByDesc('h.sent_at')
|
||||
->get();
|
||||
|
||||
foreach ($historyRecords as $record) {
|
||||
$sectionId = (int) ($record->class_section_id ?? 0);
|
||||
$teacherId = (int) ($record->teacher_id ?? 0);
|
||||
if ($sectionId <= 0 || $teacherId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sentAt = $record->sent_at ?? null;
|
||||
$sentAtText = $sentAt ? Carbon::parse($sentAt)->format('M j, Y g:i A') : '';
|
||||
|
||||
$adminName = trim((string)($record->firstname ?? '') . ' ' . (string)($record->lastname ?? ''));
|
||||
if ($adminName === '') {
|
||||
$adminName = 'Administrator';
|
||||
}
|
||||
|
||||
$historyMap[$sectionId][$teacherId][] = [
|
||||
'sent_at_text' => $sentAtText,
|
||||
'admin_name' => $adminName,
|
||||
'status' => strtolower((string) ($record->status ?? 'sent')),
|
||||
];
|
||||
}
|
||||
|
||||
// keep latest 3 per section/teacher, same as CI
|
||||
foreach ($historyMap as &$teachersHistory) {
|
||||
foreach ($teachersHistory as &$entries) {
|
||||
$entries = array_slice($entries, 0, 3);
|
||||
}
|
||||
unset($entries);
|
||||
}
|
||||
unset($teachersHistory);
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'total_items' => $totalStatuses,
|
||||
'missing_items' => $missingItemCount,
|
||||
'submitted_items' => max(0, $totalStatuses - $missingItemCount),
|
||||
'submission_percentage' => $totalStatuses > 0
|
||||
? (int) round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100)
|
||||
: 100,
|
||||
];
|
||||
|
||||
return [
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'rows' => $rows,
|
||||
'notification_history' => $historyMap,
|
||||
'summary' => $summary,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* API payload format expected:
|
||||
* [
|
||||
* 'notify' => [sectionId => [teacherId => 1, ...], ...],
|
||||
* 'missing_items' => [sectionId => [teacherId => base64(json(items))]]
|
||||
* ]
|
||||
*/
|
||||
public function sendNotifications(int $adminId, array $payload): array
|
||||
{
|
||||
$notify = $payload['notify'] ?? null;
|
||||
if (!is_array($notify)) {
|
||||
return [
|
||||
'message' => 'Select at least one teacher to notify.',
|
||||
'sent' => 0,
|
||||
'failed' => 0,
|
||||
'results' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$missingItemsPayload = is_array($payload['missing_items'] ?? null) ? $payload['missing_items'] : [];
|
||||
|
||||
$targets = [];
|
||||
foreach ($notify as $sectionIdRaw => $teachers) {
|
||||
$sectionId = (int) $sectionIdRaw;
|
||||
if ($sectionId <= 0 || !is_array($teachers)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($teachers as $teacherIdRaw => $value) {
|
||||
$teacherId = (int) $teacherIdRaw;
|
||||
if ($teacherId <= 0 || $value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets["{$sectionId}_{$teacherId}"] = [
|
||||
'class_section_id' => $sectionId,
|
||||
'teacher_id' => $teacherId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($targets)) {
|
||||
return [
|
||||
'message' => 'Select at least one teacher to notify.',
|
||||
'sent' => 0,
|
||||
'failed' => 0,
|
||||
'results' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$targets = array_values($targets);
|
||||
|
||||
$teacherIds = array_values(array_unique(array_map(
|
||||
fn ($t) => (int) $t['teacher_id'],
|
||||
$targets
|
||||
)));
|
||||
$classSectionIds = array_values(array_unique(array_map(
|
||||
fn ($t) => (int) $t['class_section_id'],
|
||||
$targets
|
||||
)));
|
||||
|
||||
$classSectionMap = DB::table('classSection')
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->get()
|
||||
->mapWithKeys(fn ($row) => [(int)$row->class_section_id => (string)($row->class_section_name ?? '')])
|
||||
->all();
|
||||
|
||||
$teacherLookup = DB::table('users')
|
||||
->select('id', 'firstname', 'lastname', 'email')
|
||||
->whereIn('id', $teacherIds)
|
||||
->get()
|
||||
->keyBy(fn ($row) => (int) $row->id);
|
||||
|
||||
$adminUser = DB::table('users')->select('id', 'firstname', 'lastname')->where('id', $adminId)->first();
|
||||
$adminName = $adminUser
|
||||
? trim((string)($adminUser->firstname ?? '') . ' ' . (string)($adminUser->lastname ?? ''))
|
||||
: '';
|
||||
if ($adminName === '') {
|
||||
$adminName = 'Administrator';
|
||||
}
|
||||
|
||||
$cfg = $this->context->currentConfig();
|
||||
$scoreUrl = rtrim((string) config('app.url'), '/') . '/';
|
||||
|
||||
$sentCount = 0;
|
||||
$failCount = 0;
|
||||
$results = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$classSectionId = (int) $target['class_section_id'];
|
||||
$teacherId = (int) $target['teacher_id'];
|
||||
|
||||
$teacher = $teacherLookup->get($teacherId);
|
||||
$sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}";
|
||||
|
||||
$teacherName = $teacher
|
||||
? trim((string)($teacher->firstname ?? '') . ' ' . (string)($teacher->lastname ?? ''))
|
||||
: '';
|
||||
if ($teacherName === '') {
|
||||
$teacherName = 'Teacher';
|
||||
}
|
||||
|
||||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
||||
|
||||
$missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
|
||||
$missingItems = $this->parseMissingItemsPayload((string) $missingPayload);
|
||||
|
||||
if (!empty($missingItems)) {
|
||||
$missingText = e($this->formatMissingItemsText($missingItems));
|
||||
$missingNote = "<p>Outstanding items: {$missingText}.</p>";
|
||||
} else {
|
||||
$missingNote = "<p>Our records show no outstanding submissions for this section, but please verify if anything still needs attention.</p>";
|
||||
}
|
||||
|
||||
$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>Al Rahma Administration</p>";
|
||||
|
||||
$email = (string) ($teacher->email ?? '');
|
||||
$status = 'failed';
|
||||
$error = null;
|
||||
|
||||
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
try {
|
||||
Mail::html($body, function ($message) use ($email, $subject) {
|
||||
$message->to($email)->subject($subject);
|
||||
});
|
||||
$status = 'sent';
|
||||
} catch (Throwable $e) {
|
||||
$status = 'failed';
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = 'Missing or invalid email';
|
||||
}
|
||||
|
||||
if ($status === 'sent') {
|
||||
$sentCount++;
|
||||
} else {
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
if ($this->hasTable('teacher_submission_notification_history')) {
|
||||
DB::table('teacher_submission_notification_history')->insert([
|
||||
'teacher_id' => $teacherId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'admin_id' => $adminId,
|
||||
'notification_category' => 'teacher_submissions',
|
||||
'message' => $this->truncateNotificationMessage($body),
|
||||
'status' => $status,
|
||||
'school_year' => $cfg['school_year'] ?? null,
|
||||
'semester' => $cfg['semester'] ?? null,
|
||||
'sent_at' => Carbon::now('UTC'),
|
||||
]);
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'teacher_id' => $teacherId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'teacher_name' => $teacherName,
|
||||
'section_name' => $sectionName,
|
||||
'email' => $email,
|
||||
'status' => $status,
|
||||
'missing_items' => $missingItems,
|
||||
'error' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($sentCount > 0) {
|
||||
$parts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent';
|
||||
}
|
||||
if ($failCount > 0) {
|
||||
$parts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed';
|
||||
}
|
||||
|
||||
return [
|
||||
'message' => !empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.',
|
||||
'sent' => $sentCount,
|
||||
'failed' => $failCount,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
private function submissionStatus(int $filled, int $expected): array
|
||||
{
|
||||
if ($expected <= 0) {
|
||||
return [
|
||||
'label' => 'No students',
|
||||
'badge' => 'bg-secondary',
|
||||
'detail' => '',
|
||||
'completed' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$completed = $filled >= $expected;
|
||||
|
||||
return [
|
||||
'label' => $completed ? 'Submitted' : 'Missing',
|
||||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||||
'detail' => "{$filled}/{$expected}",
|
||||
'completed' => $completed,
|
||||
];
|
||||
}
|
||||
|
||||
private function attendanceStatus(bool $submitted): array
|
||||
{
|
||||
return [
|
||||
'label' => $submitted ? 'Submitted' : 'Missing',
|
||||
'badge' => $submitted ? 'bg-success' : 'bg-danger',
|
||||
'completed' => $submitted,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildMissingItems(array $statusMap): array
|
||||
{
|
||||
$labels = [
|
||||
'midterm_score_status' => 'midterm scores',
|
||||
'midterm_comment_status' => 'midterm comments',
|
||||
'participation_status' => 'participation',
|
||||
'ptap_comment_status' => 'PTAP comments',
|
||||
'attendance_status' => 'attendance',
|
||||
];
|
||||
|
||||
$items = [];
|
||||
foreach ($statusMap as $key => $status) {
|
||||
$completed = (bool) ($status['completed'] ?? true);
|
||||
if (!$completed && isset($labels[$key])) {
|
||||
$items[] = $labels[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($items);
|
||||
}
|
||||
|
||||
private function formatMissingItemsText(array $items): string
|
||||
{
|
||||
$items = array_values(array_unique(array_filter(array_map(
|
||||
fn ($v) => trim((string)$v),
|
||||
$items
|
||||
))));
|
||||
|
||||
$count = count($items);
|
||||
if ($count === 0) return '';
|
||||
if ($count === 1) return $items[0];
|
||||
if ($count === 2) return $items[0] . ' and ' . $items[1];
|
||||
|
||||
$last = array_pop($items);
|
||||
return implode(', ', $items) . ' and ' . $last;
|
||||
}
|
||||
|
||||
private function teacherRolePriority(string $roleKey): int
|
||||
{
|
||||
return match (strtolower($roleKey)) {
|
||||
'main' => 1,
|
||||
'ta' => 2,
|
||||
default => 3,
|
||||
};
|
||||
}
|
||||
|
||||
private function truncateNotificationMessage(string $html, int $limit = 1000): string
|
||||
{
|
||||
$text = trim(strip_tags($html));
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strlen($text) <= $limit
|
||||
? $text
|
||||
: mb_substr($text, 0, $limit) . '…';
|
||||
}
|
||||
|
||||
private function parseMissingItemsPayload(string $payload): array
|
||||
{
|
||||
if ($payload === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decodedRaw = base64_decode($payload, true);
|
||||
if ($decodedRaw === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($decodedRaw, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($decoded as $item) {
|
||||
$item = trim((string) $item);
|
||||
if ($item !== '') {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($items));
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
try {
|
||||
return DB::getSchemaBuilder()->hasTable($table);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user