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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AssignmentService
|
||||
{
|
||||
public function __construct(
|
||||
protected User $userModel,
|
||||
protected Configuration $configModel,
|
||||
protected Student $studentModel,
|
||||
protected ClassSection $classSectionModel,
|
||||
protected TeacherClass $teacherClassModel,
|
||||
protected StudentClass $studentClassModel
|
||||
) {}
|
||||
|
||||
public function buildClassAssignments(array $filters = []): array
|
||||
{
|
||||
$currentSemester = (string) ($this->getConfigValue('semester') ?? '');
|
||||
$currentSchoolYear = (string) ($this->getConfigValue('school_year') ?? '');
|
||||
|
||||
$selectedSemester = (string) ($filters['semester'] ?? $currentSemester);
|
||||
$selectedYear = (string) ($filters['school_year'] ?? $currentSchoolYear);
|
||||
$forApi = (bool) ($filters['for_api'] ?? false);
|
||||
|
||||
// Teacher classes (year-filtered; no semester filtering like your original code)
|
||||
$teacherQ = $this->teacherClassModel->newQuery();
|
||||
if ($selectedYear !== '') {
|
||||
$teacherQ->where('school_year', $selectedYear);
|
||||
}
|
||||
$teacherClassesAll = $teacherQ->get()->toArray();
|
||||
|
||||
// Student classes (active + year-filtered)
|
||||
$studentQ = method_exists($this->studentClassModel, 'active')
|
||||
? $this->studentClassModel->active()
|
||||
: $this->studentClassModel->newQuery()->where('is_active', 1);
|
||||
|
||||
if ($selectedYear !== '') {
|
||||
// If your table is aliased in scopeActive(), you can keep student_class.school_year
|
||||
$studentQ->where('school_year', $selectedYear);
|
||||
}
|
||||
$studentClassesAll = $studentQ->get()->toArray();
|
||||
|
||||
// Group teacher and student classes by section
|
||||
$teacherBySection = [];
|
||||
foreach ($teacherClassesAll as $tc) {
|
||||
$secId = (int) ($tc['class_section_id'] ?? 0);
|
||||
if ($secId > 0) {
|
||||
$teacherBySection[$secId][] = $tc;
|
||||
}
|
||||
}
|
||||
|
||||
$studentsBySection = [];
|
||||
foreach ($studentClassesAll as $sc) {
|
||||
$secId = (int) ($sc['class_section_id'] ?? 0);
|
||||
if ($secId > 0) {
|
||||
$studentsBySection[$secId][] = $sc;
|
||||
}
|
||||
}
|
||||
|
||||
$allSectionIds = array_values(array_unique(array_merge(
|
||||
array_keys($teacherBySection),
|
||||
array_keys($studentsBySection)
|
||||
)));
|
||||
|
||||
$classSections = [];
|
||||
|
||||
foreach ($allSectionIds as $classSectionId) {
|
||||
$teacherClasses = $teacherBySection[$classSectionId] ?? [];
|
||||
$studentClasses = $studentsBySection[$classSectionId] ?? [];
|
||||
|
||||
if (empty($teacherClasses) && empty($studentClasses)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classSectionName = $this->resolveClassSectionName($classSectionId);
|
||||
|
||||
$mainTeachers = [];
|
||||
$teacherAssistants = [];
|
||||
$sectionSemester = '';
|
||||
$sectionSchoolYear = '';
|
||||
$description = '';
|
||||
|
||||
// Teacher metadata + names
|
||||
foreach ($teacherClasses as $teacherClass) {
|
||||
$teacherId = (int) ($teacherClass['teacher_id'] ?? 0);
|
||||
if ($teacherId > 0) {
|
||||
$teacher = $this->userModel->newQuery()->find($teacherId);
|
||||
if ($teacher) {
|
||||
$teacherName = trim(
|
||||
(string) ($teacher->firstname ?? '') . ' ' . (string) ($teacher->lastname ?? '')
|
||||
);
|
||||
|
||||
if ($teacherName !== '') {
|
||||
if (($teacherClass['position'] ?? '') === 'main') {
|
||||
$mainTeachers[] = $teacherName;
|
||||
} elseif (($teacherClass['position'] ?? '') === 'ta') {
|
||||
$teacherAssistants[] = $teacherName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sectionSemester === '' && !empty($teacherClass['semester'])) {
|
||||
$sectionSemester = (string) $teacherClass['semester'];
|
||||
}
|
||||
if ($sectionSchoolYear === '' && !empty($teacherClass['school_year'])) {
|
||||
$sectionSchoolYear = (string) $teacherClass['school_year'];
|
||||
}
|
||||
if ($description === '' && !empty($teacherClass['description'])) {
|
||||
$description = (string) $teacherClass['description'];
|
||||
}
|
||||
}
|
||||
|
||||
// Students
|
||||
$students = [];
|
||||
foreach ($studentClasses as $studentClass) {
|
||||
if ($sectionSemester === '' && !empty($studentClass['semester'])) {
|
||||
$sectionSemester = (string) $studentClass['semester'];
|
||||
}
|
||||
if ($sectionSchoolYear === '' && !empty($studentClass['school_year'])) {
|
||||
$sectionSchoolYear = (string) $studentClass['school_year'];
|
||||
}
|
||||
if ($description === '' && !empty($studentClass['description'])) {
|
||||
$description = (string) $studentClass['description'];
|
||||
}
|
||||
|
||||
$studentId = (int) ($studentClass['student_id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$student = $this->studentModel->newQuery()
|
||||
->where('id', $studentId)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$student) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$students[] = $forApi
|
||||
? [
|
||||
'id' => (int) $student->id,
|
||||
'firstname' => (string) ($student->firstname ?? ''),
|
||||
'lastname' => (string) ($student->lastname ?? ''),
|
||||
'age' => $student->age ?? null,
|
||||
'gender' => (string) ($student->gender ?? ''),
|
||||
'registration_grade' => (string) ($student->registration_grade ?? ''),
|
||||
'photo_consent' => (bool) ($student->photo_consent ?? false),
|
||||
'tuition_paid' => (bool) ($student->tuition_paid ?? false),
|
||||
'school_id' => (string) ($student->school_id ?? ''),
|
||||
]
|
||||
: [
|
||||
'id' => (int) $student->id,
|
||||
'firstname' => e((string) ($student->firstname ?? '')),
|
||||
'lastname' => e((string) ($student->lastname ?? '')),
|
||||
'age' => e((string) ($student->age ?? '')),
|
||||
'gender' => e((string) ($student->gender ?? '')),
|
||||
'registration_grade' => e((string) ($student->registration_grade ?? '')),
|
||||
'photo_consent' => e(((bool) ($student->photo_consent ?? false)) ? 'Yes' : 'No'),
|
||||
'tuition_paid' => e(((bool) ($student->tuition_paid ?? false)) ? 'Yes' : 'No'),
|
||||
'school_id' => e((string) ($student->school_id ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
$classSections[] = [
|
||||
'class_section_id' => (int) $classSectionId,
|
||||
'class_section_name' => (string) $classSectionName,
|
||||
'main_teachers' => array_values(array_unique($mainTeachers)),
|
||||
'teacher_assistants' => array_values(array_unique($teacherAssistants)),
|
||||
'students' => $students,
|
||||
'semester' => $sectionSemester !== '' ? $sectionSemester : $currentSemester,
|
||||
'school_year' => $sectionSchoolYear !== '' ? $sectionSchoolYear : $currentSchoolYear,
|
||||
'description' => $description,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort sections by name
|
||||
usort($classSections, fn ($a, $b) => strcmp(
|
||||
(string) ($a['class_section_name'] ?? ''),
|
||||
(string) ($b['class_section_name'] ?? '')
|
||||
));
|
||||
|
||||
$schoolYearsList = $this->getSchoolYearsList($currentSchoolYear);
|
||||
|
||||
$payload = [
|
||||
'classSections' => $classSections,
|
||||
'schoolYears' => $schoolYearsList,
|
||||
'selectedYear' => $selectedYear,
|
||||
'selectedSemester' => $selectedSemester,
|
||||
'semester' => $currentSemester,
|
||||
'school_year' => $currentSchoolYear,
|
||||
];
|
||||
|
||||
// If you're using Laravel Sanctum/API only, CSRF may not be needed.
|
||||
// Keep it if your frontend still expects it:
|
||||
if (function_exists('csrf_token')) {
|
||||
$payload['csrfToken'] = csrf_token();
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
public function saveStudentAssignment(array $data, ?int $updatedBy = null): mixed
|
||||
{
|
||||
$payload = [
|
||||
'student_id' => $data['student_id'],
|
||||
'class_section_id' => $data['class_section_id'],
|
||||
'semester' => $data['semester'] ?? $this->getConfigValue('semester'),
|
||||
'school_year' => $data['school_year'] ?? $this->getConfigValue('school_year'),
|
||||
'description' => $data['description'] ?? null,
|
||||
'updated_by' => $updatedBy,
|
||||
];
|
||||
|
||||
// If your StudentClass model supports updateOrCreate:
|
||||
// adjust unique keys as needed (e.g. student_id + school_year + semester)
|
||||
return $this->studentClassModel->newQuery()->updateOrCreate(
|
||||
[
|
||||
'student_id' => $payload['student_id'],
|
||||
'school_year' => $payload['school_year'],
|
||||
'semester' => $payload['semester'],
|
||||
],
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
protected function getConfigValue(string $key): mixed
|
||||
{
|
||||
// Match your existing model API if present
|
||||
if (method_exists($this->configModel, 'getConfig')) {
|
||||
return $this->configModel->getConfig($key);
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return $this->configModel->newQuery()
|
||||
->where('key', $key)
|
||||
->value('value');
|
||||
}
|
||||
|
||||
protected function resolveClassSectionName(int $classSectionId): string
|
||||
{
|
||||
if (method_exists($this->classSectionModel, 'getClassSectionNameBySectionId')) {
|
||||
return (string) ($this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? '');
|
||||
}
|
||||
|
||||
$section = $this->classSectionModel->newQuery()->find($classSectionId);
|
||||
|
||||
// Adjust to your actual field(s)
|
||||
return (string) ($section->name ?? $section->title ?? '');
|
||||
}
|
||||
|
||||
protected function getSchoolYearsList(string $fallbackYear = ''): array
|
||||
{
|
||||
try {
|
||||
$years = DB::table('teacher_class')
|
||||
->select('school_year')
|
||||
->whereNotNull('school_year')
|
||||
->distinct()
|
||||
->orderByDesc('school_year')
|
||||
->pluck('school_year')
|
||||
->map(fn ($y) => (string) $y)
|
||||
->filter(fn ($y) => $y !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (!empty($years)) {
|
||||
return $years;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// fallback below
|
||||
}
|
||||
|
||||
return $fallbackYear !== '' ? [$fallbackYear] : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AttendanceCommentTemplate; // <-- change if your model class name differs
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceCommentTemplateService
|
||||
{
|
||||
public function list(bool $activeOnly = false): array
|
||||
{
|
||||
$query = AttendanceCommentTemplate::query()->orderBy('min_score', 'asc');
|
||||
|
||||
if ($activeOnly) {
|
||||
$query->where('is_active', 1);
|
||||
}
|
||||
|
||||
return $query->get()
|
||||
->map(fn ($row) => $this->transform($row))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function findOrFail(int $id): array
|
||||
{
|
||||
$template = AttendanceCommentTemplate::query()->find($id);
|
||||
|
||||
if (!$template) {
|
||||
throw new ModelNotFoundException("Attendance comment template not found.");
|
||||
}
|
||||
|
||||
return $this->transform($template);
|
||||
}
|
||||
|
||||
public function create(array $data): array
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$this->ensureNoOverlap(
|
||||
(int) $data['min_score'],
|
||||
(int) $data['max_score'],
|
||||
null,
|
||||
!empty($data['is_active'])
|
||||
);
|
||||
|
||||
$template = AttendanceCommentTemplate::query()->create([
|
||||
'min_score' => (int) $data['min_score'],
|
||||
'max_score' => (int) $data['max_score'],
|
||||
'template_text' => (string) $data['template_text'],
|
||||
'is_active' => (bool) ($data['is_active'] ?? true),
|
||||
]);
|
||||
|
||||
return $this->transform($template->fresh());
|
||||
});
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): array
|
||||
{
|
||||
return DB::transaction(function () use ($id, $data) {
|
||||
$template = AttendanceCommentTemplate::query()->find($id);
|
||||
|
||||
if (!$template) {
|
||||
throw new ModelNotFoundException("Attendance comment template not found.");
|
||||
}
|
||||
|
||||
$newMin = array_key_exists('min_score', $data) ? (int) $data['min_score'] : (int) $template->min_score;
|
||||
$newMax = array_key_exists('max_score', $data) ? (int) $data['max_score'] : (int) $template->max_score;
|
||||
$newIsActive = array_key_exists('is_active', $data) ? (bool) $data['is_active'] : (bool) $template->is_active;
|
||||
|
||||
$this->ensureNoOverlap($newMin, $newMax, $id, $newIsActive);
|
||||
|
||||
$template->fill([
|
||||
'min_score' => $newMin,
|
||||
'max_score' => $newMax,
|
||||
'template_text' => array_key_exists('template_text', $data)
|
||||
? (string) $data['template_text']
|
||||
: (string) $template->template_text,
|
||||
'is_active' => $newIsActive,
|
||||
]);
|
||||
|
||||
$template->save();
|
||||
|
||||
return $this->transform($template->fresh());
|
||||
});
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$template = AttendanceCommentTemplate::query()->find($id);
|
||||
|
||||
if (!$template) {
|
||||
throw new ModelNotFoundException("Attendance comment template not found.");
|
||||
}
|
||||
|
||||
$template->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent overlapping active score ranges.
|
||||
*/
|
||||
private function ensureNoOverlap(int $min, int $max, ?int $ignoreId = null, bool $isActive = true): void
|
||||
{
|
||||
if (!$isActive) {
|
||||
return; // allow inactive templates to overlap
|
||||
}
|
||||
|
||||
$query = AttendanceCommentTemplate::query()
|
||||
->where('is_active', 1)
|
||||
->where(function ($q) use ($min, $max) {
|
||||
$q->whereBetween('min_score', [$min, $max])
|
||||
->orWhereBetween('max_score', [$min, $max])
|
||||
->orWhere(function ($q2) use ($min, $max) {
|
||||
$q2->where('min_score', '<=', $min)
|
||||
->where('max_score', '>=', $max);
|
||||
});
|
||||
});
|
||||
|
||||
if ($ignoreId !== null) {
|
||||
$query->where('id', '!=', $ignoreId);
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
abort(response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Score range overlaps with an existing active template.',
|
||||
], 422));
|
||||
}
|
||||
}
|
||||
|
||||
private function transform($row): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($row->id ?? 0),
|
||||
'min_score' => (int) ($row->min_score ?? 0),
|
||||
'max_score' => (int) ($row->max_score ?? 0),
|
||||
'template_text' => (string) ($row->template_text ?? ''),
|
||||
'is_active' => (bool) ($row->is_active ?? false),
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EmailService
|
||||
{
|
||||
protected array $from = [];
|
||||
protected array $to = [];
|
||||
protected string $subject = '';
|
||||
protected string $message = '';
|
||||
|
||||
public function setFrom(string $email, string $name = ''): self
|
||||
{
|
||||
$this->from = ['email' => $email, 'name' => $name];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTo(string|array $email): self
|
||||
{
|
||||
$this->to = is_array($email) ? $email : [$email];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSubject(string $subject): self
|
||||
{
|
||||
$this->subject = $subject;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function send(string $to = '', string $subject = '', string $message = '', ?string $template = null): bool
|
||||
{
|
||||
if (!empty($to)) {
|
||||
$this->setTo($to);
|
||||
}
|
||||
if ($subject !== '') {
|
||||
$this->setSubject($subject);
|
||||
}
|
||||
if ($message !== '') {
|
||||
$this->setMessage($message);
|
||||
}
|
||||
|
||||
if (empty($this->to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Mail::html($this->message, function ($mail) use ($template) {
|
||||
$mail->to($this->to);
|
||||
if (!empty($this->from)) {
|
||||
$mail->from($this->from['email'], $this->from['name']);
|
||||
}
|
||||
$mail->subject($this->subject);
|
||||
});
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('EmailService send failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class FeeCalculationService
|
||||
{
|
||||
public function calculateRefund(array $students, int $parentId): float
|
||||
{
|
||||
// TODO: Replace with actual refund calculation logic.
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class NavbarService
|
||||
{
|
||||
/**
|
||||
* Build a hierarchical tree of nav items grouped by parent_id.
|
||||
*
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function buildTree(array $items): array
|
||||
{
|
||||
$normalized = array_map([$this, 'normalizeItem'], $items);
|
||||
$grouped = [];
|
||||
|
||||
foreach ($normalized as $item) {
|
||||
$parentId = $this->castParentId($item['parent_id'] ?? null);
|
||||
$grouped[$parentId][] = $item;
|
||||
}
|
||||
|
||||
$buildLevel = function ($parentId) use (&$grouped, &$buildLevel) {
|
||||
if (!isset($grouped[$parentId])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$children = $grouped[$parentId];
|
||||
usort($children, fn ($a, $b) => ($a['order'] ?? 0) <=> ($b['order'] ?? 0));
|
||||
|
||||
return array_map(function ($child) use ($buildLevel) {
|
||||
$child['children'] = $buildLevel($child['id']);
|
||||
return $child;
|
||||
}, $children);
|
||||
};
|
||||
|
||||
return $buildLevel(null);
|
||||
}
|
||||
|
||||
private function normalizeItem($item): array
|
||||
{
|
||||
if (is_object($item)) {
|
||||
$item = (array) $item;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($item['id'] ?? 0),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'url' => $item['url'] ?? '',
|
||||
'icon' => $item['icon'] ?? $item['icon_class'] ?? null,
|
||||
'parent_id' => $item['parent_id'] ?? $item['menu_parent_id'] ?? null,
|
||||
'order' => (int) ($item['order'] ?? $item['sort_order'] ?? 0),
|
||||
'is_enabled'=> (bool) ($item['is_enabled'] ?? true),
|
||||
];
|
||||
}
|
||||
|
||||
private function castParentId($value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear navigation cache (placeholder for future caching implementation)
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
// TODO: Implement cache clearing if caching is added
|
||||
// For now, this is a no-op to maintain compatibility
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class PhoneFormatterService
|
||||
{
|
||||
/**
|
||||
* Normalize a phone number by stripping non-digits and formatting US numbers.
|
||||
*/
|
||||
public function formatPhoneNumber(string $phone): string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $phone);
|
||||
if ($digits === null || $digits === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (strlen($digits) === 10) {
|
||||
return sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a phone string contains any digits.
|
||||
*/
|
||||
public function hasDigits(string $phone): bool
|
||||
{
|
||||
return preg_match('/\d/', $phone) === 1;
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SchoolIdService
|
||||
{
|
||||
public function generate(): string
|
||||
{
|
||||
return strtoupper('SID-' . Str::random(8));
|
||||
}
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SemesterScoreService
|
||||
{
|
||||
public function updateStudentScores(array $data, string $semester, string $schoolYear): void
|
||||
{
|
||||
Log::info('SemesterScoreService placeholder update', [
|
||||
'student_id' => $data['student_id'] ?? null,
|
||||
'class_section_id' => $data['class_section_id'] ?? null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
// TODO: Implement actual score recalculation logic.
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scores for multiple students
|
||||
* @param array $studentInfo Array of student info arrays, each containing at least student_id and class_section_id
|
||||
*/
|
||||
public function updateScoresForStudents(array $studentInfo): void
|
||||
{
|
||||
if (empty($studentInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($studentInfo as $student) {
|
||||
if (!isset($student['student_id']) || !isset($student['class_section_id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->updateStudentScores($student, $student['semester'] ?? '', $student['school_year'] ?? '');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to update scores for student', [
|
||||
'student_id' => $student['student_id'] ?? null,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user