From 68a5c9edca8c4266d5c9a069446817c8f67db873 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 7 Jun 2026 20:01:58 -0400 Subject: [PATCH] update the new school year model --- .../Api/BadgeScan/BadgeScanController.php | 25 ++- .../ClassProgress/ClassProgressController.php | 17 +- .../Api/SchoolYears/SchoolYearController.php | 39 +++- .../Subjects/SubjectCurriculumController.php | 6 +- .../ClassProgressIndexRequest.php | 2 + .../ClassProgressStoreRequest.php | 2 + .../SubjectCurriculumStoreRequest.php | 1 + .../SubjectCurriculumUpdateRequest.php | 1 + app/Models/ClassProgressReport.php | 2 + app/Models/SubjectCurriculum.php | 15 +- app/Services/BadgeScan/BadgeScanService.php | 22 +- .../ClassProgressMetaService.php | 4 +- .../ClassProgressMutationService.php | 33 ++- .../ClassProgressQueryService.php | 24 +++ .../SchoolYears/SchoolYearContextService.php | 191 ++++++++++++++++++ .../Subjects/SubjectCurriculumService.php | 48 ++++- ...chool_year_to_subject_curriculum_items.php | 30 +++ ...ear_semester_to_class_progress_reports.php | 40 ++++ routes/api.php | 2 + 19 files changed, 472 insertions(+), 32 deletions(-) create mode 100644 app/Services/SchoolYears/SchoolYearContextService.php create mode 100644 database/migrations/2026_06_07_000000_add_school_year_to_subject_curriculum_items.php create mode 100644 database/migrations/2026_06_07_000001_add_school_year_semester_to_class_progress_reports.php diff --git a/app/Http/Controllers/Api/BadgeScan/BadgeScanController.php b/app/Http/Controllers/Api/BadgeScan/BadgeScanController.php index 81404f5c..ed55d50e 100644 --- a/app/Http/Controllers/Api/BadgeScan/BadgeScanController.php +++ b/app/Http/Controllers/Api/BadgeScan/BadgeScanController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\BadgeScan; use App\Http\Controllers\Api\Core\BaseApiController; use App\Services\BadgeScan\BadgeScanService; +use App\Services\SchoolYears\SchoolYearContextService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -13,6 +14,7 @@ class BadgeScanController extends BaseApiController { public function __construct( private BadgeScanService $badgeScan, + private SchoolYearContextService $schoolYears, ) { parent::__construct(); } @@ -24,13 +26,21 @@ class BadgeScanController extends BaseApiController { $validator = Validator::make($request->all(), [ 'badge_scan' => ['required', 'string', 'max:255'], + 'school_year' => ['nullable', 'string', 'max:50'], + 'semester' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return $this->respondValidationError($validator->errors()->toArray()); } - $result = $this->badgeScan->processScan($validator->validated()['badge_scan']); + $payload = $validator->validated(); + $context = $this->schoolYears->options($payload['school_year'] ?? null, $payload['semester'] ?? null); + $result = $this->badgeScan->processScan( + $payload['badge_scan'], + $context['school_year'] ?? null, + $context['semester'] ?? null + ); if (! $result['recognized']) { return $this->error($result['message'], Response::HTTP_NOT_FOUND); @@ -46,10 +56,19 @@ class BadgeScanController extends BaseApiController /** * Authenticated scan log listing (legacy RFIDController::log). */ - public function logs(): JsonResponse + public function logs(Request $request): JsonResponse { + $context = $this->schoolYears->options( + $request->query('school_year'), + $request->query('semester') + ); + return $this->success([ - 'logs' => $this->badgeScan->scanLogRows(), + 'logs' => $this->badgeScan->scanLogRows( + $context['school_year'] ?? null, + $context['semester'] ?? null + ), + 'meta' => $context, ]); } } diff --git a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php index 318e1a49..45ddf489 100644 --- a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php +++ b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php @@ -40,7 +40,9 @@ class ClassProgressController extends BaseApiController return $this->respondSuccess( $this->buildMetaPayload( $request->validated('class_id'), - (int) ($request->validated('sunday_count') ?? 12) + (int) ($request->validated('sunday_count') ?? 12), + $request->query('school_year'), + $request->query('semester') ), 'Progress metadata loaded.' ); @@ -66,7 +68,7 @@ class ClassProgressController extends BaseApiController fn (array $assignment) => (int) ($assignment['class_id'] ?? 0) === $activeClassId ) ?? ($assignments[0] ?? []); $sundayCount = (int) ($request->validated('sunday_count') ?? 12); - $payload = $this->buildMetaPayload($activeClassId > 0 ? $activeClassId : null, $sundayCount); + $payload = $this->buildMetaPayload($activeClassId > 0 ? $activeClassId : null, $sundayCount, $schoolYear, $semester); $payload['classes'] = $assignments; $payload['defaults'] = [ 'class_id' => $activeClassId > 0 ? $activeClassId : null, @@ -87,6 +89,9 @@ class ClassProgressController extends BaseApiController } $filters = $request->validated(); + $meta = $this->queryService->meta($filters['school_year'] ?? null, $filters['semester'] ?? null); + $filters['school_year'] = $filters['school_year'] ?? ($meta['school_year'] ?? null); + $filters['semester'] = $filters['semester'] ?? ($meta['semester'] ?? null); $paginator = $this->queryService->listReports($auth, $filters); if (!empty($filters['group_by_week'])) { @@ -96,6 +101,7 @@ class ClassProgressController extends BaseApiController return $this->respondSuccess([ 'items' => ClassProgressReportResource::collection($paginator->items()), + 'meta' => $meta, 'pagination' => [ 'current_page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), @@ -241,13 +247,16 @@ class ClassProgressController extends BaseApiController return $user; } - private function buildMetaPayload(?int $classId, int $sundayCount): array + private function buildMetaPayload(?int $classId, int $sundayCount, ?string $schoolYear = null, ?string $semester = null): array { + $meta = $this->queryService->meta($schoolYear, $semester); + return [ 'subject_sections' => $this->metaService->subjectSections(), 'status_options' => $this->metaService->statusOptions(), 'sunday_options' => $this->metaService->sundayOptionsAlignedWithCi($sundayCount), - 'curriculum' => $this->metaService->curriculumOptions($classId), + 'curriculum' => $this->metaService->curriculumOptions($classId, $meta['school_year'] ?? null), + 'meta' => $meta, ]; } } diff --git a/app/Http/Controllers/Api/SchoolYears/SchoolYearController.php b/app/Http/Controllers/Api/SchoolYears/SchoolYearController.php index 55125897..17524a98 100644 --- a/app/Http/Controllers/Api/SchoolYears/SchoolYearController.php +++ b/app/Http/Controllers/Api/SchoolYears/SchoolYearController.php @@ -7,6 +7,7 @@ use App\Http\Requests\SchoolYears\CloseSchoolYearRequest; use App\Http\Requests\SchoolYears\PreviewCloseSchoolYearRequest; use App\Http\Requests\SchoolYears\SaveSchoolYearRequest; use App\Services\SchoolYears\SchoolYearClosureService; +use App\Services\SchoolYears\SchoolYearContextService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -14,14 +15,44 @@ use Symfony\Component\HttpFoundation\Response; class SchoolYearController extends BaseApiController { - public function __construct(private SchoolYearClosureService $service) - { + public function __construct( + private SchoolYearClosureService $service, + private SchoolYearContextService $context + ) { parent::__construct(); } - public function index(): JsonResponse + public function index(Request $request): JsonResponse { - return response()->json(['ok' => true, 'data' => $this->service->list()]); + $context = $this->context->options( + $request->query('school_year'), + $request->query('semester') + ); + + return response()->json([ + 'ok' => true, + 'data' => $this->service->list(), + 'meta' => $context, + 'school_years' => $context['school_years'], + 'current_school_year' => $context['current_school_year'], + 'current_semester' => $context['current_semester'], + ]); + } + + public function current(Request $request): JsonResponse + { + return response()->json([ + 'ok' => true, + 'data' => $this->context->options( + $request->query('school_year'), + $request->query('semester') + ), + ]); + } + + public function options(Request $request): JsonResponse + { + return $this->current($request); } public function store(SaveSchoolYearRequest $request): JsonResponse diff --git a/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php b/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php index 0990ad02..d9681de7 100644 --- a/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php +++ b/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php @@ -10,6 +10,7 @@ use App\Http\Resources\Subjects\SubjectCurriculumResource; use App\Models\SubjectCurriculum; use App\Services\Subjects\SubjectCurriculumService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class SubjectCurriculumController extends BaseApiController { @@ -18,15 +19,16 @@ class SubjectCurriculumController extends BaseApiController parent::__construct(); } - public function index(): JsonResponse + public function index(Request $request): JsonResponse { - $data = $this->service->list(); + $data = $this->service->list($request->query('school_year')); return response()->json([ 'ok' => true, 'classes' => SubjectClassResource::collection($data['classes']), 'entries' => SubjectCurriculumResource::collection($data['entries']), 'subject_labels' => $data['subject_labels'], + 'meta' => $data['meta'], ]); } diff --git a/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php b/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php index 5eab4020..aded861c 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php @@ -18,6 +18,8 @@ class ClassProgressIndexRequest extends ClassProgressFormRequest return [ 'class_section_id' => ['nullable', 'integer', 'min:1'], 'teacher_id' => ['nullable', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], 'week_start' => ['nullable', 'date_format:Y-m-d'], 'week_end' => ['nullable', 'date_format:Y-m-d'], 'subject' => ['nullable', 'string', 'max:120'], diff --git a/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php b/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php index f972858e..ea3d0e69 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php @@ -15,6 +15,8 @@ class ClassProgressStoreRequest extends ClassProgressFormRequest { $rules = [ 'class_section_id' => ['required', 'integer', 'exists:classSection,class_section_id'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], 'week_start' => ['required', 'date_format:Y-m-d'], 'week_end' => ['nullable', 'date_format:Y-m-d'], 'support_needed' => ['nullable', 'string'], diff --git a/app/Http/Requests/Subjects/SubjectCurriculumStoreRequest.php b/app/Http/Requests/Subjects/SubjectCurriculumStoreRequest.php index dcaee03a..9f42218c 100644 --- a/app/Http/Requests/Subjects/SubjectCurriculumStoreRequest.php +++ b/app/Http/Requests/Subjects/SubjectCurriculumStoreRequest.php @@ -10,6 +10,7 @@ class SubjectCurriculumStoreRequest extends ApiFormRequest { return [ 'class_id' => ['required', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], 'subject' => ['required', 'in:islamic,quran'], 'chapter_name' => ['required', 'string', 'max:255'], 'unit_number' => ['nullable', 'integer'], diff --git a/app/Http/Requests/Subjects/SubjectCurriculumUpdateRequest.php b/app/Http/Requests/Subjects/SubjectCurriculumUpdateRequest.php index 3b4dec72..598a7eaa 100644 --- a/app/Http/Requests/Subjects/SubjectCurriculumUpdateRequest.php +++ b/app/Http/Requests/Subjects/SubjectCurriculumUpdateRequest.php @@ -10,6 +10,7 @@ class SubjectCurriculumUpdateRequest extends ApiFormRequest { return [ 'class_id' => ['required', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], 'subject' => ['required', 'in:islamic,quran'], 'chapter_name' => ['required', 'string', 'max:255'], 'unit_number' => ['nullable', 'integer'], diff --git a/app/Models/ClassProgressReport.php b/app/Models/ClassProgressReport.php index b9283257..9badfcd8 100644 --- a/app/Models/ClassProgressReport.php +++ b/app/Models/ClassProgressReport.php @@ -16,6 +16,8 @@ class ClassProgressReport extends BaseModel protected $fillable = [ 'class_section_id', 'teacher_id', + 'school_year', + 'semester', 'week_start', 'week_end', 'subject', diff --git a/app/Models/SubjectCurriculum.php b/app/Models/SubjectCurriculum.php index e8cefce2..c0196437 100644 --- a/app/Models/SubjectCurriculum.php +++ b/app/Models/SubjectCurriculum.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\Schema; class SubjectCurriculum extends BaseModel { @@ -12,6 +13,7 @@ class SubjectCurriculum extends BaseModel protected $fillable = [ 'class_id', + 'school_year', 'subject', 'unit_number', 'unit_title', @@ -54,6 +56,16 @@ class SubjectCurriculum extends BaseModel return $q->where('subject', $subject); } + public function scopeForSchoolYear(Builder $q, ?string $schoolYear): Builder + { + $schoolYear = trim((string) $schoolYear); + if ($schoolYear === '' || !Schema::hasColumn($this->getTable(), 'school_year')) { + return $q; + } + + return $q->where('school_year', $schoolYear); + } + public function scopeOrdered(Builder $q): Builder { return $q->orderBy('unit_number', 'asc') @@ -68,11 +80,12 @@ class SubjectCurriculum extends BaseModel /** * legacy: getOptionsForClass($classId, $subject) */ - public static function getOptionsForClass(int $classId, string $subject): array + public static function getOptionsForClass(int $classId, string $subject, ?string $schoolYear = null): array { return static::query() ->forClass($classId) ->forSubject($subject) + ->forSchoolYear($schoolYear) ->ordered() ->get() ->all(); diff --git a/app/Services/BadgeScan/BadgeScanService.php b/app/Services/BadgeScan/BadgeScanService.php index d767f9e6..644437e8 100644 --- a/app/Services/BadgeScan/BadgeScanService.php +++ b/app/Services/BadgeScan/BadgeScanService.php @@ -17,7 +17,7 @@ class BadgeScanService /** * @return array{recognized: bool, message: string, user_id?: int, display_name?: string} */ - public function processScan(string $badgeScan): array + public function processScan(string $badgeScan, ?string $schoolYear = null, ?string $semester = null): array { $tag = trim($badgeScan); if ($tag === '') { @@ -51,8 +51,8 @@ class BadgeScanService 'user_id' => $user->id, 'card_id' => $tag, 'scan_time' => now(), - 'school_year' => Configuration::getConfigValueByKey('school_year'), - 'semester' => Configuration::getConfigValueByKey('semester'), + 'school_year' => $schoolYear ?: Configuration::getConfigValueByKey('school_year'), + 'semester' => $semester ?: Configuration::getConfigValueByKey('semester'), ]); return [ @@ -68,13 +68,13 @@ class BadgeScanService * * @return list> */ - public function scanLogRows(): array + public function scanLogRows(?string $schoolYear = null, ?string $semester = null): array { if (! Schema::hasTable('scan_log')) { return []; } - return DB::table('scan_log') + $query = DB::table('scan_log') ->select([ 'scan_log.id', 'scan_log.user_id', @@ -88,7 +88,17 @@ class BadgeScanService 'students.lastname as student_lastname', ]) ->leftJoin('users', 'users.id', '=', 'scan_log.user_id') - ->leftJoin('students', 'students.rfid_tag', '=', 'scan_log.card_id') + ->leftJoin('students', 'students.rfid_tag', '=', 'scan_log.card_id'); + + if ($schoolYear !== null && trim($schoolYear) !== '') { + $query->where('scan_log.school_year', trim($schoolYear)); + } + + if ($semester !== null && trim($semester) !== '') { + $query->where('scan_log.semester', trim($semester)); + } + + return $query ->orderByDesc('scan_log.scan_time') ->limit(self::LOG_LIMIT) ->get() diff --git a/app/Services/ClassProgress/ClassProgressMetaService.php b/app/Services/ClassProgress/ClassProgressMetaService.php index ea9ba662..8b016aeb 100644 --- a/app/Services/ClassProgress/ClassProgressMetaService.php +++ b/app/Services/ClassProgress/ClassProgressMetaService.php @@ -138,7 +138,7 @@ class ClassProgressMetaService return $svc->getSchoolYearRange($schoolYear); } - public function curriculumOptions(?int $classId): array + public function curriculumOptions(?int $classId, ?string $schoolYear = null): array { if (!$classId) { return []; @@ -146,7 +146,7 @@ class ClassProgressMetaService $options = []; foreach ($this->subjectSections() as $slug => $section) { - $options[$slug] = SubjectCurriculum::getOptionsForClass((int) $classId, (string) $slug); + $options[$slug] = SubjectCurriculum::getOptionsForClass((int) $classId, (string) $slug, $schoolYear); } return $options; diff --git a/app/Services/ClassProgress/ClassProgressMutationService.php b/app/Services/ClassProgress/ClassProgressMutationService.php index 638c5b0d..86602d0f 100644 --- a/app/Services/ClassProgress/ClassProgressMutationService.php +++ b/app/Services/ClassProgress/ClassProgressMutationService.php @@ -6,6 +6,8 @@ use App\Models\ClassProgressAttachment; use App\Models\ClassProgressReport; use App\Models\TeacherClass; use App\Models\User; +use App\Services\SchoolYears\SchoolYearContextService; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -16,6 +18,7 @@ class ClassProgressMutationService private ClassProgressRuleService $rules, private ClassProgressAttachmentService $attachments, private ClassProgressQueryService $queries, + private SchoolYearContextService $schoolYears, ) {} public function createReports(User $user, array $payload, array $filesBySubject): Collection @@ -25,6 +28,9 @@ class ClassProgressMutationService $this->assertTeacherAssignment($user, $classSectionId); + $schoolYear = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null); + $semester = $this->schoolYears->currentSemester($payload['semester'] ?? null); + $weekStart = (string) ($payload['week_start'] ?? ''); $weekEnd = $this->rules->ensureWeekEnd($weekStart, $payload['week_end'] ?? null); @@ -51,7 +57,7 @@ class ClassProgressMutationService throw new \InvalidArgumentException('CONFIRM_OVERWRITE'); } - $reports = DB::transaction(function () use ($user, $payload, $subjectSections, $classSectionId, $weekStart, $weekEnd, $filesBySubject, $confirmOverwrite, $existingIds) { + $reports = DB::transaction(function () use ($user, $payload, $subjectSections, $classSectionId, $schoolYear, $semester, $weekStart, $weekEnd, $filesBySubject, $confirmOverwrite, $existingIds) { if ($existingIds !== [] && $confirmOverwrite) { ClassProgressAttachment::query()->whereIn('report_id', $existingIds)->delete(); ClassProgressReport::query()->whereIn('id', $existingIds)->delete(); @@ -70,7 +76,7 @@ class ClassProgressMutationService $chapterValues = (array) ($payload["chapter_{$slug}"] ?? []); $unitTitle = $this->rules->buildUnitChapterSummary($unitValues, $chapterValues); - $report = ClassProgressReport::query()->create([ + $data = [ 'teacher_id' => $user->id, 'class_section_id' => $classSectionId, 'week_start' => $weekStart, @@ -82,7 +88,16 @@ class ClassProgressMutationService 'status' => $this->rules->defaultStatus(), 'support_needed' => $payload['support_needed'] ?? null, 'flags_json' => $this->rules->normalizeFlags($payload['flags'] ?? null), - ]); + ]; + + if (Schema::hasColumn('class_progress_reports', 'school_year')) { + $data['school_year'] = $schoolYear !== '' ? $schoolYear : null; + } + if (Schema::hasColumn('class_progress_reports', 'semester')) { + $data['semester'] = $semester !== '' ? $semester : null; + } + + $report = ClassProgressReport::query()->create($data); $attachments = $filesBySubject[$slug] ?? []; $stored = $this->attachments->storeAttachments($report->id, $attachments); @@ -121,6 +136,9 @@ class ClassProgressMutationService $this->assertTeacherAssignment($user, $classSectionId); + $schoolYear = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null); + $semester = $this->schoolYears->currentSemester($payload['semester'] ?? null); + $weekStart = (string) ($payload['week_start'] ?? ''); $weekEnd = $this->rules->ensureWeekEnd($weekStart, $payload['week_end'] ?? null); @@ -172,6 +190,8 @@ class ClassProgressMutationService $classSectionId, $weekStart, $weekEnd, + $schoolYear, + $semester, $filesBySubject, $allowedIds, $originalWeekStart, @@ -226,6 +246,13 @@ class ClassProgressMutationService $data['flags_json'] = $this->rules->normalizeFlags($payload['flags'] ?? null); } + if (Schema::hasColumn('class_progress_reports', 'school_year')) { + $data['school_year'] = $schoolYear !== '' ? $schoolYear : null; + } + if (Schema::hasColumn('class_progress_reports', 'semester')) { + $data['semester'] = $semester !== '' ? $semester : null; + } + if ($existing) { $existing->fill($data); $existing->save(); diff --git a/app/Services/ClassProgress/ClassProgressQueryService.php b/app/Services/ClassProgress/ClassProgressQueryService.php index 6a72fc14..c3d4aa50 100644 --- a/app/Services/ClassProgress/ClassProgressQueryService.php +++ b/app/Services/ClassProgress/ClassProgressQueryService.php @@ -7,10 +7,21 @@ use App\Models\ClassProgressReport; use App\Models\Configuration; use App\Models\TeacherClass; use App\Models\User; +use App\Services\SchoolYears\SchoolYearContextService; +use Illuminate\Support\Facades\Schema; use Illuminate\Contracts\Pagination\LengthAwarePaginator; class ClassProgressQueryService { + private SchoolYearContextService $schoolYears; + + public function __construct($schoolYears = null) + { + $this->schoolYears = $schoolYears instanceof SchoolYearContextService + ? $schoolYears + : app(SchoolYearContextService::class); + } + public function listReports(User $user, array $filters): LengthAwarePaginator { $query = ClassProgressReport::query() @@ -51,6 +62,14 @@ class ClassProgressQueryService $query->where('status', (string) $filters['status']); } + if (!empty($filters['school_year']) && Schema::hasColumn('class_progress_reports', 'school_year')) { + $query->where('school_year', (string) $filters['school_year']); + } + + if (!empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) { + $query->where('semester', (string) $filters['semester']); + } + if (!empty($filters['week_start'])) { $query->whereDate('week_start', '>=', $filters['week_start']); } @@ -132,6 +151,11 @@ class ClassProgressQueryService return TeacherClass::getClassAssignmentsByUserId($user->id, $schoolYear, $semester); } + public function meta(?string $schoolYear = null, ?string $semester = null): array + { + return $this->schoolYears->options($schoolYear, $semester); + } + private function statusLabel(?string $status): string { $options = (array) config('progress.status_options', []); diff --git a/app/Services/SchoolYears/SchoolYearContextService.php b/app/Services/SchoolYears/SchoolYearContextService.php new file mode 100644 index 00000000..bbaf6f4b --- /dev/null +++ b/app/Services/SchoolYears/SchoolYearContextService.php @@ -0,0 +1,191 @@ +where('is_current', true)->orderByDesc('id')->first(); + if ($year && trim((string) $year->name) !== '') { + return (string) $year->name; + } + } + + return ''; + } + + public function currentSemester(?string $requested = null): string + { + $requested = trim((string) $requested); + if ($requested !== '') { + return $requested; + } + + return trim((string) (Configuration::getConfig('semester') ?? '')); + } + + public function options(?string $selectedSchoolYear = null, ?string $selectedSemester = null): array + { + if (Schema::hasTable('school_years')) { + $this->resolver->ensureCurrentTracked(); + } + + $currentSchoolYear = $this->currentSchoolYear($selectedSchoolYear); + $currentSemester = $this->currentSemester($selectedSemester); + + $schoolYears = $this->schoolYears($currentSchoolYear); + $semesters = $this->semesters($currentSemester); + + return [ + 'school_year' => $currentSchoolYear !== '' ? $currentSchoolYear : null, + 'current_school_year' => $currentSchoolYear !== '' ? $currentSchoolYear : null, + 'semester' => $currentSemester !== '' ? $currentSemester : null, + 'current_semester' => $currentSemester !== '' ? $currentSemester : null, + 'school_years' => $schoolYears, + 'semesters' => $semesters, + ]; + } + + public function schoolYears(?string $include = null): array + { + $years = []; + + if (Schema::hasTable('school_years')) { + $years = SchoolYear::query() + ->orderByDesc('start_date') + ->orderByDesc('id') + ->get() + ->map(fn (SchoolYear $year) => [ + 'id' => (int) $year->id, + 'name' => (string) $year->name, + 'label' => (string) $year->name, + 'start_date' => $year->start_date ? $year->start_date->format('Y-m-d') : null, + 'end_date' => $year->end_date ? $year->end_date->format('Y-m-d') : null, + 'status' => (string) $year->status, + 'is_current' => (bool) $year->is_current, + 'is_editable' => $year->status === SchoolYear::STATUS_ACTIVE, + ]) + ->all(); + } + + $seen = []; + foreach ($years as $row) { + $seen[$row['name']] = true; + } + + foreach ($this->legacySchoolYearNames() as $name) { + if (!isset($seen[$name])) { + $years[] = [ + 'id' => null, + 'name' => $name, + 'label' => $name, + 'start_date' => null, + 'end_date' => null, + 'status' => 'legacy', + 'is_current' => false, + 'is_editable' => true, + ]; + $seen[$name] = true; + } + } + + $include = trim((string) $include); + if ($include !== '' && !isset($seen[$include])) { + array_unshift($years, [ + 'id' => null, + 'name' => $include, + 'label' => $include, + 'start_date' => null, + 'end_date' => null, + 'status' => 'selected', + 'is_current' => true, + 'is_editable' => true, + ]); + } + + return $years; + } + + public function semesters(?string $include = null): array + { + $names = ['Fall', 'Spring']; + $current = trim((string) $include); + if ($current !== '' && !in_array($current, $names, true)) { + array_unshift($names, $current); + } + + return array_map(fn (string $name) => [ + 'name' => $name, + 'label' => $name, + 'is_current' => $current !== '' && strcasecmp($name, $current) === 0, + ], $names); + } + + private function legacySchoolYearNames(): array + { + $tables = [ + 'students', 'parents', 'users', 'staff', 'student_class', 'teacher_class', + 'events', 'calendar_events', 'scan_log', 'exam_drafts', 'inventory_items', + 'inventory_movements', 'certificate_records', 'semester_scores', 'final_scores', + 'student_decisions', 'below_sixty_decisions', 'whatsapp_group_links', + ]; + + $names = []; + foreach ($tables as $table) { + if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'school_year')) { + continue; + } + + try { + $rows = DB::table($table) + ->whereNotNull('school_year') + ->where('school_year', '!=', '') + ->distinct() + ->orderByDesc('school_year') + ->limit(20) + ->pluck('school_year') + ->all(); + } catch (\Throwable $e) { + continue; + } + + foreach ($rows as $row) { + $name = trim((string) $row); + if ($name !== '') { + $names[$name] = true; + } + } + } + + $configured = trim((string) (Configuration::getConfig('school_year') ?? '')); + if ($configured !== '') { + $names[$configured] = true; + } + + $out = array_keys($names); + rsort($out, SORT_NATURAL); + + return $out; + } +} diff --git a/app/Services/Subjects/SubjectCurriculumService.php b/app/Services/Subjects/SubjectCurriculumService.php index 9e76c63c..232c2748 100644 --- a/app/Services/Subjects/SubjectCurriculumService.php +++ b/app/Services/Subjects/SubjectCurriculumService.php @@ -4,25 +4,46 @@ namespace App\Services\Subjects; use App\Models\SchoolClass; use App\Models\SubjectCurriculum; +use App\Services\SchoolYears\SchoolYearContextService; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class SubjectCurriculumService { + private SchoolYearContextService $schoolYears; + public const SUBJECT_OPTIONS = [ 'islamic' => 'Islamic Studies', 'quran' => 'Quran/Arabic', ]; - public function list(): array + public function __construct(?SchoolYearContextService $schoolYears = null) + { + $this->schoolYears = $schoolYears ?: app(SchoolYearContextService::class); + } + + public function list(?string $schoolYear = null): array { $classes = SchoolClass::query() ->orderBy('class_name') ->get() ->toArray(); - $entries = DB::table('subject_curriculum_items as sci') + $resolvedSchoolYear = $this->schoolYears->currentSchoolYear($schoolYear); + + $entriesQuery = DB::table('subject_curriculum_items as sci') ->leftJoin('classes', 'classes.id', '=', 'sci.class_id') - ->select('sci.*', 'classes.class_name') + ->select('sci.*', 'classes.class_name'); + + if ($resolvedSchoolYear !== '' && Schema::hasColumn('subject_curriculum_items', 'school_year')) { + $entriesQuery->where(function ($query) use ($resolvedSchoolYear) { + $query->where('sci.school_year', $resolvedSchoolYear) + ->orWhereNull('sci.school_year') + ->orWhere('sci.school_year', ''); + }); + } + + $entries = $entriesQuery ->orderBy('classes.class_name') ->orderBy('sci.subject') ->orderBy('sci.unit_number') @@ -35,18 +56,25 @@ class SubjectCurriculumService 'classes' => $classes, 'entries' => $entries, 'subject_labels' => self::SUBJECT_OPTIONS, + 'meta' => $this->schoolYears->options($resolvedSchoolYear), ]; } public function store(array $payload): SubjectCurriculum { - return SubjectCurriculum::query()->create([ + $data = [ 'class_id' => (int) $payload['class_id'], 'subject' => (string) $payload['subject'], 'unit_number' => $payload['unit_number'] !== null ? (int) $payload['unit_number'] : null, 'unit_title' => $payload['unit_title'] !== '' ? $payload['unit_title'] : null, 'chapter_name' => (string) $payload['chapter_name'], - ]); + ]; + + if (Schema::hasColumn('subject_curriculum_items', 'school_year')) { + $data['school_year'] = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null) ?: null; + } + + return SubjectCurriculum::query()->create($data); } public function update(int $id, array $payload): ?SubjectCurriculum @@ -56,13 +84,19 @@ class SubjectCurriculumService return null; } - $entry->update([ + $data = [ 'class_id' => (int) $payload['class_id'], 'subject' => (string) $payload['subject'], 'unit_number' => $payload['unit_number'] !== null ? (int) $payload['unit_number'] : null, 'unit_title' => $payload['unit_title'] !== '' ? $payload['unit_title'] : null, 'chapter_name' => (string) $payload['chapter_name'], - ]); + ]; + + if (Schema::hasColumn('subject_curriculum_items', 'school_year')) { + $data['school_year'] = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null) ?: null; + } + + $entry->update($data); return $entry; } diff --git a/database/migrations/2026_06_07_000000_add_school_year_to_subject_curriculum_items.php b/database/migrations/2026_06_07_000000_add_school_year_to_subject_curriculum_items.php new file mode 100644 index 00000000..ea80876c --- /dev/null +++ b/database/migrations/2026_06_07_000000_add_school_year_to_subject_curriculum_items.php @@ -0,0 +1,30 @@ +string('school_year', 20)->nullable()->after('class_id')->index(); + }); + } + + public function down(): void + { + if (! Schema::hasTable('subject_curriculum_items') || ! Schema::hasColumn('subject_curriculum_items', 'school_year')) { + return; + } + + Schema::table('subject_curriculum_items', function (Blueprint $table) { + $table->dropColumn('school_year'); + }); + } +}; diff --git a/database/migrations/2026_06_07_000001_add_school_year_semester_to_class_progress_reports.php b/database/migrations/2026_06_07_000001_add_school_year_semester_to_class_progress_reports.php new file mode 100644 index 00000000..c5e5b462 --- /dev/null +++ b/database/migrations/2026_06_07_000001_add_school_year_semester_to_class_progress_reports.php @@ -0,0 +1,40 @@ +string('school_year', 20)->nullable()->after('teacher_id')->index(); + } + if (! Schema::hasColumn('class_progress_reports', 'semester')) { + $table->string('semester', 20)->nullable()->after('school_year')->index(); + } + }); + } + + public function down(): void + { + if (! Schema::hasTable('class_progress_reports')) { + return; + } + + Schema::table('class_progress_reports', function (Blueprint $table) { + if (Schema::hasColumn('class_progress_reports', 'semester')) { + $table->dropColumn('semester'); + } + if (Schema::hasColumn('class_progress_reports', 'school_year')) { + $table->dropColumn('school_year'); + } + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 25180903..bd22a180 100644 --- a/routes/api.php +++ b/routes/api.php @@ -478,6 +478,8 @@ Route::prefix('v1')->group(function () { Route::middleware(['auth:api', 'admin.access'])->prefix('school-years')->group(function () { Route::get('/', [SchoolYearController::class, 'index']); + Route::get('current', [SchoolYearController::class, 'current']); + Route::get('options', [SchoolYearController::class, 'options']); Route::post('/', [SchoolYearController::class, 'store']); Route::patch('{schoolYear}', [SchoolYearController::class, 'update'])->whereNumber('schoolYear'); Route::get('{schoolYear}/summary', [SchoolYearController::class, 'summary'])->whereNumber('schoolYear');