add all controllers logic
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\ContactUs;
|
||||
use App\Models\Configuration;
|
||||
use App\Services\Email\EmailDispatchService;
|
||||
use App\Services\System\GlobalConfigService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ContactSubmissionService
|
||||
{
|
||||
public function __construct(
|
||||
private EmailDispatchService $emailService,
|
||||
private GlobalConfigService $configService
|
||||
) {
|
||||
}
|
||||
|
||||
public function submit(array $payload): array
|
||||
{
|
||||
$email = strtolower((string) $payload['email']);
|
||||
$message = (string) $payload['message'];
|
||||
$subject = (string) ($payload['subject'] ?? 'Contact Us Form Submission');
|
||||
|
||||
$schoolYear = $payload['school_year'] ?? $this->configService->getSchoolYear() ?? '';
|
||||
$semester = $payload['semester'] ?? $this->configService->getSemester() ?? '';
|
||||
|
||||
$record = null;
|
||||
try {
|
||||
$record = ContactUs::query()->create([
|
||||
'sender_id' => (int) ($payload['sender_id'] ?? 0),
|
||||
'reciever_id' => (int) ($payload['receiver_id'] ?? 0),
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'semester' => $semester ?: 'Fall',
|
||||
'school_year' => $schoolYear ?: '2025-2026',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Contact submission save failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$recipient = Configuration::getConfig('administrator_email')
|
||||
?: (string) config('mail.from.address', '')
|
||||
?: 'alrahma.isgl@gmail.com';
|
||||
|
||||
$htmlMessage = nl2br('You have received a new message from ' . $email . ":\n\n" . $message);
|
||||
|
||||
$emailSent = false;
|
||||
if (!app()->runningUnitTests()) {
|
||||
$emailSent = $this->emailService->send($recipient, $subject, $htmlMessage, null, $email, $email);
|
||||
if (!$emailSent) {
|
||||
Log::error('Contact submission email failed for ' . $email);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'record' => $record,
|
||||
'email_sent' => $emailSent,
|
||||
'recipient' => $recipient,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
class FrontendPageService
|
||||
{
|
||||
public function page(string $name): array
|
||||
{
|
||||
return [
|
||||
'page' => $name,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\Configuration;
|
||||
|
||||
class LandingPageContextService
|
||||
{
|
||||
public function context(): array
|
||||
{
|
||||
return [
|
||||
'school_year' => (string) (Configuration::getConfig('school_year') ?? ''),
|
||||
'semester' => (string) (Configuration::getConfig('semester') ?? ''),
|
||||
'last_day_of_registration' => (string) (Configuration::getConfig('enrollment_deadline') ?? 'Not set'),
|
||||
'refund_deadline' => (string) (Configuration::getConfig('refund_deadline') ?? 'Not set'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\StudentClass;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LandingPageParentDashboardService
|
||||
{
|
||||
public function __construct(private LandingPageContextService $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function summary(int $parentUserId, string $userType): array
|
||||
{
|
||||
$context = $this->context->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$semester = (string) ($context['semester'] ?? '');
|
||||
|
||||
$parentId = $this->resolveParentId($parentUserId, $userType);
|
||||
if (!$parentId) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Unable to retrieve student data. Please contact support.',
|
||||
];
|
||||
}
|
||||
|
||||
$notifications = $this->notificationsForParent($parentId);
|
||||
|
||||
$students = DB::table('students')
|
||||
->where('parent_id', $parentId)
|
||||
->get()
|
||||
->map(function ($row) use ($schoolYear) {
|
||||
$student = (array) $row;
|
||||
$classSection = StudentClass::getClassSectionsByStudentId($student['id'], $schoolYear);
|
||||
$student['class_section'] = $classSection;
|
||||
$student['grade'] = $classSection;
|
||||
return $student;
|
||||
})
|
||||
->all();
|
||||
|
||||
$attendance = DB::table('attendance_data')
|
||||
->select('attendance_data.*', 'students.firstname', 'students.lastname')
|
||||
->join('students', 'students.id', '=', 'attendance_data.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
->where('attendance_data.school_year', $schoolYear)
|
||||
->where('attendance_data.semester', $semester)
|
||||
->orderBy('attendance_data.date', 'DESC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$grades = DB::table('final_score')
|
||||
->select('final_score.*', 'students.firstname', 'students.lastname')
|
||||
->join('students', 'students.id', '=', 'final_score.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
->where('final_score.school_year', $schoolYear)
|
||||
->where('final_score.semester', $semester)
|
||||
->orderBy('final_score.created_at', 'DESC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$enrollments = DB::table('enrollments')
|
||||
->select('enrollments.*', 'classes.class_name')
|
||||
->join('students', 'students.id', '=', 'enrollments.student_id')
|
||||
->join('classes', 'classes.id', '=', 'enrollments.class_section_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
->where('enrollments.school_year', $schoolYear)
|
||||
->where('enrollments.semester', $semester)
|
||||
->orderBy('enrollments.enrollment_date', 'DESC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$paymentBalance = Invoice::getLatestInvoiceTotalAmount($parentId);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'notifications' => $notifications,
|
||||
'students' => $students,
|
||||
'attendance' => $attendance,
|
||||
'grades' => $grades,
|
||||
'enrollments' => $enrollments,
|
||||
'last_day_of_registration' => $context['last_day_of_registration'] ?? 'Not set',
|
||||
'withdrawal_deadline' => $context['refund_deadline'] ?? 'Not set',
|
||||
'payment_balance' => $paymentBalance,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveParentId(int $userId, string $userType): ?int
|
||||
{
|
||||
if ($userType === 'primary') {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if ($userType === 'secondary') {
|
||||
$row = DB::table('parents')
|
||||
->select('parent_id')
|
||||
->where('secondparent_user_id', $userId)
|
||||
->first();
|
||||
return $row ? (int) $row->parent_id : null;
|
||||
}
|
||||
|
||||
if ($userType === 'tertiary') {
|
||||
$row = DB::table('authorized_users')
|
||||
->select('user_id as parent_id')
|
||||
->where('authorized_user_id', $userId)
|
||||
->first();
|
||||
return $row ? (int) $row->parent_id : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function notificationsForParent(int $parentId): array
|
||||
{
|
||||
return DB::table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
'user_notifications.user_id',
|
||||
DB::raw("CASE WHEN user_notifications.user_id IS NOT NULL THEN 'personal' ELSE 'broadcast' END as notification_type"),
|
||||
])
|
||||
->leftJoin('user_notifications', function ($join) use ($parentId) {
|
||||
$join->on('user_notifications.notification_id', '=', 'notifications.id')
|
||||
->where('user_notifications.user_id', '=', $parentId);
|
||||
})
|
||||
->where(function ($q) use ($parentId) {
|
||||
$q->where('notifications.target_group', 'parent')
|
||||
->orWhere('user_notifications.user_id', $parentId);
|
||||
})
|
||||
->whereNull('notifications.deleted_at')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('notifications.expires_at')
|
||||
->orWhere('notifications.expires_at', '>', now());
|
||||
})
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LandingPageRoleService
|
||||
{
|
||||
public function resolveRole(?User $user): string
|
||||
{
|
||||
if (!$user) {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
$roles = DB::table('user_roles as ur')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->where('ur.user_id', $user->id)
|
||||
->whereNull('ur.deleted_at')
|
||||
->orderBy('r.priority', 'asc')
|
||||
->pluck('r.name')
|
||||
->map(fn ($r) => strtolower((string) $r))
|
||||
->all();
|
||||
|
||||
if (!empty($roles)) {
|
||||
return $roles[0];
|
||||
}
|
||||
|
||||
return 'guest';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class LandingPageService
|
||||
{
|
||||
public function __construct(
|
||||
private LandingPageRoleService $roleService,
|
||||
private LandingPageContextService $contextService,
|
||||
private LandingPageTeacherSummaryService $teacherSummaryService,
|
||||
private LandingPageParentDashboardService $parentDashboardService
|
||||
) {
|
||||
}
|
||||
|
||||
public function dashboardForUser(?User $user, ?int $classSectionId = null): array
|
||||
{
|
||||
$role = $this->roleService->resolveRole($user);
|
||||
|
||||
return match ($role) {
|
||||
'administrator' => $this->administrator(),
|
||||
'admin' => $this->admin(),
|
||||
'teacher', 'teacher_assistant' => $this->teacher($user, $classSectionId),
|
||||
'student' => $this->student(),
|
||||
'parent', 'authorized_user' => $this->parent($user),
|
||||
default => $this->guest(),
|
||||
};
|
||||
}
|
||||
|
||||
public function administrator(): array
|
||||
{
|
||||
return [
|
||||
'role' => 'administrator',
|
||||
'dashboard' => 'administrator',
|
||||
];
|
||||
}
|
||||
|
||||
public function admin(): array
|
||||
{
|
||||
return [
|
||||
'role' => 'admin',
|
||||
'dashboard' => 'admin',
|
||||
];
|
||||
}
|
||||
|
||||
public function teacher(?User $user, ?int $classSectionId = null): array
|
||||
{
|
||||
if (!$user) {
|
||||
return ['role' => 'teacher', 'dashboard' => 'teacher', 'summary' => []];
|
||||
}
|
||||
|
||||
return [
|
||||
'role' => 'teacher',
|
||||
'dashboard' => 'teacher',
|
||||
'summary' => $this->teacherSummaryService->summary($user->id, $classSectionId),
|
||||
];
|
||||
}
|
||||
|
||||
public function student(): array
|
||||
{
|
||||
return [
|
||||
'role' => 'student',
|
||||
'dashboard' => 'student',
|
||||
];
|
||||
}
|
||||
|
||||
public function parent(?User $user): array
|
||||
{
|
||||
if (!$user) {
|
||||
return ['role' => 'parent', 'dashboard' => 'parent', 'summary' => []];
|
||||
}
|
||||
|
||||
$userType = (string) ($user->user_type ?? 'primary');
|
||||
|
||||
return [
|
||||
'role' => 'parent',
|
||||
'dashboard' => 'parent',
|
||||
'summary' => $this->parentDashboardService->summary($user->id, $userType),
|
||||
];
|
||||
}
|
||||
|
||||
public function guest(): array
|
||||
{
|
||||
return [
|
||||
'role' => 'guest',
|
||||
'dashboard' => 'guest',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Models\Calendar;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LandingPageTeacherSummaryService
|
||||
{
|
||||
public function __construct(private LandingPageContextService $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function summary(int $teacherId, ?int $requestedClassSectionId = null): array
|
||||
{
|
||||
$context = $this->context->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$semester = (string) ($context['semester'] ?? '');
|
||||
|
||||
$assignments = TeacherClass::getClassAssignmentsByUserId($teacherId, $schoolYear, $semester);
|
||||
$availableIds = array_values(array_filter(array_unique(array_map(
|
||||
static fn (array $row) => (int) ($row['class_section_id'] ?? 0),
|
||||
$assignments
|
||||
))));
|
||||
|
||||
$activeId = null;
|
||||
if ($requestedClassSectionId && in_array($requestedClassSectionId, $availableIds, true)) {
|
||||
$activeId = $requestedClassSectionId;
|
||||
} elseif (!empty($availableIds)) {
|
||||
$activeId = $availableIds[0];
|
||||
}
|
||||
|
||||
$activeName = null;
|
||||
foreach ($assignments as $row) {
|
||||
if ((int) ($row['class_section_id'] ?? 0) === (int) $activeId) {
|
||||
$activeName = $row['class_section_name'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedSemester = ucfirst(strtolower(trim((string) $semester)));
|
||||
if (!in_array($normalizedSemester, ['Fall', 'Spring'], true)) {
|
||||
$normalizedSemester = 'Fall';
|
||||
}
|
||||
|
||||
$classSectionIds = $availableIds;
|
||||
$dashboardSummary = $this->buildTeacherDashboardSummary($classSectionIds, $normalizedSemester, $schoolYear);
|
||||
|
||||
return [
|
||||
'class_section_id' => $activeId,
|
||||
'active_class_name' => $activeName,
|
||||
'classes' => $assignments,
|
||||
'jobCount' => count($assignments),
|
||||
'scoreSummary' => $dashboardSummary['scoreSummary'],
|
||||
'commentSummary' => $dashboardSummary['commentSummary'],
|
||||
'attendanceSummary' => $dashboardSummary['attendanceSummary'],
|
||||
'deadlineEvents' => $dashboardSummary['deadlineEvents'],
|
||||
'participationSummary' => $dashboardSummary['participationSummary'],
|
||||
'attendanceStatus' => $dashboardSummary['attendanceStatus'],
|
||||
'dashboardNotifications' => $dashboardSummary['notifications'],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildTeacherDashboardSummary(array $classSectionIds, string $semester, string $schoolYear): array
|
||||
{
|
||||
$scoreField = $semester === 'Spring' ? 'final_exam_score' : 'midterm_exam_score';
|
||||
$scoreLabel = $semester === 'Spring' ? 'Final Exam' : 'Midterm';
|
||||
|
||||
$summary = [
|
||||
'scoreSummary' => [
|
||||
'completionPct' => 0,
|
||||
'missing' => 0,
|
||||
'expected' => 0,
|
||||
'filled' => 0,
|
||||
'fieldLabel' => $scoreLabel,
|
||||
],
|
||||
'commentSummary' => [
|
||||
'total' => 0,
|
||||
'pendingReview' => 0,
|
||||
'reviewed' => 0,
|
||||
'byType' => [],
|
||||
],
|
||||
'attendanceSummary' => [
|
||||
'recorded' => 0,
|
||||
'students' => 0,
|
||||
'completionPct' => 0,
|
||||
'avgAbsences' => 0,
|
||||
],
|
||||
'participationSummary' => [
|
||||
'filled' => 0,
|
||||
'missing' => 0,
|
||||
'completionPct' => 0,
|
||||
'expected' => 0,
|
||||
],
|
||||
'deadlineEvents' => [],
|
||||
'notifications' => [],
|
||||
'attendanceStatus' => [
|
||||
'date' => '',
|
||||
'missingSections' => [],
|
||||
'missingNames' => [],
|
||||
'submitted' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$uniqueStudentIds = [];
|
||||
if (!empty($classSectionIds)) {
|
||||
$studentRows = DB::table('student_class as sc')
|
||||
->select('sc.student_id', 'sc.class_section_id')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->whereIn('sc.class_section_id', $classSectionIds)
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
foreach ($studentRows as $row) {
|
||||
$studentId = (int) ($row->student_id ?? 0);
|
||||
if ($studentId > 0) {
|
||||
$uniqueStudentIds[] = $studentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueStudentIds = array_values(array_unique($uniqueStudentIds));
|
||||
$totalStudents = count($uniqueStudentIds);
|
||||
$summary['attendanceSummary']['students'] = $totalStudents;
|
||||
|
||||
$scoreRows = [];
|
||||
if (!empty($classSectionIds)) {
|
||||
$scoreRows = SemesterScore::query()
|
||||
->select(['student_id', 'class_section_id', 'midterm_exam_score', 'final_exam_score', 'participation_score'])
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$scoreTypesConfig = [
|
||||
'participation' => ['label' => 'Participation', 'field' => 'participation_score'],
|
||||
];
|
||||
if ($semester === 'Fall') {
|
||||
$scoreTypesConfig = array_merge([
|
||||
'midterm' => ['label' => 'Midterm', 'field' => 'midterm_exam_score'],
|
||||
], $scoreTypesConfig);
|
||||
}
|
||||
if ($semester === 'Spring') {
|
||||
$scoreTypesConfig = array_merge([
|
||||
'final' => ['label' => 'Final exam', 'field' => 'final_exam_score'],
|
||||
], $scoreTypesConfig);
|
||||
}
|
||||
|
||||
$filledCounts = array_fill_keys(array_keys($scoreTypesConfig), 0);
|
||||
foreach ($scoreRows as $row) {
|
||||
foreach ($scoreTypesConfig as $key => $cfg) {
|
||||
$value = trim((string) ($row[$cfg['field']] ?? ''));
|
||||
if ($value !== '') {
|
||||
$filledCounts[$key]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expectedScoreCount = $totalStudents > 0 ? $totalStudents : count($scoreRows);
|
||||
$scoreDetails = [];
|
||||
foreach ($scoreTypesConfig as $key => $cfg) {
|
||||
$filled = $filledCounts[$key] ?? 0;
|
||||
$missing = max(0, $expectedScoreCount - $filled);
|
||||
$completionPct = $expectedScoreCount > 0 ? (int) round(min(100, ($filled / $expectedScoreCount) * 100)) : 0;
|
||||
$scoreDetails[$key] = [
|
||||
'label' => $cfg['label'],
|
||||
'field' => $cfg['field'],
|
||||
'filled' => $filled,
|
||||
'missing' => $missing,
|
||||
'expected' => $expectedScoreCount,
|
||||
'completionPct' => $completionPct,
|
||||
];
|
||||
}
|
||||
|
||||
$scoreTypeByField = [];
|
||||
if ($semester === 'Fall') {
|
||||
$scoreTypeByField['midterm_exam_score'] = 'midterm';
|
||||
}
|
||||
if ($semester === 'Spring') {
|
||||
$scoreTypeByField['final_exam_score'] = 'final';
|
||||
}
|
||||
$activeScoreType = $scoreTypeByField[$scoreField] ?? array_key_first($scoreDetails);
|
||||
$activeScoreDetails = $scoreDetails[$activeScoreType] ?? array_values($scoreDetails)[0];
|
||||
|
||||
$summary['scoreSummary'] = [
|
||||
'completionPct' => $activeScoreDetails['completionPct'],
|
||||
'missing' => $activeScoreDetails['missing'],
|
||||
'expected' => $activeScoreDetails['expected'],
|
||||
'filled' => $activeScoreDetails['filled'],
|
||||
'fieldLabel' => $scoreLabel,
|
||||
'types' => $scoreDetails,
|
||||
];
|
||||
|
||||
$participationMetrics = $scoreDetails['participation'];
|
||||
$summary['participationSummary'] = [
|
||||
'filled' => $participationMetrics['filled'],
|
||||
'missing' => $participationMetrics['missing'],
|
||||
'completionPct' => $participationMetrics['completionPct'],
|
||||
'expected' => $participationMetrics['expected'],
|
||||
];
|
||||
|
||||
$commentRows = [];
|
||||
if (!empty($uniqueStudentIds)) {
|
||||
$commentRows = ScoreComment::query()
|
||||
->select(['student_id', 'score_type', 'comment', 'comment_review'])
|
||||
->whereIn('student_id', $uniqueStudentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$expectedCommentTypes = array_values(array_filter([
|
||||
'ptap',
|
||||
$semester === 'Fall' ? 'midterm' : null,
|
||||
$semester === 'Spring' ? 'final' : null,
|
||||
]));
|
||||
|
||||
$commentStats = [];
|
||||
$commentsByStudent = [];
|
||||
foreach ($commentRows as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
$type = strtolower(trim((string) ($row['score_type'] ?? '')));
|
||||
if ($type === '') {
|
||||
$type = 'general';
|
||||
}
|
||||
if (!isset($commentStats[$type])) {
|
||||
$commentStats[$type] = ['pending' => 0, 'reviewed' => 0];
|
||||
}
|
||||
|
||||
$commentText = trim((string) ($row['comment'] ?? ''));
|
||||
$reviewValue = trim((string) ($row['comment_review'] ?? ''));
|
||||
|
||||
if ($sid > 0 && $commentText !== '') {
|
||||
$commentsByStudent[$sid][$type] = $commentText;
|
||||
}
|
||||
|
||||
if ($commentText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($reviewValue === '') {
|
||||
$commentStats[$type]['pending']++;
|
||||
} else {
|
||||
$commentStats[$type]['reviewed']++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
if (!isset($commentStats[$type])) {
|
||||
$commentStats[$type] = ['pending' => 0, 'reviewed' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
$missingComments = 0;
|
||||
$missingDetail = [];
|
||||
foreach ($uniqueStudentIds as $sid) {
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
$text = $commentsByStudent[$sid][$type] ?? '';
|
||||
if ($text === '') {
|
||||
$missingDetail[$type] = ($missingDetail[$type] ?? 0) + 1;
|
||||
$missingComments++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$commentTypes = [];
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
$stats = $commentStats[$type];
|
||||
$filled = $stats['pending'] + $stats['reviewed'];
|
||||
$completionPct = $totalStudents > 0 ? (int) round(min(100, ($filled / $totalStudents) * 100)) : 0;
|
||||
|
||||
$commentTypes[$type] = [
|
||||
'label' => ucfirst($type) . ' comments',
|
||||
'pending' => $stats['pending'],
|
||||
'reviewed' => $stats['reviewed'],
|
||||
'filled' => $filled,
|
||||
'missing' => $missingDetail[$type] ?? 0,
|
||||
'expected' => $totalStudents,
|
||||
'completionPct' => $completionPct,
|
||||
];
|
||||
}
|
||||
|
||||
$totalPending = array_sum(array_column($commentTypes, 'pending'));
|
||||
$totalReviewed = array_sum(array_column($commentTypes, 'reviewed'));
|
||||
$totalFilled = array_sum(array_column($commentTypes, 'filled'));
|
||||
$totalExpected = array_sum(array_column($commentTypes, 'expected'));
|
||||
$commentCompletionPct = $totalExpected > 0 ? (int) round(min(100, ($totalFilled / $totalExpected) * 100)) : 0;
|
||||
|
||||
$summary['commentSummary'] = [
|
||||
'total' => $totalFilled,
|
||||
'pendingReview' => $totalPending,
|
||||
'reviewed' => $totalReviewed,
|
||||
'missing' => $missingComments,
|
||||
'missingDetail' => $missingDetail,
|
||||
'completionPct' => $commentCompletionPct,
|
||||
'types' => $commentTypes,
|
||||
];
|
||||
|
||||
$attendanceRows = [];
|
||||
if (!empty($classSectionIds)) {
|
||||
$attendanceRows = AttendanceRecord::query()
|
||||
->select(['student_id', 'class_section_id', 'total_absence'])
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$attendanceSet = [];
|
||||
$totalAbsences = 0;
|
||||
foreach ($attendanceRows as $record) {
|
||||
$studentId = (int) ($record['student_id'] ?? 0);
|
||||
$sectionId = (int) ($record['class_section_id'] ?? 0);
|
||||
if ($studentId <= 0 || $sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$key = "{$sectionId}:{$studentId}";
|
||||
$attendanceSet[$key] = true;
|
||||
$totalAbsences += (int) ($record['total_absence'] ?? 0);
|
||||
}
|
||||
|
||||
$attendanceRecorded = count($attendanceSet);
|
||||
$attendancePct = $totalStudents > 0 ? (int) round(min(100, ($attendanceRecorded / $totalStudents) * 100)) : 0;
|
||||
$avgAbsence = $attendanceRecorded > 0 ? round($totalAbsences / $attendanceRecorded, 1) : 0;
|
||||
|
||||
$summary['attendanceSummary'] = [
|
||||
'recorded' => $attendanceRecorded,
|
||||
'students' => $totalStudents,
|
||||
'completionPct' => $attendancePct,
|
||||
'avgAbsences' => $avgAbsence,
|
||||
];
|
||||
|
||||
$timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC');
|
||||
$today = (new DateTimeImmutable('now', $timezone))->format('Y-m-d');
|
||||
|
||||
$attendanceStatus = $this->buildAttendanceSubmissionStatus($classSectionIds, $semester, $schoolYear, $today);
|
||||
$summary['attendanceStatus'] = $attendanceStatus;
|
||||
|
||||
$summary['deadlineEvents'] = Calendar::query()
|
||||
->where('notify_teacher', 1)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->where('date', '>=', $today)
|
||||
->orderBy('date', 'ASC')
|
||||
->limit(5)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
if ($attendanceStatus['submitted'] === false && !empty($classSectionIds)) {
|
||||
$summary['attendanceSummary']['completionPct'] = 0;
|
||||
$summary['attendanceSummary']['recorded'] = 0;
|
||||
}
|
||||
|
||||
$summary['notifications'] = $this->buildDashboardNotifications($summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function buildDashboardNotifications(array $summary): array
|
||||
{
|
||||
$notifications = [];
|
||||
$buildScoreLink = static function (?string $focus): string {
|
||||
$base = '/teacher/scores';
|
||||
return $focus ? $base . '?focus=' . urlencode($focus) : $base;
|
||||
};
|
||||
$determineFocus = static function (array $event): string {
|
||||
$eventTypeText = strtolower(trim((string) ($event['event_type'] ?? '')));
|
||||
if ($eventTypeText !== '' && str_contains($eventTypeText, 'draft')) {
|
||||
return 'comments';
|
||||
}
|
||||
$text = strtolower(trim(($event['title'] ?? '') . ' ' . ($event['description'] ?? '')));
|
||||
if ($text === '') {
|
||||
return 'scores';
|
||||
}
|
||||
if (str_contains($text, 'comment') || str_contains($text, 'ptap')) {
|
||||
return 'comments';
|
||||
}
|
||||
return 'scores';
|
||||
};
|
||||
$calendarLink = '/teacher/calendar';
|
||||
$attendanceLink = '/teacher/showupdate_attendance';
|
||||
$timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC');
|
||||
$todayDate = (new DateTimeImmutable('now', $timezone))->setTime(0, 0);
|
||||
|
||||
$calendarNotificationConfig = [
|
||||
'1st Semester Scores' => 14,
|
||||
'2nd Semester Scores' => 14,
|
||||
'Final' => 14,
|
||||
'Final Exam Draft' => 42,
|
||||
'Midterm' => 14,
|
||||
'Midterm Exam Draft' => 42,
|
||||
];
|
||||
|
||||
$activeDeadline = false;
|
||||
foreach ($summary['deadlineEvents'] ?? [] as $event) {
|
||||
$eventTypeRaw = trim((string) ($event['event_type'] ?? ''));
|
||||
if ($eventTypeRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$matchedType = null;
|
||||
$thresholdDays = null;
|
||||
foreach ($calendarNotificationConfig as $type => $days) {
|
||||
if (strcasecmp($eventTypeRaw, $type) === 0) {
|
||||
$matchedType = $type;
|
||||
$thresholdDays = $days;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($matchedType === null || $thresholdDays === null) {
|
||||
continue;
|
||||
}
|
||||
$rawDate = $event['date'] ?? '';
|
||||
if ($rawDate === '') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$eventDateTime = new DateTimeImmutable($rawDate, $timezone);
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
$eventDateTime = $eventDateTime->setTime(0, 0);
|
||||
$daysUntil = (int) $todayDate->diff($eventDateTime)->format('%r%a');
|
||||
|
||||
if ($daysUntil <= 0) {
|
||||
$activeDeadline = true;
|
||||
$eventFocus = $determineFocus($event);
|
||||
$notifications[] = [
|
||||
'type' => 'danger',
|
||||
'message' => sprintf(
|
||||
"Deadline today: %s is due. Submit the remaining entries before the day ends.",
|
||||
$matchedType
|
||||
),
|
||||
'link' => $buildScoreLink($eventFocus),
|
||||
'linkText' => 'Submit now',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($daysUntil > $thresholdDays) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativeText = $daysUntil === 1 ? ' (tomorrow)' : " (in {$daysUntil} days)";
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"%s is due on %s%s. Review it on the calendar so you don’t miss it.",
|
||||
$matchedType,
|
||||
$eventDateTime->format('M j, Y'),
|
||||
$relativeText
|
||||
),
|
||||
'link' => $calendarLink,
|
||||
'linkText' => 'View deadline',
|
||||
];
|
||||
}
|
||||
|
||||
if (!$activeDeadline) {
|
||||
$scoreLabel = $summary['scoreSummary']['fieldLabel'] ?? 'Score';
|
||||
if (!empty($summary['scoreSummary']['missing'] ?? 0)) {
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"You have %s %s entries missing on the scores page. Please finish submitting them.",
|
||||
(int) $summary['scoreSummary']['missing'],
|
||||
$scoreLabel
|
||||
),
|
||||
'link' => $buildScoreLink('scores'),
|
||||
'linkText' => 'Score sheet',
|
||||
];
|
||||
}
|
||||
|
||||
foreach (['midterm', 'final', 'ptap'] as $type) {
|
||||
$count = (int) ($summary['commentSummary']['missingDetail'][$type] ?? 0);
|
||||
if ($count <= 0) {
|
||||
continue;
|
||||
}
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"%s comment missing for %s student(s). Leave comments on the scores page.",
|
||||
ucfirst($type),
|
||||
$count
|
||||
),
|
||||
'link' => $buildScoreLink('comments'),
|
||||
'linkText' => ucfirst($type) . ' comments',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($summary['participationSummary']['missing'] ?? 0)) {
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"Participation entries are still missing for %s student(s). Please add them from the scores page.",
|
||||
(int) $summary['participationSummary']['missing']
|
||||
),
|
||||
'link' => $buildScoreLink('scores'),
|
||||
'linkText' => 'Participation',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($summary['attendanceStatus']['missingSections'] ?? [])) {
|
||||
$missingNames = $summary['attendanceStatus']['missingNames'] ?? [];
|
||||
$label = '';
|
||||
if (!empty($missingNames)) {
|
||||
$label = ' (' . implode(', ', array_slice($missingNames, 0, 3)) . (count($missingNames) > 3 ? '…' : '') . ')';
|
||||
}
|
||||
$notifications[] = [
|
||||
'type' => 'danger',
|
||||
'message' => "Today's attendance is still unsubmitted for {$summary['attendanceStatus']['date']}{$label}. Please submit it now.",
|
||||
'link' => $attendanceLink,
|
||||
'linkText' => 'Record attendance',
|
||||
];
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function buildAttendanceSubmissionStatus(array $classSectionIds, string $semester, string $schoolYear, string $date): array
|
||||
{
|
||||
if (empty($classSectionIds)) {
|
||||
return [
|
||||
'date' => $date,
|
||||
'missingSections' => [],
|
||||
'missingNames' => [],
|
||||
'submitted' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$rows = AttendanceDay::query()
|
||||
->select(['class_section_id', 'status'])
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$statusMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$csId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($csId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$statusMap[$csId] = strtolower(trim((string) ($row['status'] ?? '')));
|
||||
}
|
||||
|
||||
$missingSections = [];
|
||||
foreach ($classSectionIds as $classSectionId) {
|
||||
$status = $statusMap[$classSectionId] ?? '';
|
||||
if (!in_array($status, ['submitted', 'published', 'finalized'], true)) {
|
||||
$missingSections[] = $classSectionId;
|
||||
}
|
||||
}
|
||||
|
||||
$missingNames = [];
|
||||
if (!empty($missingSections)) {
|
||||
$sectionRows = ClassSection::query()
|
||||
->select(['class_section_id', 'class_section_name'])
|
||||
->whereIn('class_section_id', $missingSections)
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($sectionRows as $row) {
|
||||
$name = trim((string) ($row['class_section_name'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = (string) ($row['class_section_id'] ?? '');
|
||||
}
|
||||
$missingNames[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'date' => $date,
|
||||
'missingSections' => $missingSections,
|
||||
'missingNames' => $missingNames,
|
||||
'submitted' => empty($missingSections),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class ProfileIconService
|
||||
{
|
||||
public function buildForUser(int $userId): array
|
||||
{
|
||||
$user = User::query()->find($userId);
|
||||
if (!$user) {
|
||||
return [
|
||||
'userName' => 'User not found',
|
||||
'userInitials' => '??',
|
||||
];
|
||||
}
|
||||
|
||||
$firstname = (string) ($user->firstname ?? '');
|
||||
$lastname = (string) ($user->lastname ?? '');
|
||||
|
||||
$initials = '';
|
||||
if ($firstname !== '') {
|
||||
$initials .= strtoupper($firstname[0]);
|
||||
}
|
||||
if ($lastname !== '') {
|
||||
$initials .= strtoupper($lastname[0]);
|
||||
}
|
||||
|
||||
return [
|
||||
'userName' => trim($firstname . ' ' . $lastname),
|
||||
'userInitials' => $initials !== '' ? $initials : '??',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Frontend;
|
||||
|
||||
class StaticPageService
|
||||
{
|
||||
public function load(string $filename, string $type): array
|
||||
{
|
||||
$safeName = basename($filename);
|
||||
$path = public_path('html/' . $safeName);
|
||||
|
||||
if (!is_file($path) || !is_readable($path)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'Page content not available.',
|
||||
];
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'Page content not available.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'content' => $content,
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user