From 2ef71cc92bd178911e0c6c297cc02c6a29aafcfd Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Mar 2026 17:53:15 -0400 Subject: [PATCH] add all controllers logic --- app/Console/Commands/ImportUsers.php | 30 + .../Api/Attendance/LateSlipLogsController.php | 78 ++ .../Controllers/Api/Auth/IpBanController.php | 115 +++ .../Api/Auth/SessionTimeoutController.php | 67 -- .../Controllers/Api/BaseApiController.php | 33 +- .../ClassPreparationController.php | 74 +- .../Api/Classes/ClassController.php | 136 +++ .../Api/Finance/PaypalPaymentController.php | 3 +- .../Api/Frontend/FrontendController.php | 72 ++ .../Api/Frontend/InfoIconController.php | 31 + .../Api/Frontend/LandingPageController.php | 86 ++ .../Api/Frontend/PageController.php | 62 ++ .../Api/Messaging/MessagesController.php | 222 +++++ .../Api/Settings/PolicyController.php | 43 + .../Api/Settings/PreferencesController.php | 139 +++ .../Api/Settings/SettingsController.php | 51 + .../Controllers/Api/Staff/StaffController.php | 121 +++ .../Api/Support/ContactController.php | 27 + .../Api/Support/SupportController.php | 83 ++ .../Api/System/DashboardController.php | 31 + .../Api/System/DatabaseHealthController.php | 30 + .../Api/System/HealthController.php | 27 + .../Api/System/NavBuilderController.php | 90 ++ app/Http/Controllers/Api/Ui/UiController.php | 32 + .../Utilities/PhoneFormatterController.php | 36 + app/Http/Controllers/View/ClassController.php | 150 --- .../View/ClassPreparationController.php | 545 ----------- .../View/DashboardRedirectController.php | 45 - app/Http/Kernel.php | 2 - app/Http/Middleware/ApiDocsAuth.php | 65 ++ app/Http/Middleware/ApiJwtAuth.php | 29 + app/Http/Middleware/CleanupScheduler.php | 26 + app/Http/Middleware/PrimeTimezone.php | 26 + app/Http/Middleware/RequirePermission.php | 32 + .../Attendance/LateSlipLogIndexRequest.php | 28 + .../Auth/SessionTimeoutCheckRequest.php | 18 - .../Auth/SessionTimeoutConfigRequest.php | 18 - .../Auth/SessionTimeoutPingRequest.php | 18 - .../ClassPreparationAdjustmentRequest.php | 23 + .../ClassPreparationIndexRequest.php | 21 + .../ClassPreparationMarkPrintedRequest.php | 23 + .../ClassPreparationPrintRequest.php | 32 + .../Classes/ClassSectionIndexRequest.php | 27 + .../Classes/ClassSectionStoreRequest.php | 25 + .../Classes/ClassSectionUpdateRequest.php | 33 + .../Frontend/ContactSubmitRequest.php | 17 + .../Frontend/LandingTeacherRequest.php | 20 + .../Messaging/MessageIndexRequest.php | 21 + .../Requests/Messaging/MessageSendRequest.php | 23 + .../Messaging/MessageUpdateRequest.php | 17 + .../Payments/PaypalExecuteRequest.php | 1 + .../Preferences/PreferencesIndexRequest.php | 25 + .../Preferences/PreferencesUpsertRequest.php | 31 + .../Requests/Security/IpBanIndexRequest.php | 25 + app/Http/Requests/Security/IpBanRequest.php | 34 + app/Http/Requests/Security/IpUnbanRequest.php | 36 + .../Requests/Settings/PolicyShowRequest.php | 20 + .../Settings/SettingsUpdateRequest.php | 17 + app/Http/Requests/Staff/StaffIndexRequest.php | 21 + app/Http/Requests/Staff/StaffStoreRequest.php | 22 + .../Requests/Staff/StaffUpdateRequest.php | 21 + .../Requests/Support/ContactSendRequest.php | 18 + .../Support/SupportRequestIndexRequest.php | 20 + .../Support/SupportRequestStoreRequest.php | 16 + .../Requests/System/NavItemReorderRequest.php | 21 + .../Requests/System/NavItemStoreRequest.php | 30 + app/Http/Requests/Ui/UiStyleRequest.php | 19 + .../Requests/Utilities/PhoneFormatRequest.php | 20 + .../Attendance/LateSlipLogCollection.php | 27 + .../Attendance/LateSlipLogResource.php | 28 + .../Auth/SessionTimeoutConfigResource.php | 20 - .../Auth/SessionTimeoutStatusResource.php | 18 - .../ClassPreparationPrintResource.php | 22 + .../ClassPreparationResultResource.php | 23 + .../Classes/ClassAttendanceResource.php | 21 + .../ClassAttendanceStudentResource.php | 21 + .../Classes/ClassSectionCollection.php | 27 + .../Classes/ClassSectionResource.php | 24 + .../Frontend/ContactSubmissionResource.php | 18 + .../Frontend/FrontendPageResource.php | 16 + .../Frontend/FrontendUserResource.php | 17 + .../Frontend/LandingPageResource.php | 18 + .../Frontend/PageContentResource.php | 17 + .../Frontend/ProfileIconResource.php | 17 + .../Resources/Messaging/MessageCollection.php | 31 + .../Resources/Messaging/MessageResource.php | 31 + .../Resources/Messaging/RecipientResource.php | 17 + .../Preferences/PreferencesCollection.php | 27 + .../Preferences/PreferencesListResource.php | 28 + .../Preferences/PreferencesResource.php | 21 + .../Resources/Security/IpBanCollection.php | 27 + app/Http/Resources/Security/IpBanResource.php | 26 + .../Resources/Settings/PolicyResource.php | 20 + .../Resources/Settings/SettingsResource.php | 18 + app/Http/Resources/Staff/StaffCollection.php | 31 + app/Http/Resources/Staff/StaffResource.php | 29 + .../Support/ContactMessageResource.php | 17 + .../Support/SupportRequestCollection.php | 31 + .../Support/SupportRequestResource.php | 24 + .../System/DashboardRouteResource.php | 19 + .../Resources/System/HealthStatusResource.php | 20 + .../System/NavBuilderDataResource.php | 18 + app/Http/Resources/System/NavItemResource.php | 28 + app/Http/Resources/Ui/UiStyleResource.php | 20 + .../Utilities/PhoneFormatResource.php | 18 + app/Models/SupportRequest.php | 34 + app/Policies/ClassSectionPolicy.php | 55 ++ app/Policies/IpAttemptPolicy.php | 50 + app/Policies/LateSlipLogPolicy.php | 40 + app/Policies/MessagePolicy.php | 55 ++ app/Policies/NavItemPolicy.php | 15 + app/Policies/PreferencesPolicy.php | 50 + app/Policies/SettingPolicy.php | 35 + app/Policies/StaffPolicy.php | 50 + app/Policies/SupportRequestPolicy.php | 40 + app/Providers/AppServiceProvider.php | 29 + .../Attendance/LateSlipLogCommandService.php | 13 + .../Attendance/LateSlipLogQueryService.php | 63 ++ .../Auth/RegistrationCaptchaService.php | 14 +- app/Services/Auth/SessionActivityService.php | 39 - .../Auth/SessionTimeoutConfigService.php | 21 - app/Services/Auth/SessionTimeoutService.php | 72 -- app/Services/Auth/UserRoleService.php | 54 ++ ...lassPreparationAdjustmentWriterService.php | 51 + .../ClassPreparationPrintService.php | 60 ++ .../ClassPreparationService.php | 25 +- .../ClassSections/ClassAttendanceService.php | 32 + .../ClassSectionCommandService.php | 26 + .../ClassSectionQueryService.php | 62 ++ .../ClassSections/ClassSectionSeedService.php | 52 ++ .../Dashboard/DashboardRouteService.php | 69 ++ app/Services/Docs/OpenApiRouteExporter.php | 19 +- .../Frontend/ContactSubmissionService.php | 62 ++ app/Services/Frontend/FrontendPageService.php | 13 + .../Frontend/LandingPageContextService.php | 18 + .../LandingPageParentDashboardService.php | 148 +++ .../Frontend/LandingPageRoleService.php | 31 + app/Services/Frontend/LandingPageService.php | 90 ++ .../LandingPageTeacherSummaryService.php | 595 ++++++++++++ app/Services/Frontend/ProfileIconService.php | 35 + app/Services/Frontend/StaticPageService.php | 33 + .../Messaging/MessageCommandService.php | 126 +++ .../Messaging/MessageQueryService.php | 115 +++ .../Messaging/MessageRecipientService.php | 90 ++ app/Services/Navigation/NavBuilderService.php | 216 +++++ app/Services/Navigation/NavbarService.php | 55 ++ .../Payments/PaypalPaymentService.php | 27 +- app/Services/Phone/PhoneFormatterService.php | 22 + app/Services/Policy/PolicyContentService.php | 50 + .../Preferences/PreferencesCommandService.php | 56 ++ .../Preferences/PreferencesOptionsService.php | 48 + .../Preferences/PreferencesQueryService.php | 83 ++ app/Services/Roles/RoleSwitchService.php | 2 - .../School/SemesterSelectionService.php | 10 - app/Services/Scores/ScoreCommentService.php | 21 +- app/Services/Security/IpBanCommandService.php | 81 ++ app/Services/Security/IpBanQueryService.php | 46 + app/Services/Settings/SettingsService.php | 48 + app/Services/Staff/StaffCommandService.php | 100 ++ app/Services/Staff/StaffQueryService.php | 112 +++ .../Support/ContactMessageService.php | 41 + .../Support/SupportRequestService.php | 80 ++ .../System/CleanupSchedulerService.php | 29 + app/Services/System/DatabaseHealthService.php | 24 + app/Services/System/HealthCheckService.php | 64 ++ app/Services/System/TimeService.php | 12 + app/Services/Ui/UiStyleService.php | 94 ++ app/old/ApiClient.php | 107 --- app/old/ClassController.php | 157 ---- app/old/ContactController.php | 66 -- app/old/DashboardRedirectController.php | 48 - app/old/DocsController.php | 25 - app/old/Filters/.gitkeep | 0 app/old/Filters/ApiAuthFilter.php | 59 -- app/old/Filters/ApiDocsAuthFilter.php | 55 -- app/old/Filters/AuthFilter.php | 200 ---- app/old/Filters/CleanupScheduler.php | 36 - app/old/Filters/PermissionFilter.php | 31 - app/old/Filters/TimezoneFilter.php | 26 - app/old/FrontendController.php | 84 -- app/old/HealthController.php | 75 -- app/old/InfoIconController.php | 39 - app/old/IpBanController.php | 148 --- app/old/LandingPageController.php | 882 ------------------ app/old/LateSlipLogsController.php | 63 -- app/old/MessagesController.php | 249 ----- app/old/NavBuilderController.php | 303 ------ app/old/NavbarService.php | 66 -- app/old/PageController.php | 64 -- app/old/PhoneFormatterService.php | 31 - app/old/PolicyController.php | 36 - app/old/PreferencesController.php | 144 --- app/old/RFIDController.php | 71 -- app/old/SendSMSController.php | 54 -- app/old/SettingsController.php | 38 - app/old/StaffController.php | 156 ---- app/old/StatsController.php | 32 - app/old/SupportController.php | 133 --- app/old/TestDBController.php | 27 - app/old/TimeService.php | 222 ----- app/old/UiController.php | 102 -- bootstrap/app.php | 5 + .../2026_02_23_204001_create_users_table.php | 4 + ...026_02_23_204002_create_students_table.php | 4 + ...026_02_23_204030_create_expenses_table.php | 4 + ...3_204035_create_current_incident_table.php | 4 + ...026_02_23_204036_create_incident_table.php | 4 + ...2_23_204037_create_grading_locks_table.php | 4 + ...026_02_23_204038_create_homework_table.php | 4 + ...4039_create_inventory_categories_table.php | 4 + ...23_204040_create_inventory_items_table.php | 4 + ...04041_create_inventory_movements_table.php | 4 + ...2_23_204042_create_invoice_event_table.php | 4 + ...026_02_23_204043_create_invoices_table.php | 4 + ...044_create_invoice_students_list_table.php | 4 + ..._02_23_204045_create_ip_attempts_table.php | 4 + ..._23_204046_create_late_slip_logs_table.php | 4 + ..._23_204047_create_login_activity_table.php | 4 + ...23_204048_create_manual_payments_table.php | 4 + ...026_02_23_204049_create_messages_table.php | 4 + ...02_23_204050_create_midterm_exam_table.php | 4 + ...1_create_missing_score_overrides_table.php | 4 + ...26_02_23_204052_create_nav_items_table.php | 4 + ...2_23_204053_create_notifications_table.php | 4 + ...create_parent_attendance_reports_table.php | 4 + ..._create_parent_meeting_schedules_table.php | 4 + ...2026_02_23_204056_create_parents_table.php | 4 + ...4057_create_parent_notifications_table.php | 4 + ...2_23_204058_create_participation_table.php | 4 + ...23_204059_create_password_resets_table.php | 4 + ...0_create_password_reset_requests_table.php | 4 + ...100_create_reimbursement_batches_table.php | 4 + ...23_204101_create_paypal_payments_table.php | 4 + ..._23_204101_create_reimbursements_table.php | 4 + ...2_23_204102_create_payment_error_table.php | 4 + ...026_02_23_204103_create_payments_table.php | 4 + ...create_payment_notification_logs_table.php | 4 + ...4105_create_payment_transactions_table.php | 4 + ...04106_create_paypal_transactions_table.php | 4 + ..._02_23_204107_create_permissions_table.php | 4 + ..._204108_create_placement_batches_table.php | 4 + ...3_204109_create_placement_levels_table.php | 4 + ...3_204110_create_placement_scores_table.php | 4 + ...3_204111_create_user_preferences_table.php | 4 + ..._23_204112_create_print_requests_table.php | 4 + ...2026_02_23_204113_create_project_table.php | 4 + ...23_204114_create_promotion_queue_table.php | 4 + .../2026_02_23_204117_create_quiz_table.php | 4 + ...2026_02_23_204118_create_refunds_table.php | 4 + ..._reimbursement_batch_admin_files_table.php | 4 + .../2026_02_23_204123_create_roles_table.php | 4 + ..._23_204124_create_role_nav_items_table.php | 4 + ...3_204125_create_role_permissions_table.php | 4 + ..._23_204126_create_score_comments_table.php | 4 + ...026_02_23_204127_create_sections_table.php | 4 + ...23_204128_create_semester_scores_table.php | 4 + ...026_02_23_204129_create_settings_table.php | 4 + ...3_204130_create_staff_attendance_table.php | 4 + .../2026_02_23_204131_create_staff_table.php | 4 + ..._204133_create_student_allergies_table.php | 4 + ...2_23_204134_create_student_class_table.php | 4 + ...reate_student_medical_conditions_table.php | 4 + ..._create_subject_curriculum_items_table.php | 4 + ...2_23_204138_create_teacher_class_table.php | 4 + ..._submission_notification_history_table.php | 4 + ...204142_create_user_notifications_table.php | 4 + ...6_02_23_204143_create_user_roles_table.php | 4 + ...4144_create_whatsapp_group_links_table.php | 4 + ...reate_whatsapp_group_memberships_table.php | 4 + ...4146_create_whatsapp_invites_log_table.php | 4 + ...50_create_class_progress_reports_table.php | 4 + ...ate_report_card_acknowledgements_table.php | 4 + ...reate_class_progress_attachments_table.php | 4 + ...create_reimbursement_batch_items_table.php | 4 + ...900001_create_additional_charges_table.php | 4 + ...eate_admin_notification_subjects_table.php | 4 + ...eate_attendance_comment_template_table.php | 4 + ...23_900004_create_attendance_data_table.php | 4 + ..._23_900005_create_attendance_day_table.php | 4 + ..._900006_create_attendance_record_table.php | 4 + ...00007_create_attendance_tracking_table.php | 4 + ...3_900008_create_authorized_users_table.php | 4 + ...3_900009_create_badge_print_logs_table.php | 4 + ...23_900010_create_calendar_events_table.php | 4 + ...026_02_23_900011_create_chapters_table.php | 4 + ...02_23_900012_create_classSection_table.php | 4 + ...13_create_class_prep_adjustments_table.php | 4 + ...014_create_class_preparation_log_table.php | 4 + ...2026_02_23_900017_create_classes_table.php | 4 + ...create_competition_class_winners_table.php | 4 + ...900019_create_competition_scores_table.php | 4 + ...00020_create_competition_winners_table.php | 4 + ...02_23_900021_create_competitions_table.php | 4 + ...2_23_900022_create_configuration_table.php | 4 + ...26_02_23_900023_create_contactus_table.php | 4 + ...02_23_900024_create_current_flag_table.php | 4 + ...23_900025_create_discount_usages_table.php | 4 + ..._900026_create_discount_vouchers_table.php | 4 + ...reate_early_dismissal_signatures_table.php | 4 + ...23_900028_create_email_templates_table.php | 4 + ...900029_create_emergency_contacts_table.php | 4 + ..._02_23_900030_create_enrollments_table.php | 4 + ...2_23_900031_create_event_charges_table.php | 4 + .../2026_02_23_900032_create_events_table.php | 4 + ..._02_23_900033_create_exam_drafts_table.php | 4 + ..._create_examquestiondistribution_table.php | 4 + ...2_23_900035_create_examquestions_table.php | 4 + .../2026_02_23_900036_create_exams_table.php | 4 + ...026_02_23_900038_create_families_table.php | 4 + ..._900039_create_family_comm_prefs_table.php | 4 + ...3_900040_create_family_guardians_table.php | 4 + ...23_900041_create_family_students_table.php | 4 + ...6_02_23_900042_create_final_exam_table.php | 4 + ..._02_23_900043_create_final_score_table.php | 4 + ...00044_create_longanswerquestions_table.php | 4 + ...6_create_notification_recipients_table.php | 4 + ..._02_23_900047_create_preferences_table.php | 4 + ...02_23_900048_create_qcmquestions_table.php | 4 + ...26_02_23_900049_create_questions_table.php | 4 + ...900050_create_questionstruefalse_table.php | 4 + ...2_23_900051_create_questiontypes_table.php | 4 + ...026_02_23_900052_create_scan_log_table.php | 4 + ...0053_create_shortanswerquestions_table.php | 4 + ...3_900054_create_support_requests_table.php | 4 + ...5_create_teacher_attendance_data_table.php | 4 + ...900056_create_communication_logs_table.php | 4 + ...12_create_personal_access_tokens_table.php | 4 + ...026_03_09_064908_create_sessions_table.php | 4 + ...26_03_09_070000_create_suppliers_table.php | 4 + ...026_03_09_070010_create_supplies_table.php | 4 + ...70020_create_supply_transactions_table.php | 4 + ...09_070030_create_purchase_orders_table.php | 4 + ...0040_create_purchase_order_items_table.php | 4 + ..._070050_create_supply_categories_table.php | 4 + .../2026_03_11_045324_create_cache_table.php | 4 + ...026_03_11_205736_create_students_table.php | 31 + ...026_03_11_205736_create_teachers_table.php | 31 + ...11_205737_create_attendance_data_table.php | 31 + public/html/privacy_policy.html | 1 + public/html/terms_of_service.html | 1 + resources/policies/picture_policy.html | 2 + resources/policies/school_policy.html | 2 + routes/api.php | 159 +++- .../Attendance/LateSlipLogsControllerTest.php | 136 +++ .../Api/V1/Auth/IpBanControllerTest.php | 182 ++++ .../Api/V1/Auth/RegisterControllerTest.php | 7 +- .../V1/Auth/SessionTimeoutControllerTest.php | 123 --- .../ClassPreparationControllerTest.php | 75 +- .../V1/Classes/ClassSectionControllerTest.php | 282 ++++++ .../V1/Frontend/FrontendControllerTest.php | 72 ++ .../V1/Frontend/InfoIconControllerTest.php | 40 + .../V1/Frontend/LandingPageControllerTest.php | 81 ++ .../Api/V1/Frontend/PageControllerTest.php | 59 ++ .../V1/Messaging/MessagesControllerTest.php | 236 +++++ .../Api/V1/Settings/PolicyControllerTest.php | 27 + .../V1/Settings/PreferencesControllerTest.php | 169 ++++ .../V1/Settings/SettingsControllerTest.php | 103 ++ .../Api/V1/Staff/StaffControllerTest.php | 169 ++++ .../Api/V1/Support/ContactControllerTest.php | 33 + .../Api/V1/Support/SupportControllerTest.php | 108 +++ .../Api/V1/System/DashboardControllerTest.php | 59 ++ .../System/DatabaseHealthControllerTest.php | 19 + .../Api/V1/System/HealthControllerTest.php | 31 + .../V1/System/NavBuilderControllerTest.php | 150 +++ tests/Feature/Api/V1/Ui/UiControllerTest.php | 76 ++ .../PhoneFormatterControllerTest.php | 83 ++ .../RequirePermissionMiddlewareTest.php | 84 ++ .../Frontend/FrontendPageResourceTest.php | 17 + .../Frontend/PageContentResourceTest.php | 22 + .../Messaging/MessageResourceTest.php | 34 + .../Messaging/RecipientResourceTest.php | 18 + .../Settings/SettingsResourceTest.php | 24 + .../Resources/Staff/StaffResourceTest.php | 29 + .../Support/ContactMessageResourceTest.php | 22 + .../Support/SupportRequestResourceTest.php | 28 + .../Unit/Resources/Ui/UiStyleResourceTest.php | 26 + .../LateSlipLogCommandServiceTest.php | 33 + .../LateSlipLogQueryServiceTest.php | 37 + .../Auth/RegistrationCaptchaServiceTest.php | 10 +- .../Services/Auth/RegistrationServiceTest.php | 14 +- .../Auth/SessionActivityServiceTest.php | 42 - .../Auth/SessionTimeoutConfigServiceTest.php | 22 - .../Auth/SessionTimeoutServiceTest.php | 51 - .../Services/Auth/UserRoleServiceTest.php | 44 + ...PreparationAdjustmentWriterServiceTest.php | 33 + .../ClassPreparationPrintServiceTest.php | 105 +++ .../ClassAttendanceServiceTest.php | 73 ++ .../ClassSectionCommandServiceTest.php | 36 + .../ClassSectionQueryServiceTest.php | 47 + .../ClassSectionSeedServiceTest.php | 31 + .../Dashboard/DashboardRouteServiceTest.php | 75 ++ .../Frontend/ContactSubmissionServiceTest.php | 39 + .../Frontend/FrontendPageServiceTest.php | 17 + .../LandingPageContextServiceTest.php | 29 + .../LandingPageParentDashboardServiceTest.php | 57 ++ .../Frontend/LandingPageRoleServiceTest.php | 53 ++ .../Frontend/LandingPageServiceTest.php | 57 ++ .../LandingPageTeacherSummaryServiceTest.php | 43 + .../Frontend/ProfileIconServiceTest.php | 37 + .../Frontend/StaticPageServiceTest.php | 32 + .../Messaging/MessageCommandServiceTest.php | 72 ++ .../Messaging/MessageQueryServiceTest.php | 58 ++ .../Messaging/MessageRecipientServiceTest.php | 111 +++ .../Navigation/NavBuilderServiceTest.php | 56 ++ .../Services/Navigation/NavbarServiceTest.php | 49 + .../Phone/PhoneFormatterServiceTest.php | 26 + .../Policy/PolicyContentServiceTest.php | 23 + .../PreferencesCommandServiceTest.php | 30 + .../PreferencesOptionsServiceTest.php | 22 + .../PreferencesQueryServiceTest.php | 54 ++ .../School/SemesterSelectionServiceTest.php | 7 +- .../Security/IpBanCommandServiceTest.php | 48 + .../Security/IpBanQueryServiceTest.php | 35 + .../Services/Settings/SettingsServiceTest.php | 35 + .../Staff/StaffCommandServiceTest.php | 60 ++ .../Services/Staff/StaffQueryServiceTest.php | 49 + .../Support/ContactMessageServiceTest.php | 27 + .../Support/SupportRequestServiceTest.php | 55 ++ .../System/CleanupSchedulerServiceTest.php | 35 + .../System/HealthCheckServiceTest.php | 22 + tests/Unit/Services/Ui/UiStyleServiceTest.php | 44 + 421 files changed, 12009 insertions(+), 5211 deletions(-) create mode 100644 app/Console/Commands/ImportUsers.php create mode 100644 app/Http/Controllers/Api/Attendance/LateSlipLogsController.php create mode 100644 app/Http/Controllers/Api/Auth/IpBanController.php delete mode 100644 app/Http/Controllers/Api/Auth/SessionTimeoutController.php create mode 100644 app/Http/Controllers/Api/Classes/ClassController.php create mode 100644 app/Http/Controllers/Api/Frontend/FrontendController.php create mode 100644 app/Http/Controllers/Api/Frontend/InfoIconController.php create mode 100644 app/Http/Controllers/Api/Frontend/LandingPageController.php create mode 100644 app/Http/Controllers/Api/Frontend/PageController.php create mode 100644 app/Http/Controllers/Api/Messaging/MessagesController.php create mode 100644 app/Http/Controllers/Api/Settings/PolicyController.php create mode 100644 app/Http/Controllers/Api/Settings/PreferencesController.php create mode 100644 app/Http/Controllers/Api/Settings/SettingsController.php create mode 100644 app/Http/Controllers/Api/Staff/StaffController.php create mode 100644 app/Http/Controllers/Api/Support/ContactController.php create mode 100644 app/Http/Controllers/Api/Support/SupportController.php create mode 100644 app/Http/Controllers/Api/System/DashboardController.php create mode 100644 app/Http/Controllers/Api/System/DatabaseHealthController.php create mode 100644 app/Http/Controllers/Api/System/HealthController.php create mode 100644 app/Http/Controllers/Api/System/NavBuilderController.php create mode 100644 app/Http/Controllers/Api/Ui/UiController.php create mode 100644 app/Http/Controllers/Api/Utilities/PhoneFormatterController.php delete mode 100755 app/Http/Controllers/View/ClassController.php delete mode 100755 app/Http/Controllers/View/ClassPreparationController.php delete mode 100755 app/Http/Controllers/View/DashboardRedirectController.php create mode 100644 app/Http/Middleware/ApiDocsAuth.php create mode 100644 app/Http/Middleware/ApiJwtAuth.php create mode 100644 app/Http/Middleware/CleanupScheduler.php create mode 100644 app/Http/Middleware/PrimeTimezone.php create mode 100644 app/Http/Middleware/RequirePermission.php create mode 100644 app/Http/Requests/Attendance/LateSlipLogIndexRequest.php delete mode 100644 app/Http/Requests/Auth/SessionTimeoutCheckRequest.php delete mode 100644 app/Http/Requests/Auth/SessionTimeoutConfigRequest.php delete mode 100644 app/Http/Requests/Auth/SessionTimeoutPingRequest.php create mode 100644 app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php create mode 100644 app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php create mode 100644 app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php create mode 100644 app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php create mode 100644 app/Http/Requests/Classes/ClassSectionIndexRequest.php create mode 100644 app/Http/Requests/Classes/ClassSectionStoreRequest.php create mode 100644 app/Http/Requests/Classes/ClassSectionUpdateRequest.php create mode 100644 app/Http/Requests/Frontend/ContactSubmitRequest.php create mode 100644 app/Http/Requests/Frontend/LandingTeacherRequest.php create mode 100644 app/Http/Requests/Messaging/MessageIndexRequest.php create mode 100644 app/Http/Requests/Messaging/MessageSendRequest.php create mode 100644 app/Http/Requests/Messaging/MessageUpdateRequest.php create mode 100644 app/Http/Requests/Preferences/PreferencesIndexRequest.php create mode 100644 app/Http/Requests/Preferences/PreferencesUpsertRequest.php create mode 100644 app/Http/Requests/Security/IpBanIndexRequest.php create mode 100644 app/Http/Requests/Security/IpBanRequest.php create mode 100644 app/Http/Requests/Security/IpUnbanRequest.php create mode 100644 app/Http/Requests/Settings/PolicyShowRequest.php create mode 100644 app/Http/Requests/Settings/SettingsUpdateRequest.php create mode 100644 app/Http/Requests/Staff/StaffIndexRequest.php create mode 100644 app/Http/Requests/Staff/StaffStoreRequest.php create mode 100644 app/Http/Requests/Staff/StaffUpdateRequest.php create mode 100644 app/Http/Requests/Support/ContactSendRequest.php create mode 100644 app/Http/Requests/Support/SupportRequestIndexRequest.php create mode 100644 app/Http/Requests/Support/SupportRequestStoreRequest.php create mode 100644 app/Http/Requests/System/NavItemReorderRequest.php create mode 100644 app/Http/Requests/System/NavItemStoreRequest.php create mode 100644 app/Http/Requests/Ui/UiStyleRequest.php create mode 100644 app/Http/Requests/Utilities/PhoneFormatRequest.php create mode 100644 app/Http/Resources/Attendance/LateSlipLogCollection.php create mode 100644 app/Http/Resources/Attendance/LateSlipLogResource.php delete mode 100644 app/Http/Resources/Auth/SessionTimeoutConfigResource.php delete mode 100644 app/Http/Resources/Auth/SessionTimeoutStatusResource.php create mode 100644 app/Http/Resources/ClassPreparation/ClassPreparationPrintResource.php create mode 100644 app/Http/Resources/ClassPreparation/ClassPreparationResultResource.php create mode 100644 app/Http/Resources/Classes/ClassAttendanceResource.php create mode 100644 app/Http/Resources/Classes/ClassAttendanceStudentResource.php create mode 100644 app/Http/Resources/Classes/ClassSectionCollection.php create mode 100644 app/Http/Resources/Classes/ClassSectionResource.php create mode 100644 app/Http/Resources/Frontend/ContactSubmissionResource.php create mode 100644 app/Http/Resources/Frontend/FrontendPageResource.php create mode 100644 app/Http/Resources/Frontend/FrontendUserResource.php create mode 100644 app/Http/Resources/Frontend/LandingPageResource.php create mode 100644 app/Http/Resources/Frontend/PageContentResource.php create mode 100644 app/Http/Resources/Frontend/ProfileIconResource.php create mode 100644 app/Http/Resources/Messaging/MessageCollection.php create mode 100644 app/Http/Resources/Messaging/MessageResource.php create mode 100644 app/Http/Resources/Messaging/RecipientResource.php create mode 100644 app/Http/Resources/Preferences/PreferencesCollection.php create mode 100644 app/Http/Resources/Preferences/PreferencesListResource.php create mode 100644 app/Http/Resources/Preferences/PreferencesResource.php create mode 100644 app/Http/Resources/Security/IpBanCollection.php create mode 100644 app/Http/Resources/Security/IpBanResource.php create mode 100644 app/Http/Resources/Settings/PolicyResource.php create mode 100644 app/Http/Resources/Settings/SettingsResource.php create mode 100644 app/Http/Resources/Staff/StaffCollection.php create mode 100644 app/Http/Resources/Staff/StaffResource.php create mode 100644 app/Http/Resources/Support/ContactMessageResource.php create mode 100644 app/Http/Resources/Support/SupportRequestCollection.php create mode 100644 app/Http/Resources/Support/SupportRequestResource.php create mode 100644 app/Http/Resources/System/DashboardRouteResource.php create mode 100644 app/Http/Resources/System/HealthStatusResource.php create mode 100644 app/Http/Resources/System/NavBuilderDataResource.php create mode 100644 app/Http/Resources/System/NavItemResource.php create mode 100644 app/Http/Resources/Ui/UiStyleResource.php create mode 100644 app/Http/Resources/Utilities/PhoneFormatResource.php create mode 100644 app/Models/SupportRequest.php create mode 100644 app/Policies/ClassSectionPolicy.php create mode 100644 app/Policies/IpAttemptPolicy.php create mode 100644 app/Policies/LateSlipLogPolicy.php create mode 100644 app/Policies/MessagePolicy.php create mode 100644 app/Policies/NavItemPolicy.php create mode 100644 app/Policies/PreferencesPolicy.php create mode 100644 app/Policies/SettingPolicy.php create mode 100644 app/Policies/StaffPolicy.php create mode 100644 app/Policies/SupportRequestPolicy.php create mode 100644 app/Services/Attendance/LateSlipLogCommandService.php create mode 100644 app/Services/Attendance/LateSlipLogQueryService.php delete mode 100644 app/Services/Auth/SessionActivityService.php delete mode 100644 app/Services/Auth/SessionTimeoutConfigService.php delete mode 100644 app/Services/Auth/SessionTimeoutService.php create mode 100644 app/Services/Auth/UserRoleService.php create mode 100644 app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php create mode 100644 app/Services/ClassPreparation/ClassPreparationPrintService.php create mode 100644 app/Services/ClassSections/ClassAttendanceService.php create mode 100644 app/Services/ClassSections/ClassSectionCommandService.php create mode 100644 app/Services/ClassSections/ClassSectionQueryService.php create mode 100644 app/Services/ClassSections/ClassSectionSeedService.php create mode 100644 app/Services/Dashboard/DashboardRouteService.php create mode 100644 app/Services/Frontend/ContactSubmissionService.php create mode 100644 app/Services/Frontend/FrontendPageService.php create mode 100644 app/Services/Frontend/LandingPageContextService.php create mode 100644 app/Services/Frontend/LandingPageParentDashboardService.php create mode 100644 app/Services/Frontend/LandingPageRoleService.php create mode 100644 app/Services/Frontend/LandingPageService.php create mode 100644 app/Services/Frontend/LandingPageTeacherSummaryService.php create mode 100644 app/Services/Frontend/ProfileIconService.php create mode 100644 app/Services/Frontend/StaticPageService.php create mode 100644 app/Services/Messaging/MessageCommandService.php create mode 100644 app/Services/Messaging/MessageQueryService.php create mode 100644 app/Services/Messaging/MessageRecipientService.php create mode 100644 app/Services/Navigation/NavBuilderService.php create mode 100644 app/Services/Navigation/NavbarService.php create mode 100644 app/Services/Phone/PhoneFormatterService.php create mode 100644 app/Services/Policy/PolicyContentService.php create mode 100644 app/Services/Preferences/PreferencesCommandService.php create mode 100644 app/Services/Preferences/PreferencesOptionsService.php create mode 100644 app/Services/Preferences/PreferencesQueryService.php create mode 100644 app/Services/Security/IpBanCommandService.php create mode 100644 app/Services/Security/IpBanQueryService.php create mode 100644 app/Services/Settings/SettingsService.php create mode 100644 app/Services/Staff/StaffCommandService.php create mode 100644 app/Services/Staff/StaffQueryService.php create mode 100644 app/Services/Support/ContactMessageService.php create mode 100644 app/Services/Support/SupportRequestService.php create mode 100644 app/Services/System/CleanupSchedulerService.php create mode 100644 app/Services/System/DatabaseHealthService.php create mode 100644 app/Services/System/HealthCheckService.php create mode 100644 app/Services/Ui/UiStyleService.php delete mode 100644 app/old/ApiClient.php delete mode 100644 app/old/ClassController.php delete mode 100644 app/old/ContactController.php delete mode 100644 app/old/DashboardRedirectController.php delete mode 100644 app/old/DocsController.php delete mode 100644 app/old/Filters/.gitkeep delete mode 100644 app/old/Filters/ApiAuthFilter.php delete mode 100644 app/old/Filters/ApiDocsAuthFilter.php delete mode 100644 app/old/Filters/AuthFilter.php delete mode 100644 app/old/Filters/CleanupScheduler.php delete mode 100644 app/old/Filters/PermissionFilter.php delete mode 100644 app/old/Filters/TimezoneFilter.php delete mode 100644 app/old/FrontendController.php delete mode 100644 app/old/HealthController.php delete mode 100644 app/old/InfoIconController.php delete mode 100644 app/old/IpBanController.php delete mode 100644 app/old/LandingPageController.php delete mode 100644 app/old/LateSlipLogsController.php delete mode 100644 app/old/MessagesController.php delete mode 100644 app/old/NavBuilderController.php delete mode 100644 app/old/NavbarService.php delete mode 100644 app/old/PageController.php delete mode 100644 app/old/PhoneFormatterService.php delete mode 100644 app/old/PolicyController.php delete mode 100644 app/old/PreferencesController.php delete mode 100644 app/old/RFIDController.php delete mode 100644 app/old/SendSMSController.php delete mode 100644 app/old/SettingsController.php delete mode 100644 app/old/StaffController.php delete mode 100644 app/old/StatsController.php delete mode 100644 app/old/SupportController.php delete mode 100644 app/old/TestDBController.php delete mode 100644 app/old/TimeService.php delete mode 100644 app/old/UiController.php create mode 100644 database/migrations/2026_03_11_205736_create_students_table.php create mode 100644 database/migrations/2026_03_11_205736_create_teachers_table.php create mode 100644 database/migrations/2026_03_11_205737_create_attendance_data_table.php create mode 100644 public/html/privacy_policy.html create mode 100644 public/html/terms_of_service.html create mode 100644 resources/policies/picture_policy.html create mode 100644 resources/policies/school_policy.html create mode 100644 tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php create mode 100644 tests/Feature/Api/V1/Auth/IpBanControllerTest.php delete mode 100644 tests/Feature/Api/V1/Auth/SessionTimeoutControllerTest.php create mode 100644 tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php create mode 100644 tests/Feature/Api/V1/Frontend/FrontendControllerTest.php create mode 100644 tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php create mode 100644 tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php create mode 100644 tests/Feature/Api/V1/Frontend/PageControllerTest.php create mode 100644 tests/Feature/Api/V1/Messaging/MessagesControllerTest.php create mode 100644 tests/Feature/Api/V1/Settings/PolicyControllerTest.php create mode 100644 tests/Feature/Api/V1/Settings/PreferencesControllerTest.php create mode 100644 tests/Feature/Api/V1/Settings/SettingsControllerTest.php create mode 100644 tests/Feature/Api/V1/Staff/StaffControllerTest.php create mode 100644 tests/Feature/Api/V1/Support/ContactControllerTest.php create mode 100644 tests/Feature/Api/V1/Support/SupportControllerTest.php create mode 100644 tests/Feature/Api/V1/System/DashboardControllerTest.php create mode 100644 tests/Feature/Api/V1/System/DatabaseHealthControllerTest.php create mode 100644 tests/Feature/Api/V1/System/HealthControllerTest.php create mode 100644 tests/Feature/Api/V1/System/NavBuilderControllerTest.php create mode 100644 tests/Feature/Api/V1/Ui/UiControllerTest.php create mode 100644 tests/Feature/Api/V1/Utilities/PhoneFormatterControllerTest.php create mode 100644 tests/Feature/Middleware/RequirePermissionMiddlewareTest.php create mode 100644 tests/Unit/Resources/Frontend/FrontendPageResourceTest.php create mode 100644 tests/Unit/Resources/Frontend/PageContentResourceTest.php create mode 100644 tests/Unit/Resources/Messaging/MessageResourceTest.php create mode 100644 tests/Unit/Resources/Messaging/RecipientResourceTest.php create mode 100644 tests/Unit/Resources/Settings/SettingsResourceTest.php create mode 100644 tests/Unit/Resources/Staff/StaffResourceTest.php create mode 100644 tests/Unit/Resources/Support/ContactMessageResourceTest.php create mode 100644 tests/Unit/Resources/Support/SupportRequestResourceTest.php create mode 100644 tests/Unit/Resources/Ui/UiStyleResourceTest.php create mode 100644 tests/Unit/Services/Attendance/LateSlipLogCommandServiceTest.php create mode 100644 tests/Unit/Services/Attendance/LateSlipLogQueryServiceTest.php delete mode 100644 tests/Unit/Services/Auth/SessionActivityServiceTest.php delete mode 100644 tests/Unit/Services/Auth/SessionTimeoutConfigServiceTest.php delete mode 100644 tests/Unit/Services/Auth/SessionTimeoutServiceTest.php create mode 100644 tests/Unit/Services/Auth/UserRoleServiceTest.php create mode 100644 tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentWriterServiceTest.php create mode 100644 tests/Unit/Services/ClassPreparation/ClassPreparationPrintServiceTest.php create mode 100644 tests/Unit/Services/ClassSections/ClassAttendanceServiceTest.php create mode 100644 tests/Unit/Services/ClassSections/ClassSectionCommandServiceTest.php create mode 100644 tests/Unit/Services/ClassSections/ClassSectionQueryServiceTest.php create mode 100644 tests/Unit/Services/ClassSections/ClassSectionSeedServiceTest.php create mode 100644 tests/Unit/Services/Dashboard/DashboardRouteServiceTest.php create mode 100644 tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php create mode 100644 tests/Unit/Services/Frontend/FrontendPageServiceTest.php create mode 100644 tests/Unit/Services/Frontend/LandingPageContextServiceTest.php create mode 100644 tests/Unit/Services/Frontend/LandingPageParentDashboardServiceTest.php create mode 100644 tests/Unit/Services/Frontend/LandingPageRoleServiceTest.php create mode 100644 tests/Unit/Services/Frontend/LandingPageServiceTest.php create mode 100644 tests/Unit/Services/Frontend/LandingPageTeacherSummaryServiceTest.php create mode 100644 tests/Unit/Services/Frontend/ProfileIconServiceTest.php create mode 100644 tests/Unit/Services/Frontend/StaticPageServiceTest.php create mode 100644 tests/Unit/Services/Messaging/MessageCommandServiceTest.php create mode 100644 tests/Unit/Services/Messaging/MessageQueryServiceTest.php create mode 100644 tests/Unit/Services/Messaging/MessageRecipientServiceTest.php create mode 100644 tests/Unit/Services/Navigation/NavBuilderServiceTest.php create mode 100644 tests/Unit/Services/Navigation/NavbarServiceTest.php create mode 100644 tests/Unit/Services/Phone/PhoneFormatterServiceTest.php create mode 100644 tests/Unit/Services/Policy/PolicyContentServiceTest.php create mode 100644 tests/Unit/Services/Preferences/PreferencesCommandServiceTest.php create mode 100644 tests/Unit/Services/Preferences/PreferencesOptionsServiceTest.php create mode 100644 tests/Unit/Services/Preferences/PreferencesQueryServiceTest.php create mode 100644 tests/Unit/Services/Security/IpBanCommandServiceTest.php create mode 100644 tests/Unit/Services/Security/IpBanQueryServiceTest.php create mode 100644 tests/Unit/Services/Settings/SettingsServiceTest.php create mode 100644 tests/Unit/Services/Staff/StaffCommandServiceTest.php create mode 100644 tests/Unit/Services/Staff/StaffQueryServiceTest.php create mode 100644 tests/Unit/Services/Support/ContactMessageServiceTest.php create mode 100644 tests/Unit/Services/Support/SupportRequestServiceTest.php create mode 100644 tests/Unit/Services/System/CleanupSchedulerServiceTest.php create mode 100644 tests/Unit/Services/System/HealthCheckServiceTest.php create mode 100644 tests/Unit/Services/Ui/UiStyleServiceTest.php diff --git a/app/Console/Commands/ImportUsers.php b/app/Console/Commands/ImportUsers.php new file mode 100644 index 00000000..0ad21819 --- /dev/null +++ b/app/Console/Commands/ImportUsers.php @@ -0,0 +1,30 @@ +authorize('viewAny', LateSlipLog::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 50); + + $logs = $this->queryService->paginate($filters, $page, $perPage); + $collection = new LateSlipLogCollection($logs); + + return $this->success([ + 'logs' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'], + ]); + } + + public function show(int $id): JsonResponse + { + $log = $this->queryService->find($id); + if (!$log) { + return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('view', $log); + + return $this->success([ + 'log' => new LateSlipLogResource($log), + ]); + } + + public function destroy(int $id): JsonResponse + { + $log = $this->queryService->find($id); + if (!$log) { + return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('delete', $log); + + try { + $deleted = $this->commandService->delete($log); + } catch (\Throwable $e) { + Log::error('Late slip log delete failed: ' . $e->getMessage()); + return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$deleted) { + return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Late slip log deleted.'); + } +} diff --git a/app/Http/Controllers/Api/Auth/IpBanController.php b/app/Http/Controllers/Api/Auth/IpBanController.php new file mode 100644 index 00000000..59dcf7ae --- /dev/null +++ b/app/Http/Controllers/Api/Auth/IpBanController.php @@ -0,0 +1,115 @@ +authorize('viewAny', IpAttempt::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $bans = $this->queryService->paginate($filters, $page, $perPage); + $collection = new IpBanCollection($bans); + + return $this->success([ + 'bans' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'], + ]); + } + + public function show(int $id): JsonResponse + { + $ban = $this->queryService->find($id); + if (!$ban) { + return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('view', $ban); + + return $this->success([ + 'ban' => new IpBanResource($ban), + ]); + } + + public function ban(IpBanRequest $request): JsonResponse + { + $this->authorize('create', IpAttempt::class); + + $validated = $request->validated(); + $id = $validated['id'] ?? null; + $ip = $validated['ip'] ?? null; + $hours = (int) ($validated['hours'] ?? 24); + + try { + $ban = $this->commandService->banNow($id ? (int) $id : null, $ip ? (string) $ip : null, $hours); + } catch (\Throwable $e) { + Log::error('IP ban failed: ' . $e->getMessage()); + return $this->error('Unable to ban IP.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$ban) { + return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'ban' => new IpBanResource($ban), + ], 'IP banned.'); + } + + public function unban(IpUnbanRequest $request): JsonResponse + { + $this->authorize('update', IpAttempt::class); + + $validated = $request->validated(); + $all = filter_var($validated['all'] ?? false, FILTER_VALIDATE_BOOLEAN); + + try { + if ($all) { + $count = $this->commandService->unbanAll(); + return $this->success([ + 'count' => $count, + ], $count . ' IP(s) unbanned.'); + } + + $ban = $this->commandService->unbanOne( + isset($validated['id']) ? (int) $validated['id'] : null, + $validated['ip'] ?? null + ); + } catch (\Throwable $e) { + Log::error('IP unban failed: ' . $e->getMessage()); + return $this->error('Unable to unban IP.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$ban) { + return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'ban' => new IpBanResource($ban), + ], 'IP unbanned.'); + } +} diff --git a/app/Http/Controllers/Api/Auth/SessionTimeoutController.php b/app/Http/Controllers/Api/Auth/SessionTimeoutController.php deleted file mode 100644 index 94a2744c..00000000 --- a/app/Http/Controllers/Api/Auth/SessionTimeoutController.php +++ /dev/null @@ -1,67 +0,0 @@ -configService->config(); - - return $this->success([ - 'config' => new SessionTimeoutConfigResource($config), - ]); - } - - public function check(SessionTimeoutCheckRequest $request): JsonResponse - { - $result = $this->timeoutService->checkTimeout(); - - if (($result['status'] ?? '') === 'expired') { - return $this->error( - $result['message'] ?? 'Session expired.', - Response::HTTP_UNAUTHORIZED, - (new SessionTimeoutStatusResource($result))->toArray($request) - ); - } - - return $this->success([ - 'session' => new SessionTimeoutStatusResource($result), - ]); - } - - public function ping(SessionTimeoutPingRequest $request): JsonResponse - { - $result = $this->timeoutService->pingActivity(); - - if (($result['status'] ?? '') === 'expired') { - return $this->error( - $result['message'] ?? 'Session expired.', - Response::HTTP_UNAUTHORIZED, - (new SessionTimeoutStatusResource($result))->toArray($request) - ); - } - - return $this->success([ - 'session' => new SessionTimeoutStatusResource($result), - ]); - } -} diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index c9d7adce..aa93b2ab 100755 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -229,7 +229,7 @@ class BaseApiController extends Controller protected function getCurrentUserId(): ?int { - $userId = (int) (session()->get('user_id') ?? 0); + $userId = (int) (auth()->id() ?? 0); return $userId > 0 ? $userId : null; } @@ -251,23 +251,20 @@ class BaseApiController extends Controller $name = $row['email'] ?? 'User #' . $userId; } - $roles = (array) (session()->get('roles') ?? []); - if (empty($roles)) { - try { - $roles = DB::table('user_roles ur') - ->select('LOWER(r.name) AS name') - ->join('roles r', 'r.id', '=', 'ur.role_id') - ->where('ur.user_id', $userId) - ->whereNull('ur.deleted_at') - ->pluck('name') - ->map(fn($name) => strtolower((string) $name)) - ->unique() - ->values() - ->toArray(); - } catch (\Throwable $e) { - Log::error('Unable to load user roles: ' . $e->getMessage()); - $roles = []; - } + try { + $roles = DB::table('user_roles ur') + ->select('LOWER(r.name) AS name') + ->join('roles r', 'r.id', '=', 'ur.role_id') + ->where('ur.user_id', $userId) + ->whereNull('ur.deleted_at') + ->pluck('name') + ->map(fn($name) => strtolower((string) $name)) + ->unique() + ->values() + ->toArray(); + } catch (\Throwable $e) { + Log::error('Unable to load user roles: ' . $e->getMessage()); + $roles = []; } return (object) [ diff --git a/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php b/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php index 584ff75d..da68969b 100644 --- a/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php +++ b/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php @@ -3,9 +3,16 @@ namespace App\Http\Controllers\Api\ClassPreparation; use App\Http\Controllers\Api\BaseApiController; +use App\Http\Requests\ClassPreparation\ClassPreparationAdjustmentRequest; +use App\Http\Requests\ClassPreparation\ClassPreparationIndexRequest; +use App\Http\Requests\ClassPreparation\ClassPreparationMarkPrintedRequest; +use App\Http\Requests\ClassPreparation\ClassPreparationPrintRequest; +use App\Http\Resources\ClassPreparation\ClassPreparationPrintResource; +use App\Http\Resources\ClassPreparation\ClassPreparationResultResource; use App\Services\ClassPreparation\ClassPreparationService; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response; class ClassPreparationController extends BaseApiController { @@ -17,7 +24,7 @@ class ClassPreparationController extends BaseApiController $this->service = $service; } - public function index(Request $request): JsonResponse + public function index(ClassPreparationIndexRequest $request): JsonResponse { $schoolYear = (string) ($request->query('school_year') ?? ''); $semester = (string) ($request->query('semester') ?? ''); @@ -27,34 +34,63 @@ class ClassPreparationController extends BaseApiController $semester !== '' ? $semester : null ); - return response()->json([ - 'ok' => true, + return $this->success([ 'schoolYear' => $payload['schoolYear'], 'semester' => $payload['semester'], - 'results' => $payload['results'], + 'results' => ClassPreparationResultResource::collection($payload['results']), 'totals' => $payload['totals'], 'shortages' => $payload['shortages'], ]); } - public function markPrinted(Request $request): JsonResponse + public function markPrinted(ClassPreparationMarkPrintedRequest $request): JsonResponse { - $schoolYear = (string) ($request->input('school_year') ?? ''); - $semester = (string) ($request->input('semester') ?? ''); - $ids = $request->input('class_section_ids', []); - - if ($schoolYear === '' || !is_array($ids)) { - return response()->json([ - 'ok' => false, - 'message' => 'school_year and class_section_ids are required.', - ], 422); - } + $validated = $request->validated(); + $schoolYear = (string) ($validated['school_year'] ?? ''); + $semester = (string) ($validated['semester'] ?? ''); + $ids = $validated['class_section_ids'] ?? []; $count = $this->service->markPrinted($schoolYear, $semester, $ids); - return response()->json([ - 'ok' => true, + return $this->success([ 'updated' => $count, - ]); + ], 'Class preparation logs updated.'); + } + + public function saveAdjustments(ClassPreparationAdjustmentRequest $request): JsonResponse + { + $validated = $request->validated(); + $classSectionId = (string) $validated['class_section_id']; + $schoolYear = (string) ($validated['school_year'] ?? ''); + + try { + $count = $this->service->saveAdjustments($classSectionId, $schoolYear, $validated['adjustments']); + } catch (\Throwable $e) { + Log::error('Class prep adjustment save failed: ' . $e->getMessage()); + return $this->error('Unable to save adjustments.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'updated' => $count, + ], 'Adjustments saved.'); + } + + public function print(ClassPreparationPrintRequest $request): JsonResponse + { + $validated = $request->validated(); + + $payload = $this->service->printPrep( + (string) $validated['class_section_id'], + (string) $validated['school_year'], + (string) ($validated['semester'] ?? '') + ); + + if (empty($payload['ok'])) { + return $this->error('Unable to log class preparation print.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'print' => new ClassPreparationPrintResource($payload), + ], 'Class preparation logged.'); } } diff --git a/app/Http/Controllers/Api/Classes/ClassController.php b/app/Http/Controllers/Api/Classes/ClassController.php new file mode 100644 index 00000000..e5f50d02 --- /dev/null +++ b/app/Http/Controllers/Api/Classes/ClassController.php @@ -0,0 +1,136 @@ +authorize('viewAny', ClassSection::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $sections = $this->queryService->list($filters, $page, $perPage); + $collection = new ClassSectionCollection($sections); + + return $this->success([ + 'sections' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'], + ]); + } + + public function show(ClassSection $classSection): JsonResponse + { + $this->authorize('view', $classSection); + + $section = $this->queryService->find((int) $classSection->id); + + if (!$section) { + return $this->error('Class section not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'section' => new ClassSectionResource($section), + ]); + } + + public function store(ClassSectionStoreRequest $request): JsonResponse + { + $this->authorize('create', ClassSection::class); + + $section = $this->commandService->create($request->validated()); + + return $this->success([ + 'section' => new ClassSectionResource($section), + ], 'Class section created.', Response::HTTP_CREATED); + } + + public function update(ClassSectionUpdateRequest $request, ClassSection $classSection): JsonResponse + { + $this->authorize('update', $classSection); + + $section = $this->commandService->update($classSection, $request->validated()); + + return $this->success([ + 'section' => new ClassSectionResource($section), + ], 'Class section updated.'); + } + + public function destroy(ClassSection $classSection): JsonResponse + { + $this->authorize('delete', $classSection); + + if (!$this->commandService->delete($classSection)) { + return $this->error('Class section not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success(null, 'Class section deleted.'); + } + + public function attendance(Request $request, int $classSectionId): JsonResponse + { + $teacherId = (int) ($request->user()?->id ?? 0); + if ($teacherId <= 0) { + return $this->error('Missing teacher authentication.', Response::HTTP_UNAUTHORIZED); + } + + $payload = $this->attendanceService->getAttendancePayload( + $classSectionId, + $teacherId, + $request->query('school_year'), + $request->query('semester') + ); + + if (empty($payload['students'])) { + return $this->error('No data found for this class.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'attendance' => new ClassAttendanceResource($payload), + ]); + } + + public function seedDefaults(): JsonResponse + { + $this->authorize('seedDefaults', ClassSection::class); + + try { + $created = $this->seedService->seedDefaults(); + } catch (\Throwable $e) { + Log::error('Seeding default classes failed: ' . $e->getMessage()); + return $this->error('Unable to seed default classes.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'created' => $created, + ], 'Default classes seeded.'); + } +} diff --git a/app/Http/Controllers/Api/Finance/PaypalPaymentController.php b/app/Http/Controllers/Api/Finance/PaypalPaymentController.php index cfaf9530..f9a93879 100644 --- a/app/Http/Controllers/Api/Finance/PaypalPaymentController.php +++ b/app/Http/Controllers/Api/Finance/PaypalPaymentController.php @@ -34,7 +34,8 @@ class PaypalPaymentController extends BaseApiController $payload = $request->validated(); $this->service->executePayment( (string) $payload['payer_id'], - $payload['paypal_payment_id'] ?? null + $payload['paypal_payment_id'] ?? null, + isset($payload['payment_id']) ? (int) $payload['payment_id'] : null ); return response()->json(['ok' => true]); diff --git a/app/Http/Controllers/Api/Frontend/FrontendController.php b/app/Http/Controllers/Api/Frontend/FrontendController.php new file mode 100644 index 00000000..e1ce8cb7 --- /dev/null +++ b/app/Http/Controllers/Api/Frontend/FrontendController.php @@ -0,0 +1,72 @@ +success([ + 'page' => new FrontendPageResource($this->pageService->page('index')), + ]); + } + + public function facility(): JsonResponse + { + return $this->success([ + 'page' => new FrontendPageResource($this->pageService->page('facility')), + ]); + } + + public function team(): JsonResponse + { + return $this->success([ + 'page' => new FrontendPageResource($this->pageService->page('team')), + ]); + } + + public function callToAction(): JsonResponse + { + return $this->success([ + 'page' => new FrontendPageResource($this->pageService->page('call_to_action')), + ]); + } + + public function testimonial(): JsonResponse + { + return $this->success([ + 'page' => new FrontendPageResource($this->pageService->page('testimonial')), + ]); + } + + public function notFound(): JsonResponse + { + return $this->success([ + 'page' => new FrontendPageResource($this->pageService->page('not_found')), + ]); + } + + public function fetchUser(): JsonResponse + { + $user = auth()->user(); + if (!$user) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + return $this->success([ + 'user' => new FrontendUserResource($user), + ]); + } +} diff --git a/app/Http/Controllers/Api/Frontend/InfoIconController.php b/app/Http/Controllers/Api/Frontend/InfoIconController.php new file mode 100644 index 00000000..45acf53a --- /dev/null +++ b/app/Http/Controllers/Api/Frontend/InfoIconController.php @@ -0,0 +1,31 @@ +id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $payload = $this->service->buildForUser($userId); + + return $this->success([ + 'profile' => new ProfileIconResource($payload), + ]); + } +} diff --git a/app/Http/Controllers/Api/Frontend/LandingPageController.php b/app/Http/Controllers/Api/Frontend/LandingPageController.php new file mode 100644 index 00000000..121d3822 --- /dev/null +++ b/app/Http/Controllers/Api/Frontend/LandingPageController.php @@ -0,0 +1,86 @@ +service->dashboardForUser( + $request->user(), + $request->validated()['class_section_id'] ?? null + ); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function teacher(LandingTeacherRequest $request): JsonResponse + { + $payload = $this->service->teacher( + $request->user(), + $request->validated()['class_section_id'] ?? null + ); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function parent(): JsonResponse + { + $payload = $this->service->parent(auth()->user()); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function administrator(): JsonResponse + { + $payload = $this->service->administrator(); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function admin(): JsonResponse + { + $payload = $this->service->admin(); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function student(): JsonResponse + { + $payload = $this->service->student(); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } + + public function guest(): JsonResponse + { + $payload = $this->service->guest(); + + return $this->success([ + 'landing' => new LandingPageResource($payload), + ]); + } +} diff --git a/app/Http/Controllers/Api/Frontend/PageController.php b/app/Http/Controllers/Api/Frontend/PageController.php new file mode 100644 index 00000000..86cf6a2c --- /dev/null +++ b/app/Http/Controllers/Api/Frontend/PageController.php @@ -0,0 +1,62 @@ +renderStatic('privacy_policy.html', 'privacy_policy'); + } + + public function termsOfService(): JsonResponse + { + return $this->renderStatic('terms_of_service.html', 'terms_of_service'); + } + + public function helpCenter(): JsonResponse + { + return $this->renderStatic('help_center.html', 'help_center'); + } + + public function submitContact(ContactSubmitRequest $request): JsonResponse + { + $result = $this->contactService->submit($request->validated()); + + return $this->success([ + 'submission' => new ContactSubmissionResource([ + 'email' => $request->validated('email'), + 'email_sent' => $result['email_sent'], + 'contact_id' => $result['record']?->id, + ]), + ], 'Message received.', Response::HTTP_CREATED); + } + + private function renderStatic(string $filename, string $type): JsonResponse + { + $result = $this->pageService->load($filename, $type); + if (!$result['ok']) { + return $this->error($result['error'], Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'page' => new PageContentResource($result), + ]); + } +} diff --git a/app/Http/Controllers/Api/Messaging/MessagesController.php b/app/Http/Controllers/Api/Messaging/MessagesController.php new file mode 100644 index 00000000..f9d51ba0 --- /dev/null +++ b/app/Http/Controllers/Api/Messaging/MessagesController.php @@ -0,0 +1,222 @@ +id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $this->authorize('viewAny', Message::class); + + $role = $this->queryService->getUserRoleName($userId); + $messages = $this->queryService->inbox($userId, $filters, $page, $perPage); + $collection = new MessageCollection($messages); + $meta = $collection->with($request)['meta'] ?? null; + + return $this->success([ + 'role' => $role, + 'messages' => $collection->toArray($request), + 'meta' => $meta, + ]); + } + + public function inbox(MessageIndexRequest $request): JsonResponse + { + return $this->listMessages($request, 'inbox'); + } + + public function sent(MessageIndexRequest $request): JsonResponse + { + return $this->listMessages($request, 'sent'); + } + + public function drafts(MessageIndexRequest $request): JsonResponse + { + return $this->listMessages($request, 'drafts'); + } + + public function trash(MessageIndexRequest $request): JsonResponse + { + return $this->listMessages($request, 'trash'); + } + + public function show(int $id): JsonResponse + { + $message = $this->queryService->findForUser($id); + if (!$message) { + return $this->error('Message not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('view', $message); + + $userId = (int) (auth()->id() ?? 0); + if ($message->recipient_id === $userId) { + $this->commandService->markRead($message); + $message->refresh(); + } + + return $this->success([ + 'message' => new MessageResource($message), + ]); + } + + public function store(MessageSendRequest $request): JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $this->authorize('create', Message::class); + + try { + $message = $this->commandService->create($userId, $request->validated(), $request->file('attachment')); + } catch (\Throwable $e) { + Log::error('Message send failed: ' . $e->getMessage()); + return $this->error($e->getMessage(), Response::HTTP_BAD_REQUEST); + } + + return $this->success([ + 'message' => new MessageResource($message->fresh(['sender', 'recipient'])), + ], 'Message sent.', Response::HTTP_CREATED); + } + + public function receive(): JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $messages = $this->queryService->receivedAndMarkRead($userId); + + return $this->success([ + 'messages' => (new MessageCollection(collect($messages)))->toArray(request()), + ]); + } + + public function recipients(string $type): JsonResponse + { + $type = strtolower(trim($type)); + $data = match ($type) { + 'teacher' => $this->recipientService->teachers(), + 'parent' => $this->recipientService->parents(), + default => null, + }; + + if ($data === null) { + return $this->error('Invalid recipient type.', Response::HTTP_BAD_REQUEST); + } + + $payload = array_map(fn ($row) => (new RecipientResource($row))->toArray(request()), $data); + + return $this->success([ + 'recipients' => $payload, + ]); + } + + public function update(MessageUpdateRequest $request, int $id): JsonResponse + { + $message = Message::query()->find($id); + if (!$message) { + return $this->error('Message not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('update', $message); + + try { + $updated = $this->commandService->update($message, $request->validated()); + } catch (\Throwable $e) { + Log::error('Message update failed: ' . $e->getMessage()); + return $this->error('Unable to update message.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'message' => new MessageResource($updated->load(['sender', 'recipient'])), + ], 'Message updated.'); + } + + public function destroy(int $id): JsonResponse + { + $message = Message::query()->find($id); + if (!$message) { + return $this->error('Message not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('delete', $message); + + try { + $trashed = $this->commandService->trash($message); + } catch (\Throwable $e) { + Log::error('Message delete failed: ' . $e->getMessage()); + return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$trashed) { + return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Message deleted.'); + } + + private function listMessages(MessageIndexRequest $request, string $bucket): JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $this->authorize('viewAny', Message::class); + + $paginator = match ($bucket) { + 'sent' => $this->queryService->sent($userId, $filters, $page, $perPage), + 'drafts' => $this->queryService->drafts($userId, $filters, $page, $perPage), + 'trash' => $this->queryService->trash($userId, $filters, $page, $perPage), + default => $this->queryService->inbox($userId, $filters, $page, $perPage), + }; + + $collection = new MessageCollection($paginator); + $meta = $collection->with($request)['meta'] ?? null; + + return $this->success([ + 'messages' => $collection->toArray($request), + 'meta' => $meta, + ]); + } +} diff --git a/app/Http/Controllers/Api/Settings/PolicyController.php b/app/Http/Controllers/Api/Settings/PolicyController.php new file mode 100644 index 00000000..6cb589a8 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/PolicyController.php @@ -0,0 +1,43 @@ +respondPolicy('school'); + } + + public function picture(PolicyShowRequest $request): JsonResponse + { + return $this->respondPolicy('picture'); + } + + private function respondPolicy(string $type): JsonResponse + { + try { + $policy = $this->service->getPolicy($type); + } catch (\InvalidArgumentException $e) { + return $this->error('Policy not found.', Response::HTTP_NOT_FOUND); + } catch (\Throwable $e) { + return $this->error('Unable to load policy.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'policy' => new PolicyResource($policy), + ]); + } +} diff --git a/app/Http/Controllers/Api/Settings/PreferencesController.php b/app/Http/Controllers/Api/Settings/PreferencesController.php new file mode 100644 index 00000000..bf6470aa --- /dev/null +++ b/app/Http/Controllers/Api/Settings/PreferencesController.php @@ -0,0 +1,139 @@ +authorize('viewAny', Preferences::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $rows = $this->queryService->paginate($filters, $page, $perPage); + $collection = new PreferencesCollection($rows); + + return $this->success([ + 'preferences' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'], + ]); + } + + public function show(): JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $payload = $this->queryService->getForUser($userId); + + return $this->success([ + 'preferences' => new PreferencesResource($payload['preferences']), + 'options' => $payload['options'], + ]); + } + + public function showForUser(int $userId): JsonResponse + { + $row = Preferences::query()->where('user_id', $userId)->first(); + if (!$row) { + return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('view', $row); + + $payload = $this->queryService->getForUser($userId); + + return $this->success([ + 'preferences' => new PreferencesResource($payload['preferences']), + 'options' => $payload['options'], + ]); + } + + public function store(PreferencesUpsertRequest $request): JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + try { + $saved = $this->commandService->upsert($userId, $request->validated()); + } catch (\Throwable $e) { + Log::error('Preferences save failed: ' . $e->getMessage()); + return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $payload = $this->queryService->getForUser($userId); + + return $this->success([ + 'preferences' => new PreferencesResource($payload['preferences']), + ], 'Preferences saved.', $saved->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK); + } + + public function updateForUser(PreferencesUpsertRequest $request, int $userId): JsonResponse + { + $row = Preferences::query()->where('user_id', $userId)->first(); + if ($row) { + $this->authorize('update', $row); + } + + try { + $saved = $this->commandService->upsert($userId, $request->validated()); + } catch (\Throwable $e) { + Log::error('Preferences update failed: ' . $e->getMessage()); + return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $payload = $this->queryService->getForUser($userId); + + return $this->success([ + 'preferences' => new PreferencesResource($payload['preferences']), + ], 'Preferences saved.', $saved->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK); + } + + public function destroy(int $userId): JsonResponse + { + $row = Preferences::query()->where('user_id', $userId)->first(); + if (!$row) { + return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('delete', $row); + + try { + $deleted = $this->commandService->delete($row); + } catch (\Throwable $e) { + Log::error('Preferences delete failed: ' . $e->getMessage()); + return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$deleted) { + return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Preferences deleted.'); + } +} diff --git a/app/Http/Controllers/Api/Settings/SettingsController.php b/app/Http/Controllers/Api/Settings/SettingsController.php new file mode 100644 index 00000000..fed17162 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/SettingsController.php @@ -0,0 +1,51 @@ +authorize('viewAny', Setting::class); + + return $this->success([ + 'settings' => new SettingsResource($this->settingsService->get()), + ]); + } + + public function update(SettingsUpdateRequest $request): JsonResponse + { + $settings = Setting::singleton(); + $this->authorize('update', $settings); + + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + try { + $updated = $this->settingsService->update($request->validated(), $userId); + } catch (\Throwable $e) { + Log::error('Settings update failed: ' . $e->getMessage()); + return $this->error('Unable to update settings.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'settings' => new SettingsResource($updated), + ], 'Settings updated.'); + } +} diff --git a/app/Http/Controllers/Api/Staff/StaffController.php b/app/Http/Controllers/Api/Staff/StaffController.php new file mode 100644 index 00000000..f0aa39aa --- /dev/null +++ b/app/Http/Controllers/Api/Staff/StaffController.php @@ -0,0 +1,121 @@ +authorize('viewAny', Staff::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $result = $this->queryService->paginate($filters, $page, $perPage); + $collection = new StaffCollection($result['paginator']); + + return $this->success([ + 'staff' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'] ?? null, + 'issues_count' => $result['issues_count'], + 'semester' => $result['semester'], + 'school_year' => $result['school_year'], + ]); + } + + public function show(int $id): JsonResponse + { + $staff = $this->queryService->find($id); + if (!$staff) { + return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('view', $staff); + $staff->school_id = \App\Models\User::getSchoolIdByUserId((int) ($staff->user_id ?? 0)); + + return $this->success([ + 'staff' => new StaffResource($staff), + ]); + } + + public function store(StaffStoreRequest $request): JsonResponse + { + $this->authorize('create', Staff::class); + + try { + $staff = $this->commandService->create($request->validated()); + } catch (\Throwable $e) { + Log::error('Staff create failed: ' . $e->getMessage()); + return $this->error($e->getMessage(), Response::HTTP_BAD_REQUEST); + } + + return $this->success([ + 'staff' => new StaffResource($staff), + ], 'Staff member added.', Response::HTTP_CREATED); + } + + public function update(StaffUpdateRequest $request, int $id): JsonResponse + { + $staff = $this->queryService->find($id); + if (!$staff) { + return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('update', $staff); + + try { + $updated = $this->commandService->update($staff, $request->validated()); + } catch (\Throwable $e) { + Log::error('Staff update failed: ' . $e->getMessage()); + return $this->error('Unable to update staff.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'staff' => new StaffResource($updated), + ], 'Staff member updated.'); + } + + public function destroy(int $id): JsonResponse + { + $staff = $this->queryService->find($id); + if (!$staff) { + return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); + } + + $this->authorize('delete', $staff); + + try { + $deleted = $this->commandService->delete($staff); + } catch (\Throwable $e) { + Log::error('Staff delete failed: ' . $e->getMessage()); + return $this->error('Unable to delete staff.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!$deleted) { + return $this->error('Unable to delete staff.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Staff member deleted.'); + } +} diff --git a/app/Http/Controllers/Api/Support/ContactController.php b/app/Http/Controllers/Api/Support/ContactController.php new file mode 100644 index 00000000..8f40429d --- /dev/null +++ b/app/Http/Controllers/Api/Support/ContactController.php @@ -0,0 +1,27 @@ +messageService->send($request->validated()); + + return $this->success([ + 'contact' => new ContactMessageResource($result), + ], 'Message sent.', Response::HTTP_CREATED); + } +} diff --git a/app/Http/Controllers/Api/Support/SupportController.php b/app/Http/Controllers/Api/Support/SupportController.php new file mode 100644 index 00000000..db7eb7d1 --- /dev/null +++ b/app/Http/Controllers/Api/Support/SupportController.php @@ -0,0 +1,83 @@ +id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $this->authorize('viewAny', SupportRequest::class); + + $filters = $request->validated(); + $page = (int) ($filters['page'] ?? 1); + $perPage = (int) ($filters['per_page'] ?? 20); + + $isAdmin = auth()->user()?->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->intersect(['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal']) + ->isNotEmpty(); + + $paginator = $this->supportService->list($userId, $filters, $page, $perPage, $isAdmin); + $collection = new SupportRequestCollection($paginator); + + return $this->success([ + 'requests' => $collection->toArray($request), + 'meta' => $collection->with($request)['meta'] ?? null, + ]); + } + + public function store(SupportRequestStoreRequest $request): JsonResponse + { + $user = auth()->user(); + if (!$user) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $this->authorize('create', SupportRequest::class); + + $payload = $request->validated(); + $payload['user_email'] = $user->email ?? null; + $payload['user_name'] = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')); + + try { + $result = $this->supportService->create($user->id, $payload); + } catch (\Throwable $e) { + Log::error('Support request failed: ' . $e->getMessage()); + return $this->error('Unable to submit support request.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'request' => new SupportRequestResource($result['record']), + 'email_sent' => $result['email_sent'], + ], 'Support request submitted.', Response::HTTP_CREATED); + } + + public function teacher(): JsonResponse + { + return $this->success([ + 'page' => 'teacher_support', + ]); + } +} diff --git a/app/Http/Controllers/Api/System/DashboardController.php b/app/Http/Controllers/Api/System/DashboardController.php new file mode 100644 index 00000000..e81e8fc2 --- /dev/null +++ b/app/Http/Controllers/Api/System/DashboardController.php @@ -0,0 +1,31 @@ +user(); + if (!$user) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $payload = $this->service->resolveForUser($user); + + return $this->success([ + 'dashboard' => new DashboardRouteResource($payload), + ]); + } +} diff --git a/app/Http/Controllers/Api/System/DatabaseHealthController.php b/app/Http/Controllers/Api/System/DatabaseHealthController.php new file mode 100644 index 00000000..7e30e288 --- /dev/null +++ b/app/Http/Controllers/Api/System/DatabaseHealthController.php @@ -0,0 +1,30 @@ +healthService->check(); + if (!$result['ok']) { + return $this->error('Database connection failed.', Response::HTTP_SERVICE_UNAVAILABLE, [ + 'error' => $result['error'], + ]); + } + + return $this->success([ + 'ok' => true, + ]); + } +} diff --git a/app/Http/Controllers/Api/System/HealthController.php b/app/Http/Controllers/Api/System/HealthController.php new file mode 100644 index 00000000..7c9ab1ef --- /dev/null +++ b/app/Http/Controllers/Api/System/HealthController.php @@ -0,0 +1,27 @@ +service->check(); + $status = $payload['ok'] ? Response::HTTP_OK : Response::HTTP_SERVICE_UNAVAILABLE; + + return $this->success([ + 'health' => new HealthStatusResource($payload), + ], 'Health check.', $status); + } +} diff --git a/app/Http/Controllers/Api/System/NavBuilderController.php b/app/Http/Controllers/Api/System/NavBuilderController.php new file mode 100644 index 00000000..44c7c474 --- /dev/null +++ b/app/Http/Controllers/Api/System/NavBuilderController.php @@ -0,0 +1,90 @@ +user(); + $roles = $user?->roles()->pluck('roles.name')->map(fn ($name) => strtolower((string) $name))->toArray() ?? []; + + $menu = $this->navbarService->getMenuForRoles($roles); + + return $this->success([ + 'items' => NavItemResource::collection($menu), + ]); + } + + public function data(): JsonResponse + { + $this->authorize('manage', NavItem::class); + + $payload = $this->builderService->getAdminPayload(); + + return $this->success([ + 'data' => new NavBuilderDataResource($payload), + ]); + } + + public function store(NavItemStoreRequest $request): JsonResponse + { + $this->authorize('manage', NavItem::class); + + $result = $this->builderService->save($request->validated()); + + if (empty($result['ok'])) { + return $this->error($result['message'] ?? 'Unable to save menu item.', Response::HTTP_UNPROCESSABLE_ENTITY); + } + + return $this->success([ + 'id' => $result['id'], + ], 'Menu saved.', Response::HTTP_CREATED); + } + + public function destroy(int $id): JsonResponse + { + $this->authorize('manage', NavItem::class); + + $deleted = $this->builderService->delete($id); + + if (!$deleted) { + return $this->error('Menu item not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success(null, 'Menu item deleted.'); + } + + public function reorder(NavItemReorderRequest $request): JsonResponse + { + $this->authorize('manage', NavItem::class); + + try { + $this->builderService->reorder($request->validated()['orders']); + } catch (\Throwable $e) { + Log::error('Nav reorder failed: ' . $e->getMessage()); + return $this->error('Unable to reorder items.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(['ok' => true], 'Menu reordered.'); + } +} diff --git a/app/Http/Controllers/Api/Ui/UiController.php b/app/Http/Controllers/Api/Ui/UiController.php new file mode 100644 index 00000000..f962200c --- /dev/null +++ b/app/Http/Controllers/Api/Ui/UiController.php @@ -0,0 +1,32 @@ +id() ?? 0); + if ($userId <= 0) { + return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); + } + + $prefs = $this->styleService->update($userId, $request->validated()); + + return $this->success([ + 'preferences' => new UiStyleResource($prefs), + ], 'Style updated.'); + } +} diff --git a/app/Http/Controllers/Api/Utilities/PhoneFormatterController.php b/app/Http/Controllers/Api/Utilities/PhoneFormatterController.php new file mode 100644 index 00000000..3c1014d1 --- /dev/null +++ b/app/Http/Controllers/Api/Utilities/PhoneFormatterController.php @@ -0,0 +1,36 @@ +validated()['number']; + $formatted = $this->service->format($raw); + + if ($formatted === null) { + return $this->error('Invalid phone number.', Response::HTTP_UNPROCESSABLE_ENTITY); + } + + return $this->success([ + 'phone' => new PhoneFormatResource([ + 'raw' => $raw, + 'formatted' => $formatted, + 'is_valid' => true, + ]), + ]); + } +} diff --git a/app/Http/Controllers/View/ClassController.php b/app/Http/Controllers/View/ClassController.php deleted file mode 100755 index 18df10ab..00000000 --- a/app/Http/Controllers/View/ClassController.php +++ /dev/null @@ -1,150 +0,0 @@ -classSection = $classSection; - $this->student = $student; - $this->teacher = $teacher; - $this->config = $config; - - $this->semester = $this->config->getConfig('semester'); - $this->schoolYear = $this->config->getConfig('school_year'); - } - - public function index(): View - { - $classSections = $this->classSection - ->newQuery() - ->orderBy('class_section_name', 'ASC') - ->get(); - - return view('administrator.class_section', [ - 'classsection' => $classSections, - ]); - } - - public function create(): View - { - return view('administrator.create_class'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->only(['name', 'description']); - // TODO: persist the payload once the backing model/table columns are finalized. - - return redirect()->to('/administrator/classes')->with([ - 'payload' => $data, - ]); - } - - public function edit(int $id): View - { - return view('administrator.edit_class', [ - 'class' => $this->classSection->find($id), - ]); - } - - public function updateClass(Request $request, int $id): RedirectResponse - { - $data = $request->only(['name', 'description']); - // TODO: persist the payload once the backing model/table columns are finalized. - - return redirect()->to('/administrator/classes')->with([ - 'payload' => $data, - 'id' => $id, - ]); - } - - public function destroyClass(int $id): RedirectResponse - { - // TODO: remove the record once the backing model/table columns are finalized. - - return redirect()->to('/administrator/classes')->with([ - 'deleted_id' => $id, - ]); - } - - public function addClasses(): RedirectResponse - { - $classes = [ - ['class_name' => 'Class 1', 'teacher_id' => 1, 'schedule' => 'Monday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 2', 'teacher_id' => 2, 'schedule' => 'Tuesday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 3', 'teacher_id' => 3, 'schedule' => 'Wednesday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 4', 'teacher_id' => 4, 'schedule' => 'Thursday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 5', 'teacher_id' => 5, 'schedule' => 'Friday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 6', 'teacher_id' => 6, 'schedule' => 'Monday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 7', 'teacher_id' => 7, 'schedule' => 'Tuesday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 8', 'teacher_id' => 8, 'schedule' => 'Wednesday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 9', 'teacher_id' => 9, 'schedule' => 'Thursday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Youth', 'teacher_id' => 10, 'schedule' => 'Friday 10:00 AM - 11:00 AM', 'capacity' => 30], - ]; - - DB::transaction(function () use ($classes) { - foreach ($classes as $class) { - DB::table('classes')->insert($class); - } - }); - - return redirect()->to('/parent/classes')->with('success', 'Classes added successfully.'); - } - - public function classAttendance(int $classSectionId): RedirectResponse|View - { - $teacherId = (int) (session()->get('user_id') ?? 0); - if ($teacherId <= 0) { - return redirect()->to('/teacher/classes')->with('error', 'Missing teacher session.'); - } - - $teacherData = $this->teacher->find($teacherId); - - $studentsData = $this->student->newQuery() - ->join('student_class', 'students.id', '=', 'student_class.student_id') - ->leftJoin('class_sections', 'class_sections.id', '=', 'student_class.class_section_id') - ->where('student_class.class_section_id', $classSectionId) - ->select('students.*', 'student_class.class_section_id', 'class_sections.class_section_name') - ->get() - ->toArray(); - - $className = $studentsData[0]['class_section_name'] ?? 'Class Name Not Available'; - - if (empty($teacherData) || empty($studentsData)) { - return redirect()->to('/teacher/classes')->with('error', 'No data found for this class.'); - } - $teacherFirst = $teacherData['teacher_first'] ?? $teacherData['firstname'] ?? ''; - $teacherLast = $teacherData['teacher_last'] ?? $teacherData['lastname'] ?? ''; - - return view('teacher.classes', [ - 'teacher_name' => trim($teacherFirst . ' ' . $teacherLast), - 'students' => $studentsData, - 'class_name' => $className, - 'semester' => $this->semester, - 'class_section_id' => $classSectionId, - ]); - } -} diff --git a/app/Http/Controllers/View/ClassPreparationController.php b/app/Http/Controllers/View/ClassPreparationController.php deleted file mode 100755 index 0b9bbd0d..00000000 --- a/app/Http/Controllers/View/ClassPreparationController.php +++ /dev/null @@ -1,545 +0,0 @@ -studentClass = $studentClass; - $this->prepLog = $prepLog; - $this->classSection = $classSection; - $this->config = $config; - $this->adjustment = $adjustment; - - $this->schoolYear = $this->config->getConfig('school_year'); - $this->semester = $this->config->getConfig('semester'); - } - - public function index(Request $request): View - { - $schoolYear = (string) ($request->query('school_year') ?? $this->schoolYear ?? ''); - $semParam = $request->query('semester'); - $semester = is_string($semParam) && $semParam !== '' ? $semParam : (string) ($this->semester ?? ''); - $allowed = $this->allowedPrepCategories; - - $classSections = $this->studentClass->newQuery() - ->selectRaw('class_section_id, COUNT(DISTINCT student_id) AS student_count') - ->where('school_year', $schoolYear) - ->where('semester', $semester) - ->groupBy('class_section_id') - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - $inventoryMap = $this->buildInventoryMap($schoolYear, $semester); - - $requiredTotals = array_fill_keys($allowed, 0); - $prepResults = []; - - foreach ($classSections as $section) { - $classSectionId = (string) ($section['class_section_id'] ?? ''); - $studentCount = (int) ($section['student_count'] ?? 0); - $classLevel = $this->getClassLevelBySection($classSectionId); - $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; - - $baseItems = $this->calculatePrepItems($studentCount, $classLevel, $classSectionId); - - $rawAdjustments = $this->adjustment->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('adjustable', 1) - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - $adjMap = ['Large Table' => 0, 'Small Table' => 0]; - - foreach ($rawAdjustments as $adjustment) { - $item = $adjustment['item_name'] ?? ''; - $delta = (int) ($adjustment['adjustment'] ?? 0); - - if (array_key_exists($item, $adjMap)) { - $adjMap[$item] = $delta; - } - - if (isset($baseItems[$item])) { - $baseItems[$item] = max(0, (int) $baseItems[$item] + $delta); - } elseif (in_array($item, $allowed, true)) { - $baseItems[$item] = max(0, $delta); - } - } - - foreach ($allowed as $category) { - $requiredTotals[$category] += (int) ($baseItems[$category] ?? 0); - } - - $oldSnap = $this->prepLog->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->orderBy('created_at', 'DESC') - ->first(); - - $oldPrep = $oldSnap ? json_decode($oldSnap['prep_data'] ?? '[]', true) : []; - $hasChanged = $this->hasPrepChanged($baseItems, $oldPrep); - - $prepResults[] = [ - 'class_section' => $className, - 'class_section_id' => $classSectionId, - 'student_count' => $studentCount, - 'class_level' => $classLevel, - 'prep_items' => $baseItems, - 'needs_print' => $hasChanged, - 'last_printed_at' => $oldSnap['created_at'] ?? null, - 'adjustments' => $adjMap, - ]; - } - - $shortages = []; - foreach ($requiredTotals as $item => $required) { - $available = (int) ($inventoryMap[$item] ?? 0); - if ($available < (int) $required) { - $shortages[$item] = (int) $required - $available; - } - } - - return view('class_prep/list', [ - 'prepResults' => $prepResults, - 'schoolYear' => $schoolYear, - 'semester' => $semester, - 'shortages' => $shortages, - 'totalNeeded' => $requiredTotals, - 'available' => $inventoryMap, - ]); - } - - public function saveAdjustment(Request $request): RedirectResponse - { - $data = $request->all(); - $sectionId = (string) ($data['class_section_id'] ?? ''); - $schoolYear = (string) ($data['school_year'] ?? $this->schoolYear ?? ''); - $adjustments = is_array($data['adjustments'] ?? null) ? $data['adjustments'] : []; - - foreach ($adjustments as $itemName => $adjustment) { - $row = $this->adjustment->newQuery() - ->where('class_section_id', $sectionId) - ->where('school_year', $schoolYear) - ->where('item_name', $itemName) - ->first(); - - if ($row) { - $this->adjustment->update($row['id'], [ - 'adjustment' => (int) $adjustment, - 'adjustable' => 1, - ]); - continue; - } - - $this->adjustment->insert([ - 'class_section_id' => $sectionId, - 'item_name' => $itemName, - 'adjustment' => (int) $adjustment, - 'school_year' => $schoolYear, - 'adjustable' => 1, - ]); - } - - return redirect()->to('/class-prep?school_year=' . urlencode($schoolYear)) - ->with('success', 'Adjustments saved successfully.'); - } - - public function print(string $classSectionId, string $schoolYear): View - { - $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; - - $studentRow = $this->studentClass->newQuery() - ->selectRaw('COUNT(DISTINCT student_id) AS cnt') - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('semester', $this->semester) - ->first(); - - $studentCount = (int) ($studentRow['cnt'] ?? 0); - $classLevel = $this->getClassLevelBySection($classSectionId); - $items = $this->calculatePrepItems($studentCount, $classLevel, $classSectionId); - - $rawAdjustments = $this->adjustment->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('adjustable', 1) - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - foreach ($rawAdjustments as $adjustment) { - $item = $adjustment['item_name'] ?? ''; - $delta = (int) ($adjustment['adjustment'] ?? 0); - if (isset($items[$item])) { - $items[$item] = max(0, (int) $items[$item] + $delta); - } - } - - $this->prepLog->insert([ - 'class_section_id' => $classSectionId, - 'class_section' => $className, - 'school_year' => $schoolYear, - 'prep_data' => json_encode($items), - 'created_at' => utc_now(), - ]); - - return view('class_prep/print', [ - 'classSectionId' => $classSectionId, - 'classSection' => $className, - 'schoolYear' => $schoolYear, - 'prepItems' => $items, - ]); - } - - public function apiList(Request $request): JsonResponse - { - $schoolYear = (string) ($request->query('school_year') ?? $this->schoolYear ?? ''); - $allowed = $this->allowedPrepCategories; - - $classSections = $this->studentClass->newQuery() - ->selectRaw('class_section_id, COUNT(DISTINCT student_id) AS student_count') - ->where('school_year', $schoolYear) - ->where('semester', $this->semester) - ->groupBy('class_section_id') - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - $inventoryMap = $this->buildInventoryMap($schoolYear, $this->semester); - - $requiredTotals = array_fill_keys($allowed, 0); - $results = []; - - foreach ($classSections as $row) { - $classSectionId = (string) ($row['class_section_id'] ?? ''); - $studentCount = (int) ($row['student_count'] ?? 0); - $classLevel = $this->getClassLevelBySection($classSectionId); - $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; - - $baseItems = $this->calculatePrepItems($studentCount, $classLevel, $classSectionId); - - $rawAdjustments = $this->adjustment->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('adjustable', 1) - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - $adjMap = ['Large Table' => 0, 'Small Table' => 0]; - - foreach ($rawAdjustments as $adjustment) { - $item = $adjustment['item_name'] ?? ''; - $delta = (int) ($adjustment['adjustment'] ?? 0); - if (array_key_exists($item, $adjMap)) { - $adjMap[$item] = $delta; - } - if (isset($baseItems[$item])) { - $baseItems[$item] = max(0, (int) $baseItems[$item] + $delta); - } - } - - foreach ($allowed as $cat) { - $requiredTotals[$cat] += (int) ($baseItems[$cat] ?? 0); - } - - $oldSnap = $this->prepLog->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->orderBy('created_at', 'DESC') - ->first(); - - $oldPrep = $oldSnap ? json_decode($oldSnap['prep_data'] ?? '[]', true) : []; - $hasChanged = $this->hasPrepChanged($baseItems, $oldPrep); - - $results[] = [ - 'class_section' => $className, - 'class_section_id' => $classSectionId, - 'student_count' => $studentCount, - 'class_level' => $classLevel, - 'prep_items' => $baseItems, - 'needs_print' => $hasChanged, - 'last_printed_at' => $oldSnap['created_at'] ?? null, - 'adjustments' => $adjMap, - ]; - } - - $shortages = []; - foreach ($requiredTotals as $item => $required) { - $available = (int) ($inventoryMap[$item] ?? 0); - if ($available < (int) $required) { - $shortages[$item] = (int) $required - $available; - } - } - - return response()->json([ - 'ok' => true, - 'schoolYear' => $schoolYear, - 'results' => $results, - 'totals' => $requiredTotals, - 'shortages' => $shortages, - 'csrf_token' => csrf_token(), - 'csrf_hash' => csrf_token(), - ]); - } - - public function apiMarkPrinted(Request $request): JsonResponse - { - $schoolYear = (string) ($request->post('school_year') ?? $this->schoolYear ?? ''); - $ids = $request->post('class_section_ids', []); - if (!is_array($ids)) { - $ids = $ids ? [$ids] : []; - } - $ids = array_values(array_unique(array_filter(array_map('strval', $ids)))); - - $count = 0; - foreach ($ids as $classSectionId) { - $studentRow = $this->studentClass->newQuery() - ->selectRaw('COUNT(DISTINCT student_id) AS cnt') - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('semester', $this->semester) - ->first(); - - $studentCount = (int) ($studentRow['cnt'] ?? 0); - $classLevel = $this->getClassLevelBySection($classSectionId); - $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; - $items = $this->calculatePrepItems($studentCount, $classLevel, $classSectionId); - - $rawAdjustments = $this->adjustment->newQuery() - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear) - ->where('adjustable', 1) - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - foreach ($rawAdjustments as $adjustment) { - $item = $adjustment['item_name'] ?? ''; - $delta = (int) ($adjustment['adjustment'] ?? 0); - if (isset($items[$item])) { - $items[$item] = max(0, (int) $items[$item] + $delta); - } - } - - try { - $this->prepLog->insert([ - 'class_section_id' => $classSectionId, - 'class_section' => $className, - 'school_year' => $schoolYear, - 'prep_data' => json_encode($items), - 'created_at' => utc_now(), - ]); - $count++; - } catch (\Throwable $e) { - // ignore and continue to next section - } - } - - return response()->json([ - 'ok' => true, - 'updated' => $count, - 'csrf_token' => csrf_token(), - 'csrf_hash' => csrf_token(), - ]); - } - - private function getTeacherCountForSection(string $classSectionId, ?string $schoolYear = null): int - { - $schoolYear = $schoolYear ?? $this->schoolYear; - - $row = DB::table('teacher_class') - ->selectRaw('COUNT(*) AS cnt') - ->where('class_section_id', $classSectionId) - ->whereIn('position', ['main', 'ta']) - ->where('school_year', $schoolYear) - ->first(); - - return (int) ($row->cnt ?? 0); - } - - private function calculatePrepItems(int $students, int $classLevel, string $classSectionId): array - { - $allowed = $this->allowedPrepCategories; - $items = array_fill_keys($allowed, 0); - - $categories = DB::table('inventory_categories') - ->select('name', 'grade_min', 'grade_max') - ->where('type', 'classroom') - ->whereIn('name', $allowed) - ->orderBy('name') - ->get() - ->map(fn ($row) => (array) $row) - ->toArray(); - - $isLowerGrades = in_array($classLevel, [1, 2], true); - $teacherCount = $this->getTeacherCountForSection($classSectionId); - - foreach ($categories as $category) { - $name = $category['name'] ?? ''; - $gradeMin = $category['grade_min']; - $gradeMax = $category['grade_max']; - - if ($gradeMin !== null && $classLevel < (int) $gradeMin) { - $items[$name] = 0; - continue; - } - if ($gradeMax !== null && $classLevel > (int) $gradeMax) { - $items[$name] = 0; - continue; - } - - $qty = 0; - switch (strtolower($name)) { - case 'small table': - $qty = $isLowerGrades ? (int) ceil($students / 3) : 0; - break; - case 'large table': - $qty = $isLowerGrades ? 0 : (int) ceil($students / 4); - break; - case 'small chair': - $qty = $isLowerGrades ? $students : 0; - break; - case 'regular chair': - $qty = $isLowerGrades ? 0 : $students; - break; - case 'teacher chair': - $qty = $teacherCount; - break; - case 'trash bin': - case 'white board': - case 'grade box': - $qty = 1; - break; - default: - $qty = 0; - } - - if (array_key_exists($name, $items)) { - $items[$name] = $qty; - } - } - - return $items; - } - - private function hasPrepChanged(array $new, array $old): bool - { - ksort($new); - ksort($old); - - return json_encode($new) !== json_encode($old); - } - - private function buildInventoryMap(string $schoolYear, string $semester): array - { - $allowed = $this->allowedPrepCategories; - $inventoryMap = array_fill_keys($allowed, 0); - - $queries = [ - DB::table('inventory_items as ii') - ->selectRaw('ic.name AS item_name, COALESCE(SUM(ii.good_qty),0) AS available') - ->join('inventory_categories as ic', 'ic.id', '=', 'ii.category_id') - ->where('ii.type', 'classroom') - ->where('ii.school_year', $schoolYear) - ->where('ii.semester', $semester) - ->whereIn('ic.name', $allowed) - ->groupBy('ic.name'), - DB::table('inventory_items') - ->selectRaw('name AS item_name, COALESCE(SUM(good_qty),0) AS available') - ->where('type', 'classroom') - ->where('school_year', $schoolYear) - ->where('semester', $semester) - ->whereIn('name', $allowed) - ->groupBy('name'), - DB::table('inventory_items as ii') - ->selectRaw('ic.name AS item_name, COALESCE(SUM(CASE WHEN ii.`condition`="good" THEN ii.quantity ELSE 0 END),0) AS available') - ->join('inventory_categories as ic', 'ic.id', '=', 'ii.category_id') - ->where('ii.type', 'classroom') - ->where('ii.school_year', $schoolYear) - ->where('ii.semester', $semester) - ->whereIn('ic.name', $allowed) - ->groupBy('ic.name'), - DB::table('inventory_items') - ->selectRaw('name AS item_name, COALESCE(SUM(CASE WHEN `condition`="good" THEN quantity ELSE 0 END),0) AS available') - ->where('type', 'classroom') - ->where('school_year', $schoolYear) - ->where('semester', $semester) - ->whereIn('name', $allowed) - ->groupBy('name'), - ]; - - foreach ($queries as $query) { - foreach ($query->get()->toArray() as $row) { - $row = (array) $row; - $name = (string) ($row['item_name'] ?? $row['name'] ?? ''); - $val = (int) ($row['available'] ?? 0); - - if ($name !== '' && isset($inventoryMap[$name])) { - $inventoryMap[$name] = max($inventoryMap[$name], $val); - } - } - } - - return $inventoryMap; - } - - private function getClassLevelBySection(string $classSectionId): int - { - if (preg_match('/^(kg|k)(\b|[^a-z0-9])/i', $classSectionId)) { - return 1; - } - - $numeric = (int) preg_replace('/\D/', '', $classSectionId); - if ($numeric > 0 && $numeric < 30) { - $firstDigit = (int) substr((string) $numeric, 0, 1); - return match ($firstDigit) { - 1 => 1, - 2 => 2, - default => 3, - }; - } - - return 3; - } -} diff --git a/app/Http/Controllers/View/DashboardRedirectController.php b/app/Http/Controllers/View/DashboardRedirectController.php deleted file mode 100755 index a05e138c..00000000 --- a/app/Http/Controllers/View/DashboardRedirectController.php +++ /dev/null @@ -1,45 +0,0 @@ -get('roles') ?? []; - - if (empty($roles) && $session->has('role')) { - $roles = [$session->get('role')]; - } - - return $this->redirectToDashboard($roles); - } - - private function redirectToDashboard(array $roles): RedirectResponse - { - if (empty($roles)) { - log_message('error', 'Empty roles array passed to redirectToDashboard.'); - return redirect()->to('/landing_page/guest_dashboard'); - } - - $rows = $this->role->findByNamesOrSlugs($roles); - - if (!empty($rows)) { - $route = $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard'; - log_message('debug', 'Redirecting user to: ' . $route); - return redirect()->to($route); - } - - log_message('warning', 'No matching role found. Redirecting to guest dashboard.'); - return redirect()->to('/landing_page/guest_dashboard'); - } -} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 405c9887..00f76766 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -35,8 +35,6 @@ class Kernel extends HttpKernel 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], diff --git a/app/Http/Middleware/ApiDocsAuth.php b/app/Http/Middleware/ApiDocsAuth.php new file mode 100644 index 00000000..21e7fae4 --- /dev/null +++ b/app/Http/Middleware/ApiDocsAuth.php @@ -0,0 +1,65 @@ +header('Authorization'); + if ($authHeader !== '' && stripos($authHeader, 'Bearer ') === 0) { + $token = trim(substr($authHeader, 7)); + if ($token === '') { + return $this->deny('Missing token.', 401); + } + + try { + $user = JWTAuth::setToken($token)->authenticate(); + if ($user) { + Auth::setUser($user); + } + } catch (JWTException $e) { + $user = null; + } + } + } + + if (!$user) { + return $this->deny('Unauthorized access to documentation.', 401); + } + + if (!$this->userIsAdmin((int) $user->id)) { + return $this->deny('Admin privileges required.', 403); + } + + return $next($request); + } + + private function userIsAdmin(int $userId): bool + { + return DB::table('user_roles as ur') + ->join('roles as r', 'r.id', '=', 'ur.role_id') + ->where('ur.user_id', $userId) + ->whereNull('ur.deleted_at') + ->whereRaw('LOWER(r.name) = ?', ['admin']) + ->exists(); + } + + private function deny(string $message, int $status): Response + { + return response()->json([ + 'status' => false, + 'message' => $message, + ], $status); + } +} diff --git a/app/Http/Middleware/ApiJwtAuth.php b/app/Http/Middleware/ApiJwtAuth.php new file mode 100644 index 00000000..f6f2d6c1 --- /dev/null +++ b/app/Http/Middleware/ApiJwtAuth.php @@ -0,0 +1,29 @@ +authenticate(); + if (!$user) { + return response()->json(['status' => false, 'message' => 'Unauthorized.'], 401); + } + + Auth::setUser($user); + } catch (JWTException $e) { + return response()->json(['status' => false, 'message' => 'Unauthorized.'], 401); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CleanupScheduler.php b/app/Http/Middleware/CleanupScheduler.php new file mode 100644 index 00000000..f2fe3ac1 --- /dev/null +++ b/app/Http/Middleware/CleanupScheduler.php @@ -0,0 +1,26 @@ +service->shouldRun($cacheKey, 2)) { + $this->service->runUnverifiedCleanup(); + $this->service->markRun($cacheKey); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/PrimeTimezone.php b/app/Http/Middleware/PrimeTimezone.php new file mode 100644 index 00000000..34811419 --- /dev/null +++ b/app/Http/Middleware/PrimeTimezone.php @@ -0,0 +1,26 @@ +timeService->primeFromRequest($request); + } catch (\Throwable $e) { + // ignore; fallback detection still works + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RequirePermission.php b/app/Http/Middleware/RequirePermission.php new file mode 100644 index 00000000..bd5ae72b --- /dev/null +++ b/app/Http/Middleware/RequirePermission.php @@ -0,0 +1,32 @@ +user()?->id ?? 0); + if ($userId <= 0) { + return response()->json(['status' => false, 'message' => 'Unauthorized.'], 401); + } + + if (!$this->permissions->hasPermission($userId, $permission)) { + if ($request->expectsJson()) { + return response()->json(['status' => false, 'message' => 'Access denied.'], 403); + } + return response()->json(['status' => false, 'message' => 'Access denied.'], 403); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Attendance/LateSlipLogIndexRequest.php b/app/Http/Requests/Attendance/LateSlipLogIndexRequest.php new file mode 100644 index 00000000..9292582e --- /dev/null +++ b/app/Http/Requests/Attendance/LateSlipLogIndexRequest.php @@ -0,0 +1,28 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'school_year' => ['nullable', 'string', 'max:9'], + 'semester' => ['nullable', 'string', 'max:255'], + 'q' => ['nullable', 'string', 'max:120'], + 'date_from' => ['nullable', 'date'], + 'date_to' => ['nullable', 'date'], + 'sort_by' => ['nullable', 'string', 'in:id,slip_date,student_name,printed_at,updated_at'], + 'sort_dir' => ['nullable', 'string', 'in:asc,desc'], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/Auth/SessionTimeoutCheckRequest.php b/app/Http/Requests/Auth/SessionTimeoutCheckRequest.php deleted file mode 100644 index 1ef812e3..00000000 --- a/app/Http/Requests/Auth/SessionTimeoutCheckRequest.php +++ /dev/null @@ -1,18 +0,0 @@ -check(); - } - - public function rules(): array - { - return []; - } -} diff --git a/app/Http/Requests/Auth/SessionTimeoutConfigRequest.php b/app/Http/Requests/Auth/SessionTimeoutConfigRequest.php deleted file mode 100644 index a178fb64..00000000 --- a/app/Http/Requests/Auth/SessionTimeoutConfigRequest.php +++ /dev/null @@ -1,18 +0,0 @@ -check(); - } - - public function rules(): array - { - return []; - } -} diff --git a/app/Http/Requests/Auth/SessionTimeoutPingRequest.php b/app/Http/Requests/Auth/SessionTimeoutPingRequest.php deleted file mode 100644 index e6ee51bf..00000000 --- a/app/Http/Requests/Auth/SessionTimeoutPingRequest.php +++ /dev/null @@ -1,18 +0,0 @@ -check(); - } - - public function rules(): array - { - return []; - } -} diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php new file mode 100644 index 00000000..1b4b2ad2 --- /dev/null +++ b/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php @@ -0,0 +1,23 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'class_section_id' => ['required', 'string', 'max:32'], + 'school_year' => ['nullable', 'string', 'max:9'], + 'adjustments' => ['required', 'array'], + 'adjustments.*' => ['required', 'integer'], + ]; + } +} diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php new file mode 100644 index 00000000..25892303 --- /dev/null +++ b/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php @@ -0,0 +1,21 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'school_year' => ['nullable', 'string', 'max:9'], + 'semester' => ['nullable', 'string', 'max:255'], + ]; + } +} diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php new file mode 100644 index 00000000..1c06d114 --- /dev/null +++ b/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php @@ -0,0 +1,23 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'school_year' => ['required', 'string', 'max:9'], + 'semester' => ['nullable', 'string', 'max:255'], + 'class_section_ids' => ['required', 'array'], + 'class_section_ids.*' => ['required', 'string', 'max:32'], + ]; + } +} diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php new file mode 100644 index 00000000..4459fa63 --- /dev/null +++ b/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php @@ -0,0 +1,32 @@ +user() !== null; + } + + protected function prepareForValidation(): void + { + parent::prepareForValidation(); + + $this->merge([ + 'class_section_id' => $this->route('classSectionId'), + 'school_year' => $this->route('schoolYear'), + ]); + } + + public function rules(): array + { + return [ + 'class_section_id' => ['required', 'string', 'max:32'], + 'school_year' => ['required', 'string', 'max:9'], + 'semester' => ['nullable', 'string', 'max:255'], + ]; + } +} diff --git a/app/Http/Requests/Classes/ClassSectionIndexRequest.php b/app/Http/Requests/Classes/ClassSectionIndexRequest.php new file mode 100644 index 00000000..a9ec26c5 --- /dev/null +++ b/app/Http/Requests/Classes/ClassSectionIndexRequest.php @@ -0,0 +1,27 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'search' => ['nullable', 'string', 'max:120'], + 'class_id' => ['nullable', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:9'], + 'semester' => ['nullable', 'string', 'max:255'], + 'sort_by' => ['nullable', 'string', 'in:class_section_name,class_section_id,class_id,school_year,semester,class_name'], + 'sort_dir' => ['nullable', 'string', 'in:asc,desc'], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/Classes/ClassSectionStoreRequest.php b/app/Http/Requests/Classes/ClassSectionStoreRequest.php new file mode 100644 index 00000000..9d9d84bb --- /dev/null +++ b/app/Http/Requests/Classes/ClassSectionStoreRequest.php @@ -0,0 +1,25 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'class_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['required', 'integer', 'min:1', Rule::unique('classSection', 'class_section_id')], + 'class_section_name' => ['required', 'string', 'max:100'], + 'semester' => ['required', 'string', 'max:255'], + 'school_year' => ['required', 'string', 'max:9'], + ]; + } +} diff --git a/app/Http/Requests/Classes/ClassSectionUpdateRequest.php b/app/Http/Requests/Classes/ClassSectionUpdateRequest.php new file mode 100644 index 00000000..50b0778c --- /dev/null +++ b/app/Http/Requests/Classes/ClassSectionUpdateRequest.php @@ -0,0 +1,33 @@ +user() !== null; + } + + public function rules(): array + { + $section = $this->route('classSection'); + $sectionId = is_object($section) ? $section->id : $section; + + return [ + 'class_id' => ['sometimes', 'integer', 'min:1'], + 'class_section_id' => [ + 'sometimes', + 'integer', + 'min:1', + Rule::unique('classSection', 'class_section_id')->ignore($sectionId), + ], + 'class_section_name' => ['sometimes', 'string', 'max:100'], + 'semester' => ['sometimes', 'string', 'max:255'], + 'school_year' => ['sometimes', 'string', 'max:9'], + ]; + } +} diff --git a/app/Http/Requests/Frontend/ContactSubmitRequest.php b/app/Http/Requests/Frontend/ContactSubmitRequest.php new file mode 100644 index 00000000..13216d70 --- /dev/null +++ b/app/Http/Requests/Frontend/ContactSubmitRequest.php @@ -0,0 +1,17 @@ + ['required', 'email', 'max:255'], + 'message' => ['required', 'string', 'min:10'], + 'subject' => ['nullable', 'string', 'max:255'], + ]; + } +} diff --git a/app/Http/Requests/Frontend/LandingTeacherRequest.php b/app/Http/Requests/Frontend/LandingTeacherRequest.php new file mode 100644 index 00000000..d226ac2e --- /dev/null +++ b/app/Http/Requests/Frontend/LandingTeacherRequest.php @@ -0,0 +1,20 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'class_section_id' => ['nullable', 'integer', 'min:1'], + ]; + } +} diff --git a/app/Http/Requests/Messaging/MessageIndexRequest.php b/app/Http/Requests/Messaging/MessageIndexRequest.php new file mode 100644 index 00000000..7e3fe2c6 --- /dev/null +++ b/app/Http/Requests/Messaging/MessageIndexRequest.php @@ -0,0 +1,21 @@ + ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], + 'priority' => ['nullable', 'in:low,normal,high'], + 'read_status' => ['nullable', 'in:0,1'], + 'search' => ['nullable', 'string', 'max:255'], + 'sort_by' => ['nullable', 'in:sent_datetime,subject,priority'], + 'sort_dir' => ['nullable', 'in:asc,desc'], + ]; + } +} diff --git a/app/Http/Requests/Messaging/MessageSendRequest.php b/app/Http/Requests/Messaging/MessageSendRequest.php new file mode 100644 index 00000000..5709bc93 --- /dev/null +++ b/app/Http/Requests/Messaging/MessageSendRequest.php @@ -0,0 +1,23 @@ + ['nullable', 'integer', 'min:1', 'required_without:recipient_role'], + 'recipient_role' => ['nullable', 'string', 'required_without:recipient_id', 'in:teacher,parent,admin,student,guest,administrator'], + 'subject' => ['required', 'string', 'max:255'], + 'message' => ['required', 'string'], + 'priority' => ['nullable', 'in:low,normal,high'], + 'status' => ['nullable', 'in:sent,received,draft,trashed'], + 'semester' => ['nullable', 'string', 'max:255'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'attachment' => ['nullable', 'file', 'max:10240'], + ]; + } +} diff --git a/app/Http/Requests/Messaging/MessageUpdateRequest.php b/app/Http/Requests/Messaging/MessageUpdateRequest.php new file mode 100644 index 00000000..9a36539f --- /dev/null +++ b/app/Http/Requests/Messaging/MessageUpdateRequest.php @@ -0,0 +1,17 @@ + ['nullable', 'in:sent,received,draft,trashed'], + 'priority' => ['nullable', 'in:low,normal,high'], + 'read_status' => ['nullable', 'in:0,1'], + ]; + } +} diff --git a/app/Http/Requests/Payments/PaypalExecuteRequest.php b/app/Http/Requests/Payments/PaypalExecuteRequest.php index 7879bba9..82a360ca 100644 --- a/app/Http/Requests/Payments/PaypalExecuteRequest.php +++ b/app/Http/Requests/Payments/PaypalExecuteRequest.php @@ -16,6 +16,7 @@ class PaypalExecuteRequest extends ApiFormRequest return [ 'payer_id' => ['required', 'string', 'max:100'], 'paypal_payment_id' => ['nullable', 'string', 'max:100'], + 'payment_id' => ['nullable', 'integer', 'min:1'], ]; } } diff --git a/app/Http/Requests/Preferences/PreferencesIndexRequest.php b/app/Http/Requests/Preferences/PreferencesIndexRequest.php new file mode 100644 index 00000000..400624e7 --- /dev/null +++ b/app/Http/Requests/Preferences/PreferencesIndexRequest.php @@ -0,0 +1,25 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'user_id' => ['nullable', 'integer', 'min:1'], + 'q' => ['nullable', 'string', 'max:120'], + 'sort_by' => ['nullable', 'string', 'in:updated_at,created_at,user_id'], + 'sort_dir' => ['nullable', 'string', 'in:asc,desc'], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/Preferences/PreferencesUpsertRequest.php b/app/Http/Requests/Preferences/PreferencesUpsertRequest.php new file mode 100644 index 00000000..418f61c3 --- /dev/null +++ b/app/Http/Requests/Preferences/PreferencesUpsertRequest.php @@ -0,0 +1,31 @@ +user() !== null; + } + + public function rules(): array + { + $options = app(PreferencesOptionsService::class); + + return [ + 'receive_email_notifications' => ['nullable', 'boolean'], + 'receive_sms_notifications' => ['nullable', 'boolean'], + 'notification_email' => ['nullable', 'boolean'], + 'notification_sms' => ['nullable', 'boolean'], + 'theme' => ['nullable', 'string', Rule::in(['light', 'dark'])], + 'language' => ['nullable', 'string', Rule::in(['en', 'fr', 'es'])], + 'style_color' => ['nullable', 'string', Rule::in($options->styleOptions())], + 'menu_color' => ['nullable', 'string', Rule::in($options->menuOptions())], + ]; + } +} diff --git a/app/Http/Requests/Security/IpBanIndexRequest.php b/app/Http/Requests/Security/IpBanIndexRequest.php new file mode 100644 index 00000000..0720fc30 --- /dev/null +++ b/app/Http/Requests/Security/IpBanIndexRequest.php @@ -0,0 +1,25 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'status' => ['nullable', 'string', 'in:all,active'], + 'search' => ['nullable', 'string', 'max:100'], + 'sort_by' => ['nullable', 'string', 'in:updated_at,blocked_until,attempts,ip_address,last_attempt_at'], + 'sort_dir' => ['nullable', 'string', 'in:asc,desc'], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/Security/IpBanRequest.php b/app/Http/Requests/Security/IpBanRequest.php new file mode 100644 index 00000000..85ca2767 --- /dev/null +++ b/app/Http/Requests/Security/IpBanRequest.php @@ -0,0 +1,34 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'id' => ['nullable', 'integer', 'min:1'], + 'ip' => ['nullable', 'ip'], + 'hours' => ['nullable', 'integer', 'min:1', 'max:720'], + ]; + } + + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator) { + $id = $this->input('id'); + $ip = $this->input('ip'); + if (!$id && !$ip) { + $validator->errors()->add('id', 'Either id or ip is required.'); + } + }); + } +} diff --git a/app/Http/Requests/Security/IpUnbanRequest.php b/app/Http/Requests/Security/IpUnbanRequest.php new file mode 100644 index 00000000..682a3353 --- /dev/null +++ b/app/Http/Requests/Security/IpUnbanRequest.php @@ -0,0 +1,36 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'id' => ['nullable', 'integer', 'min:1'], + 'ip' => ['nullable', 'ip'], + 'all' => ['nullable', 'boolean'], + ]; + } + + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator) { + $id = $this->input('id'); + $ip = $this->input('ip'); + $all = filter_var($this->input('all'), FILTER_VALIDATE_BOOLEAN); + + if (!$all && !$id && !$ip) { + $validator->errors()->add('id', 'Either id, ip, or all=true is required.'); + } + }); + } +} diff --git a/app/Http/Requests/Settings/PolicyShowRequest.php b/app/Http/Requests/Settings/PolicyShowRequest.php new file mode 100644 index 00000000..a0f76fe5 --- /dev/null +++ b/app/Http/Requests/Settings/PolicyShowRequest.php @@ -0,0 +1,20 @@ + ['nullable', 'in:html'], + ]; + } +} diff --git a/app/Http/Requests/Settings/SettingsUpdateRequest.php b/app/Http/Requests/Settings/SettingsUpdateRequest.php new file mode 100644 index 00000000..9b056ed8 --- /dev/null +++ b/app/Http/Requests/Settings/SettingsUpdateRequest.php @@ -0,0 +1,17 @@ + ['nullable', 'string', 'max:255'], + 'administrator_email' => ['nullable', 'email', 'max:255'], + 'timezone' => ['nullable', 'string', 'max:80'], + ]; + } +} diff --git a/app/Http/Requests/Staff/StaffIndexRequest.php b/app/Http/Requests/Staff/StaffIndexRequest.php new file mode 100644 index 00000000..33949291 --- /dev/null +++ b/app/Http/Requests/Staff/StaffIndexRequest.php @@ -0,0 +1,21 @@ + ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], + 'role' => ['nullable', 'string', 'max:100'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + 'sort_by' => ['nullable', 'in:created_at,lastname,firstname'], + 'sort_dir' => ['nullable', 'in:asc,desc'], + ]; + } +} diff --git a/app/Http/Requests/Staff/StaffStoreRequest.php b/app/Http/Requests/Staff/StaffStoreRequest.php new file mode 100644 index 00000000..b948fc4f --- /dev/null +++ b/app/Http/Requests/Staff/StaffStoreRequest.php @@ -0,0 +1,22 @@ + ['nullable', 'integer', 'min:1'], + 'firstname' => ['required', 'string', 'max:100'], + 'lastname' => ['required', 'string', 'max:100'], + 'email' => ['required', 'email', 'max:150'], + 'phone' => ['nullable', 'string', 'max:20'], + 'role_name' => ['required', 'string', 'max:100'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'status' => ['nullable', 'string', 'max:40'], + ]; + } +} diff --git a/app/Http/Requests/Staff/StaffUpdateRequest.php b/app/Http/Requests/Staff/StaffUpdateRequest.php new file mode 100644 index 00000000..67319f9f --- /dev/null +++ b/app/Http/Requests/Staff/StaffUpdateRequest.php @@ -0,0 +1,21 @@ + ['nullable', 'string', 'max:100'], + 'lastname' => ['nullable', 'string', 'max:100'], + 'email' => ['nullable', 'email', 'max:150'], + 'phone' => ['nullable', 'string', 'max:20'], + 'role_name' => ['nullable', 'string', 'max:100'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'status' => ['nullable', 'string', 'max:40'], + ]; + } +} diff --git a/app/Http/Requests/Support/ContactSendRequest.php b/app/Http/Requests/Support/ContactSendRequest.php new file mode 100644 index 00000000..eef2ec3c --- /dev/null +++ b/app/Http/Requests/Support/ContactSendRequest.php @@ -0,0 +1,18 @@ + ['required', 'string', 'min:3', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'subject' => ['required', 'string', 'min:3', 'max:255'], + 'message' => ['required', 'string', 'min:10'], + ]; + } +} diff --git a/app/Http/Requests/Support/SupportRequestIndexRequest.php b/app/Http/Requests/Support/SupportRequestIndexRequest.php new file mode 100644 index 00000000..11951723 --- /dev/null +++ b/app/Http/Requests/Support/SupportRequestIndexRequest.php @@ -0,0 +1,20 @@ + ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], + 'status' => ['nullable', 'string', 'max:25'], + 'user_id' => ['nullable', 'integer', 'min:1'], + 'sort_by' => ['nullable', 'in:created_at,status,subject'], + 'sort_dir' => ['nullable', 'in:asc,desc'], + ]; + } +} diff --git a/app/Http/Requests/Support/SupportRequestStoreRequest.php b/app/Http/Requests/Support/SupportRequestStoreRequest.php new file mode 100644 index 00000000..a8bf56c5 --- /dev/null +++ b/app/Http/Requests/Support/SupportRequestStoreRequest.php @@ -0,0 +1,16 @@ + ['required', 'string', 'min:3', 'max:255'], + 'message' => ['required', 'string', 'min:10'], + ]; + } +} diff --git a/app/Http/Requests/System/NavItemReorderRequest.php b/app/Http/Requests/System/NavItemReorderRequest.php new file mode 100644 index 00000000..f656fede --- /dev/null +++ b/app/Http/Requests/System/NavItemReorderRequest.php @@ -0,0 +1,21 @@ + ['required', 'array'], + 'orders.*' => ['integer'], + ]; + } +} diff --git a/app/Http/Requests/System/NavItemStoreRequest.php b/app/Http/Requests/System/NavItemStoreRequest.php new file mode 100644 index 00000000..ce52b3fc --- /dev/null +++ b/app/Http/Requests/System/NavItemStoreRequest.php @@ -0,0 +1,30 @@ + ['nullable', 'integer', 'min:1', 'exists:nav_items,id'], + 'menu_parent_id' => ['nullable', 'integer', 'min:1', 'exists:nav_items,id'], + 'parent_id' => ['nullable', 'integer', 'min:1', 'exists:nav_items,id'], + 'label' => ['required', 'string', 'max:255'], + 'url' => ['nullable', 'string', 'max:255'], + 'icon_class' => ['nullable', 'string', 'max:255'], + 'target' => ['nullable', 'string', 'max:50'], + 'sort_order' => ['nullable', 'integer'], + 'is_enabled' => ['nullable', 'boolean'], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['integer', 'min:1', 'exists:roles,id'], + ]; + } +} diff --git a/app/Http/Requests/Ui/UiStyleRequest.php b/app/Http/Requests/Ui/UiStyleRequest.php new file mode 100644 index 00000000..9d0bebe0 --- /dev/null +++ b/app/Http/Requests/Ui/UiStyleRequest.php @@ -0,0 +1,19 @@ + ['nullable', 'string', 'max:50'], + 'menu' => ['nullable', 'string', 'max:50'], + 'menu_bg' => ['nullable', 'string', 'max:20'], + 'menu_text' => ['nullable', 'string', 'max:20'], + 'menu_mode' => ['nullable', 'in:light,dark,auto'], + ]; + } +} diff --git a/app/Http/Requests/Utilities/PhoneFormatRequest.php b/app/Http/Requests/Utilities/PhoneFormatRequest.php new file mode 100644 index 00000000..12df9a64 --- /dev/null +++ b/app/Http/Requests/Utilities/PhoneFormatRequest.php @@ -0,0 +1,20 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'number' => ['required', 'string', 'max:50'], + ]; + } +} diff --git a/app/Http/Resources/Attendance/LateSlipLogCollection.php b/app/Http/Resources/Attendance/LateSlipLogCollection.php new file mode 100644 index 00000000..2ae9108a --- /dev/null +++ b/app/Http/Resources/Attendance/LateSlipLogCollection.php @@ -0,0 +1,27 @@ + [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Attendance/LateSlipLogResource.php b/app/Http/Resources/Attendance/LateSlipLogResource.php new file mode 100644 index 00000000..ad2986cc --- /dev/null +++ b/app/Http/Resources/Attendance/LateSlipLogResource.php @@ -0,0 +1,28 @@ + $this->resource->id ?? null, + 'school_year' => $this->resource->school_year ?? null, + 'semester' => $this->resource->semester ?? null, + 'student_name' => $this->resource->student_name ?? null, + 'slip_date' => $this->resource->slip_date ?? null, + 'time_in' => $this->resource->time_in ?? null, + 'grade' => $this->resource->grade ?? null, + 'reason' => $this->resource->reason ?? null, + 'admin_name' => $this->resource->admin_name ?? null, + 'printed_by' => $this->resource->printed_by ?? null, + 'printed_at' => $this->resource->printed_at ?? null, + 'created_at' => $this->resource->created_at ?? null, + 'updated_at' => $this->resource->updated_at ?? null, + ]; + } +} diff --git a/app/Http/Resources/Auth/SessionTimeoutConfigResource.php b/app/Http/Resources/Auth/SessionTimeoutConfigResource.php deleted file mode 100644 index dcb670e5..00000000 --- a/app/Http/Resources/Auth/SessionTimeoutConfigResource.php +++ /dev/null @@ -1,20 +0,0 @@ - (int) ($this['timeout'] ?? 0), - 'warning_time' => (int) ($this['warning_time'] ?? 0), - 'check_interval' => (int) ($this['check_interval'] ?? 0), - 'logout_url' => (string) ($this['logout_url'] ?? ''), - 'keep_alive_url' => (string) ($this['keep_alive_url'] ?? ''), - 'check_url' => (string) ($this['check_url'] ?? ''), - ]; - } -} diff --git a/app/Http/Resources/Auth/SessionTimeoutStatusResource.php b/app/Http/Resources/Auth/SessionTimeoutStatusResource.php deleted file mode 100644 index 887a18c9..00000000 --- a/app/Http/Resources/Auth/SessionTimeoutStatusResource.php +++ /dev/null @@ -1,18 +0,0 @@ - (string) ($this['status'] ?? ''), - 'time_remaining' => (int) ($this['time_remaining'] ?? 0), - 'redirect' => $this['redirect'] ?? null, - 'message' => $this['message'] ?? null, - ]; - } -} diff --git a/app/Http/Resources/ClassPreparation/ClassPreparationPrintResource.php b/app/Http/Resources/ClassPreparation/ClassPreparationPrintResource.php new file mode 100644 index 00000000..86d2b3be --- /dev/null +++ b/app/Http/Resources/ClassPreparation/ClassPreparationPrintResource.php @@ -0,0 +1,22 @@ + $this->resource['class_section_id'] ?? null, + 'class_section' => $this->resource['class_section'] ?? null, + 'school_year' => $this->resource['school_year'] ?? null, + 'semester' => $this->resource['semester'] ?? null, + 'prep_items' => $this->resource['prep_items'] ?? [], + 'adjustments' => $this->resource['adjustments'] ?? [], + 'printed_at' => $this->resource['printed_at'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/ClassPreparation/ClassPreparationResultResource.php b/app/Http/Resources/ClassPreparation/ClassPreparationResultResource.php new file mode 100644 index 00000000..fdb35278 --- /dev/null +++ b/app/Http/Resources/ClassPreparation/ClassPreparationResultResource.php @@ -0,0 +1,23 @@ + $this->resource['class_section'] ?? null, + 'class_section_id' => $this->resource['class_section_id'] ?? null, + 'student_count' => $this->resource['student_count'] ?? 0, + 'class_level' => $this->resource['class_level'] ?? null, + 'prep_items' => $this->resource['prep_items'] ?? [], + 'needs_print' => $this->resource['needs_print'] ?? false, + 'last_printed_at' => $this->resource['last_printed_at'] ?? null, + 'adjustments' => $this->resource['adjustments'] ?? [], + ]; + } +} diff --git a/app/Http/Resources/Classes/ClassAttendanceResource.php b/app/Http/Resources/Classes/ClassAttendanceResource.php new file mode 100644 index 00000000..93bc9853 --- /dev/null +++ b/app/Http/Resources/Classes/ClassAttendanceResource.php @@ -0,0 +1,21 @@ + $this->resource['teacher_name'] ?? null, + 'class_name' => $this->resource['class_name'] ?? null, + 'class_section_id' => $this->resource['class_section_id'] ?? null, + 'semester' => $this->resource['semester'] ?? null, + 'school_year' => $this->resource['school_year'] ?? null, + 'students' => ClassAttendanceStudentResource::collection($this->resource['students'] ?? []), + ]; + } +} diff --git a/app/Http/Resources/Classes/ClassAttendanceStudentResource.php b/app/Http/Resources/Classes/ClassAttendanceStudentResource.php new file mode 100644 index 00000000..974e23a8 --- /dev/null +++ b/app/Http/Resources/Classes/ClassAttendanceStudentResource.php @@ -0,0 +1,21 @@ + $this->resource['student_id'] ?? null, + 'school_id' => $this->resource['school_id'] ?? null, + 'firstname' => $this->resource['firstname'] ?? null, + 'lastname' => $this->resource['lastname'] ?? null, + 'class_section_id' => $this->resource['class_section_id'] ?? null, + 'updated_by' => $this->resource['updated_by'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Classes/ClassSectionCollection.php b/app/Http/Resources/Classes/ClassSectionCollection.php new file mode 100644 index 00000000..273ae72f --- /dev/null +++ b/app/Http/Resources/Classes/ClassSectionCollection.php @@ -0,0 +1,27 @@ + [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Classes/ClassSectionResource.php b/app/Http/Resources/Classes/ClassSectionResource.php new file mode 100644 index 00000000..f638eabd --- /dev/null +++ b/app/Http/Resources/Classes/ClassSectionResource.php @@ -0,0 +1,24 @@ + $this->resource->id ?? null, + 'class_id' => $this->resource->class_id ?? null, + 'class_section_id' => $this->resource->class_section_id ?? null, + 'class_section_name' => $this->resource->class_section_name ?? null, + 'class_name' => $this->resource->class_name ?? null, + 'semester' => $this->resource->semester ?? null, + 'school_year' => $this->resource->school_year ?? null, + 'created_at' => $this->resource->created_at ?? null, + 'updated_at' => $this->resource->updated_at ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/ContactSubmissionResource.php b/app/Http/Resources/Frontend/ContactSubmissionResource.php new file mode 100644 index 00000000..7f95bd06 --- /dev/null +++ b/app/Http/Resources/Frontend/ContactSubmissionResource.php @@ -0,0 +1,18 @@ + $this->resource['email'] ?? null, + 'email_sent' => (bool) ($this->resource['email_sent'] ?? false), + 'contact_id' => $this->resource['contact_id'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/FrontendPageResource.php b/app/Http/Resources/Frontend/FrontendPageResource.php new file mode 100644 index 00000000..b1c92626 --- /dev/null +++ b/app/Http/Resources/Frontend/FrontendPageResource.php @@ -0,0 +1,16 @@ + $this->resource['page'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/FrontendUserResource.php b/app/Http/Resources/Frontend/FrontendUserResource.php new file mode 100644 index 00000000..b02f262e --- /dev/null +++ b/app/Http/Resources/Frontend/FrontendUserResource.php @@ -0,0 +1,17 @@ + $this->firstname ?? null, + 'lastname' => $this->lastname ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/LandingPageResource.php b/app/Http/Resources/Frontend/LandingPageResource.php new file mode 100644 index 00000000..e8aaccb4 --- /dev/null +++ b/app/Http/Resources/Frontend/LandingPageResource.php @@ -0,0 +1,18 @@ + $this->resource['role'] ?? null, + 'dashboard' => $this->resource['dashboard'] ?? null, + 'summary' => $this->resource['summary'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/PageContentResource.php b/app/Http/Resources/Frontend/PageContentResource.php new file mode 100644 index 00000000..ccb1eb99 --- /dev/null +++ b/app/Http/Resources/Frontend/PageContentResource.php @@ -0,0 +1,17 @@ + $this->resource['type'] ?? null, + 'content' => $this->resource['content'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Frontend/ProfileIconResource.php b/app/Http/Resources/Frontend/ProfileIconResource.php new file mode 100644 index 00000000..b9310089 --- /dev/null +++ b/app/Http/Resources/Frontend/ProfileIconResource.php @@ -0,0 +1,17 @@ + $this->resource['userName'] ?? null, + 'user_initials' => $this->resource['userInitials'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Messaging/MessageCollection.php b/app/Http/Resources/Messaging/MessageCollection.php new file mode 100644 index 00000000..3703c1a4 --- /dev/null +++ b/app/Http/Resources/Messaging/MessageCollection.php @@ -0,0 +1,31 @@ +resource, 'total')) { + return []; + } + + return [ + 'meta' => [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } + + public function toArray($request): array + { + return $this->collection->map(function ($item) use ($request) { + return (new MessageResource($item))->toArray($request); + })->all(); + } +} diff --git a/app/Http/Resources/Messaging/MessageResource.php b/app/Http/Resources/Messaging/MessageResource.php new file mode 100644 index 00000000..3acb8ac3 --- /dev/null +++ b/app/Http/Resources/Messaging/MessageResource.php @@ -0,0 +1,31 @@ + $this->id ?? null, + 'sender_id' => $this->sender_id ?? null, + 'recipient_id' => $this->recipient_id ?? null, + 'subject' => $this->subject ?? null, + 'message' => $this->message ?? null, + 'sent_datetime' => optional($this->sent_datetime)->toDateTimeString(), + 'read_status' => (bool) ($this->read_status ?? false), + 'read_datetime' => optional($this->read_datetime)->toDateTimeString(), + 'message_number' => $this->message_number ?? null, + 'priority' => $this->priority ?? null, + 'attachment' => $this->attachment ?? null, + 'status' => $this->status ?? null, + 'semester' => $this->semester ?? null, + 'school_year' => $this->school_year ?? null, + 'sender_name' => $this->sender ? trim($this->sender->firstname . ' ' . $this->sender->lastname) : null, + 'recipient_name' => $this->recipient ? trim($this->recipient->firstname . ' ' . $this->recipient->lastname) : null, + ]; + } +} diff --git a/app/Http/Resources/Messaging/RecipientResource.php b/app/Http/Resources/Messaging/RecipientResource.php new file mode 100644 index 00000000..27c93202 --- /dev/null +++ b/app/Http/Resources/Messaging/RecipientResource.php @@ -0,0 +1,17 @@ + $this->resource['id'] ?? null, + 'name' => $this->resource['name'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Preferences/PreferencesCollection.php b/app/Http/Resources/Preferences/PreferencesCollection.php new file mode 100644 index 00000000..244c03f3 --- /dev/null +++ b/app/Http/Resources/Preferences/PreferencesCollection.php @@ -0,0 +1,27 @@ + [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Preferences/PreferencesListResource.php b/app/Http/Resources/Preferences/PreferencesListResource.php new file mode 100644 index 00000000..e745c931 --- /dev/null +++ b/app/Http/Resources/Preferences/PreferencesListResource.php @@ -0,0 +1,28 @@ + $this->resource->id ?? null, + 'user_id' => $this->resource->user_id ?? null, + 'firstname' => $this->resource->firstname ?? null, + 'lastname' => $this->resource->lastname ?? null, + 'email' => $this->resource->email ?? null, + 'receive_email_notifications' => (bool) ($this->resource->notification_email ?? false), + 'receive_sms_notifications' => (bool) ($this->resource->notification_sms ?? false), + 'theme' => $this->resource->theme ?? null, + 'language' => $this->resource->language ?? null, + 'style_color' => $this->resource->style_color ?? null, + 'menu_color' => $this->resource->menu_color ?? null, + 'created_at' => $this->resource->created_at ?? null, + 'updated_at' => $this->resource->updated_at ?? null, + ]; + } +} diff --git a/app/Http/Resources/Preferences/PreferencesResource.php b/app/Http/Resources/Preferences/PreferencesResource.php new file mode 100644 index 00000000..dc8ea91e --- /dev/null +++ b/app/Http/Resources/Preferences/PreferencesResource.php @@ -0,0 +1,21 @@ + $this->resource['receive_email_notifications'] ?? false, + 'receive_sms_notifications' => $this->resource['receive_sms_notifications'] ?? false, + 'theme' => $this->resource['theme'] ?? null, + 'language' => $this->resource['language'] ?? null, + 'style_color' => $this->resource['style_color'] ?? null, + 'menu_color' => $this->resource['menu_color'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Security/IpBanCollection.php b/app/Http/Resources/Security/IpBanCollection.php new file mode 100644 index 00000000..355ca64c --- /dev/null +++ b/app/Http/Resources/Security/IpBanCollection.php @@ -0,0 +1,27 @@ + [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Security/IpBanResource.php b/app/Http/Resources/Security/IpBanResource.php new file mode 100644 index 00000000..3e215201 --- /dev/null +++ b/app/Http/Resources/Security/IpBanResource.php @@ -0,0 +1,26 @@ +resource->blocked_until ?? null; + $isActive = $blockedUntil !== null && $blockedUntil > now('UTC'); + + return [ + 'id' => $this->resource->id ?? null, + 'ip_address' => $this->resource->ip_address ?? null, + 'attempts' => $this->resource->attempts ?? 0, + 'last_attempt_at' => $this->resource->last_attempt_at ?? null, + 'blocked_until' => $blockedUntil, + 'is_active' => $isActive, + 'created_at' => $this->resource->created_at ?? null, + 'updated_at' => $this->resource->updated_at ?? null, + ]; + } +} diff --git a/app/Http/Resources/Settings/PolicyResource.php b/app/Http/Resources/Settings/PolicyResource.php new file mode 100644 index 00000000..065db175 --- /dev/null +++ b/app/Http/Resources/Settings/PolicyResource.php @@ -0,0 +1,20 @@ + $this->resource['type'] ?? null, + 'title' => $this->resource['title'] ?? null, + 'format' => $this->resource['format'] ?? 'html', + 'content' => $this->resource['content'] ?? '', + 'updated_at' => $this->resource['updated_at'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Settings/SettingsResource.php b/app/Http/Resources/Settings/SettingsResource.php new file mode 100644 index 00000000..fbec7cb3 --- /dev/null +++ b/app/Http/Resources/Settings/SettingsResource.php @@ -0,0 +1,18 @@ + $this->resource['site_name'] ?? null, + 'administrator_email' => $this->resource['administrator_email'] ?? null, + 'timezone' => $this->resource['timezone'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Staff/StaffCollection.php b/app/Http/Resources/Staff/StaffCollection.php new file mode 100644 index 00000000..7eb263e1 --- /dev/null +++ b/app/Http/Resources/Staff/StaffCollection.php @@ -0,0 +1,31 @@ +collection->map(function ($item) use ($request) { + return (new StaffResource($item))->toArray($request); + })->all(); + } + + public function with($request): array + { + if (!method_exists($this->resource, 'total')) { + return []; + } + + return [ + 'meta' => [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Staff/StaffResource.php b/app/Http/Resources/Staff/StaffResource.php new file mode 100644 index 00000000..9a880e21 --- /dev/null +++ b/app/Http/Resources/Staff/StaffResource.php @@ -0,0 +1,29 @@ + $this->id ?? null, + 'user_id' => $this->user_id ?? null, + 'firstname' => $this->firstname ?? null, + 'lastname' => $this->lastname ?? null, + 'email' => $this->email ?? null, + 'phone' => $this->phone ?? null, + 'role_name' => $this->role_name ?? null, + 'active_role' => $this->active_role ?? null, + 'school_year' => $this->school_year ?? null, + 'class_section' => $this->class_section ?? null, + 'verification_issue' => (bool) ($this->verification_issue ?? false), + 'school_id' => $this->school_id ?? null, + 'created_at' => optional($this->created_at)->toDateTimeString(), + 'updated_at' => optional($this->updated_at)->toDateTimeString(), + ]; + } +} diff --git a/app/Http/Resources/Support/ContactMessageResource.php b/app/Http/Resources/Support/ContactMessageResource.php new file mode 100644 index 00000000..e631421e --- /dev/null +++ b/app/Http/Resources/Support/ContactMessageResource.php @@ -0,0 +1,17 @@ + (bool) ($this->resource['email_sent'] ?? false), + 'recipient' => $this->resource['recipient'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Support/SupportRequestCollection.php b/app/Http/Resources/Support/SupportRequestCollection.php new file mode 100644 index 00000000..fda04237 --- /dev/null +++ b/app/Http/Resources/Support/SupportRequestCollection.php @@ -0,0 +1,31 @@ +collection->map(function ($item) use ($request) { + return (new SupportRequestResource($item))->toArray($request); + })->all(); + } + + public function with($request): array + { + if (!method_exists($this->resource, 'total')) { + return []; + } + + return [ + 'meta' => [ + 'total' => $this->resource->total() ?? null, + 'per_page' => $this->resource->perPage() ?? null, + 'current_page' => $this->resource->currentPage() ?? null, + 'last_page' => $this->resource->lastPage() ?? null, + ], + ]; + } +} diff --git a/app/Http/Resources/Support/SupportRequestResource.php b/app/Http/Resources/Support/SupportRequestResource.php new file mode 100644 index 00000000..e8dafa00 --- /dev/null +++ b/app/Http/Resources/Support/SupportRequestResource.php @@ -0,0 +1,24 @@ + $this->id ?? null, + 'user_id' => $this->user_id ?? null, + 'subject' => $this->subject ?? null, + 'message' => $this->message ?? null, + 'status' => $this->status ?? null, + 'semester' => $this->semester ?? null, + 'school_year' => $this->school_year ?? null, + 'created_at' => optional($this->created_at)->toDateTimeString(), + 'updated_at' => optional($this->updated_at)->toDateTimeString(), + ]; + } +} diff --git a/app/Http/Resources/System/DashboardRouteResource.php b/app/Http/Resources/System/DashboardRouteResource.php new file mode 100644 index 00000000..5b6dabf3 --- /dev/null +++ b/app/Http/Resources/System/DashboardRouteResource.php @@ -0,0 +1,19 @@ + $this->resource['route'] ?? null, + 'role' => $this->resource['role'] ?? null, + 'role_slug' => $this->resource['role_slug'] ?? null, + 'roles' => $this->resource['roles'] ?? [], + ]; + } +} diff --git a/app/Http/Resources/System/HealthStatusResource.php b/app/Http/Resources/System/HealthStatusResource.php new file mode 100644 index 00000000..e78d0025 --- /dev/null +++ b/app/Http/Resources/System/HealthStatusResource.php @@ -0,0 +1,20 @@ + $this->resource['ok'] ?? false, + 'paths' => $this->resource['paths'] ?? [], + 'database' => $this->resource['database'] ?? [], + 'write_path' => $this->resource['write_path'] ?? null, + 'timestamp' => $this->resource['timestamp'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/System/NavBuilderDataResource.php b/app/Http/Resources/System/NavBuilderDataResource.php new file mode 100644 index 00000000..bc53417a --- /dev/null +++ b/app/Http/Resources/System/NavBuilderDataResource.php @@ -0,0 +1,18 @@ + $this->resource['items'] ?? [], + 'roles' => $this->resource['roles'] ?? [], + 'parentOptions' => $this->resource['parentOptions'] ?? [], + ]; + } +} diff --git a/app/Http/Resources/System/NavItemResource.php b/app/Http/Resources/System/NavItemResource.php new file mode 100644 index 00000000..67b0d82b --- /dev/null +++ b/app/Http/Resources/System/NavItemResource.php @@ -0,0 +1,28 @@ +resource) ? $this->resource : $this->resource->toArray(); + + return [ + 'id' => (int) ($item['id'] ?? 0), + 'menu_parent_id' => $item['menu_parent_id'] ?? null, + 'label' => (string) ($item['label'] ?? ''), + 'url' => $item['url'] ?? null, + 'icon_class' => $item['icon_class'] ?? null, + 'target' => $item['target'] ?? null, + 'sort_order' => (int) ($item['sort_order'] ?? 0), + 'is_enabled' => (int) ($item['is_enabled'] ?? 0), + 'children' => !empty($item['children']) + ? NavItemResource::collection($item['children']) + : [], + ]; + } +} diff --git a/app/Http/Resources/Ui/UiStyleResource.php b/app/Http/Resources/Ui/UiStyleResource.php new file mode 100644 index 00000000..ee052699 --- /dev/null +++ b/app/Http/Resources/Ui/UiStyleResource.php @@ -0,0 +1,20 @@ + $this->style_color ?? null, + 'menu_color' => $this->menu_color ?? null, + 'menu_custom_bg' => $this->menu_custom_bg ?? null, + 'menu_custom_text' => $this->menu_custom_text ?? null, + 'menu_custom_mode' => $this->menu_custom_mode ?? null, + ]; + } +} diff --git a/app/Http/Resources/Utilities/PhoneFormatResource.php b/app/Http/Resources/Utilities/PhoneFormatResource.php new file mode 100644 index 00000000..9a440784 --- /dev/null +++ b/app/Http/Resources/Utilities/PhoneFormatResource.php @@ -0,0 +1,18 @@ + $this->resource['raw'] ?? null, + 'formatted' => $this->resource['formatted'] ?? null, + 'is_valid' => $this->resource['is_valid'] ?? false, + ]; + } +} diff --git a/app/Models/SupportRequest.php b/app/Models/SupportRequest.php new file mode 100644 index 00000000..792488a3 --- /dev/null +++ b/app/Models/SupportRequest.php @@ -0,0 +1,34 @@ + 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Policies/ClassSectionPolicy.php b/app/Policies/ClassSectionPolicy.php new file mode 100644 index 00000000..fff0f965 --- /dev/null +++ b/app/Policies/ClassSectionPolicy.php @@ -0,0 +1,55 @@ +isAdmin($user); + } + + public function update(User $user, ClassSection $classSection): bool + { + return $this->isAdmin($user); + } + + public function delete(User $user, ClassSection $classSection): bool + { + return $this->isAdmin($user); + } + + public function seedDefaults(User $user): bool + { + return $this->isAdmin($user); + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/IpAttemptPolicy.php b/app/Policies/IpAttemptPolicy.php new file mode 100644 index 00000000..17bf6c23 --- /dev/null +++ b/app/Policies/IpAttemptPolicy.php @@ -0,0 +1,50 @@ +isAdmin($user); + } + + public function view(User $user, IpAttempt $ipAttempt): bool + { + return $this->isAdmin($user); + } + + public function create(User $user): bool + { + return $this->isAdmin($user); + } + + public function update(User $user, IpAttempt $ipAttempt): bool + { + return $this->isAdmin($user); + } + + public function delete(User $user, IpAttempt $ipAttempt): bool + { + return $this->isAdmin($user); + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/LateSlipLogPolicy.php b/app/Policies/LateSlipLogPolicy.php new file mode 100644 index 00000000..b143d7f2 --- /dev/null +++ b/app/Policies/LateSlipLogPolicy.php @@ -0,0 +1,40 @@ +isAdmin($user); + } + + public function view(User $user, LateSlipLog $log): bool + { + return $this->isAdmin($user); + } + + public function delete(User $user, LateSlipLog $log): bool + { + return $this->isAdmin($user); + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/MessagePolicy.php b/app/Policies/MessagePolicy.php new file mode 100644 index 00000000..7d98e084 --- /dev/null +++ b/app/Policies/MessagePolicy.php @@ -0,0 +1,55 @@ +owns($user, $message) || $this->isAdmin($user); + } + + public function create(User $user): bool + { + return $user !== null; + } + + public function update(User $user, Message $message): bool + { + return $this->owns($user, $message) || $this->isAdmin($user); + } + + public function delete(User $user, Message $message): bool + { + return $this->owns($user, $message) || $this->isAdmin($user); + } + + private function owns(User $user, Message $message): bool + { + return $message->sender_id === $user->id || $message->recipient_id === $user->id; + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/NavItemPolicy.php b/app/Policies/NavItemPolicy.php new file mode 100644 index 00000000..f6022037 --- /dev/null +++ b/app/Policies/NavItemPolicy.php @@ -0,0 +1,15 @@ +roles()->pluck('roles.name')->map(fn ($name) => strtolower((string) $name))->toArray(); + return in_array('admin', $roles, true) || in_array('administrator', $roles, true); + } +} diff --git a/app/Policies/PreferencesPolicy.php b/app/Policies/PreferencesPolicy.php new file mode 100644 index 00000000..7ce69ca0 --- /dev/null +++ b/app/Policies/PreferencesPolicy.php @@ -0,0 +1,50 @@ +isAdmin($user); + } + + public function view(User $user, Preferences $preferences): bool + { + return $preferences->user_id === $user->id || $this->isAdmin($user); + } + + public function update(User $user, Preferences $preferences): bool + { + return $preferences->user_id === $user->id || $this->isAdmin($user); + } + + public function delete(User $user, Preferences $preferences): bool + { + return $this->isAdmin($user); + } + + public function create(User $user): bool + { + return $user !== null; + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/SettingPolicy.php b/app/Policies/SettingPolicy.php new file mode 100644 index 00000000..974410c2 --- /dev/null +++ b/app/Policies/SettingPolicy.php @@ -0,0 +1,35 @@ +isAdmin($user); + } + + public function update(User $user, Setting $setting): bool + { + return $this->isAdmin($user); + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/StaffPolicy.php b/app/Policies/StaffPolicy.php new file mode 100644 index 00000000..90e6143e --- /dev/null +++ b/app/Policies/StaffPolicy.php @@ -0,0 +1,50 @@ +isAdmin($user); + } + + public function view(User $user, Staff $staff): bool + { + return $this->isAdmin($user); + } + + public function create(User $user): bool + { + return $this->isAdmin($user); + } + + public function update(User $user, Staff $staff): bool + { + return $this->isAdmin($user); + } + + public function delete(User $user, Staff $staff): bool + { + return $this->isAdmin($user); + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Policies/SupportRequestPolicy.php b/app/Policies/SupportRequestPolicy.php new file mode 100644 index 00000000..c623c169 --- /dev/null +++ b/app/Policies/SupportRequestPolicy.php @@ -0,0 +1,40 @@ +user_id === $user->id || $this->isAdmin($user); + } + + public function create(User $user): bool + { + return $user !== null; + } + + private function isAdmin(User $user): bool + { + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { + if (in_array($role, $roles, true)) { + return true; + } + } + + return false; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 28f26a46..812b79af 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -12,6 +12,25 @@ use App\Services\Invoices\InvoiceConfigService; use App\Services\Invoices\InvoiceGradeService; use App\Services\Invoices\InvoiceTuitionService; use App\Services\Staff\StaffTimeOffLinkService; +use App\Models\NavItem; +use App\Models\ClassSection; +use App\Models\IpAttempt; +use App\Models\LateSlipLog; +use App\Models\Message; +use App\Models\Preferences; +use App\Models\Staff; +use App\Models\SupportRequest; +use App\Models\Setting; +use App\Policies\NavItemPolicy; +use App\Policies\ClassSectionPolicy; +use App\Policies\IpAttemptPolicy; +use App\Policies\LateSlipLogPolicy; +use App\Policies\MessagePolicy; +use App\Policies\PreferencesPolicy; +use App\Policies\StaffPolicy; +use App\Policies\SupportRequestPolicy; +use App\Policies\SettingPolicy; +use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Routing\Redirector; use Illuminate\Support\ServiceProvider; @@ -48,6 +67,16 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { + Gate::policy(NavItem::class, NavItemPolicy::class); + Gate::policy(ClassSection::class, ClassSectionPolicy::class); + Gate::policy(IpAttempt::class, IpAttemptPolicy::class); + Gate::policy(LateSlipLog::class, LateSlipLogPolicy::class); + Gate::policy(Message::class, MessagePolicy::class); + Gate::policy(Preferences::class, PreferencesPolicy::class); + Gate::policy(Staff::class, StaffPolicy::class); + Gate::policy(SupportRequest::class, SupportRequestPolicy::class); + Gate::policy(Setting::class, SettingPolicy::class); + $this->app->resolving(FormRequest::class, function (FormRequest $request, $app): void { $request->setContainer($app)->setRedirector($app->make(Redirector::class)); }); diff --git a/app/Services/Attendance/LateSlipLogCommandService.php b/app/Services/Attendance/LateSlipLogCommandService.php new file mode 100644 index 00000000..6987dc89 --- /dev/null +++ b/app/Services/Attendance/LateSlipLogCommandService.php @@ -0,0 +1,13 @@ +delete(); + } +} diff --git a/app/Services/Attendance/LateSlipLogQueryService.php b/app/Services/Attendance/LateSlipLogQueryService.php new file mode 100644 index 00000000..5d27271c --- /dev/null +++ b/app/Services/Attendance/LateSlipLogQueryService.php @@ -0,0 +1,63 @@ +where('school_year', (string) $filters['school_year']); + } + + if (!empty($filters['semester'])) { + $query->where('semester', (string) $filters['semester']); + } + + if (!empty($filters['q'])) { + $query->where('student_name', 'like', '%' . $filters['q'] . '%'); + } + + $dateFrom = $this->toDbDate($filters['date_from'] ?? null); + if ($dateFrom) { + $query->where('slip_date', '>=', $dateFrom); + } + + $dateTo = $this->toDbDate($filters['date_to'] ?? null); + if ($dateTo) { + $query->where('slip_date', '<=', $dateTo); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'id'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + $allowedSorts = ['id', 'slip_date', 'student_name', 'printed_at', 'updated_at']; + + if (in_array($sortBy, $allowedSorts, true)) { + $query->orderBy($sortBy, $sortDir); + } else { + $query->orderBy('id', 'desc'); + } + + return $query->paginate($perPage, ['*'], 'page', $page); + } + + public function find(int $id): ?LateSlipLog + { + return LateSlipLog::query()->find($id); + } + + private function toDbDate(?string $value): ?string + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + $ts = strtotime($value); + return $ts ? date('Y-m-d', $ts) : null; + } +} diff --git a/app/Services/Auth/RegistrationCaptchaService.php b/app/Services/Auth/RegistrationCaptchaService.php index 47c79924..90cfc3fd 100644 --- a/app/Services/Auth/RegistrationCaptchaService.php +++ b/app/Services/Auth/RegistrationCaptchaService.php @@ -4,6 +4,14 @@ namespace App\Services\Auth; class RegistrationCaptchaService { + private function cacheKey(): string + { + $ip = (string) (request()?->ip() ?? 'unknown'); + $ua = (string) (request()?->header('User-Agent') ?? ''); + $hash = substr(sha1($ua), 0, 12); + return 'captcha:' . $ip . ':' . $hash; + } + public function generate(?int $length = null): string { $length = $length ?? random_int(4, 8); @@ -14,14 +22,14 @@ class RegistrationCaptchaService $captchaText .= $characters[random_int(0, strlen($characters) - 1)]; } - session()->put('captcha_answer', $captchaText); + cache()->put($this->cacheKey(), $captchaText, now()->addMinutes(10)); return $captchaText; } public function verify(string $input): bool { - $expected = (string) session()->get('captcha_answer', ''); + $expected = (string) cache()->get($this->cacheKey(), ''); if ($expected === '') { return false; } @@ -31,6 +39,6 @@ class RegistrationCaptchaService public function clear(): void { - session()->forget('captcha_answer'); + cache()->forget($this->cacheKey()); } } diff --git a/app/Services/Auth/SessionActivityService.php b/app/Services/Auth/SessionActivityService.php deleted file mode 100644 index a4ea4934..00000000 --- a/app/Services/Auth/SessionActivityService.php +++ /dev/null @@ -1,39 +0,0 @@ -session->has('last_activity'); - } - - public function getLastActivity(): ?int - { - $value = $this->session->get('last_activity'); - return $value !== null ? (int) $value : null; - } - - public function setLastActivity(int $timestamp): void - { - $this->session->put('last_activity', $timestamp); - } - - public function clearLastActivity(): void - { - $this->session->forget('last_activity'); - } - - public function invalidate(): void - { - $this->session->invalidate(); - $this->session->regenerateToken(); - } -} diff --git a/app/Services/Auth/SessionTimeoutConfigService.php b/app/Services/Auth/SessionTimeoutConfigService.php deleted file mode 100644 index ac3e416b..00000000 --- a/app/Services/Auth/SessionTimeoutConfigService.php +++ /dev/null @@ -1,21 +0,0 @@ - SessionTimeout::TIMEOUT_DURATION, - 'warning_time' => SessionTimeout::WARNING_THRESHOLD, - 'check_interval' => SessionTimeout::CHECK_INTERVAL, - 'logout_url' => URL::to('/auth/logout'), - 'keep_alive_url' => route('session.timeout.ping'), - 'check_url' => route('session.timeout.check'), - ]; - } -} diff --git a/app/Services/Auth/SessionTimeoutService.php b/app/Services/Auth/SessionTimeoutService.php deleted file mode 100644 index 54f9f15b..00000000 --- a/app/Services/Auth/SessionTimeoutService.php +++ /dev/null @@ -1,72 +0,0 @@ -activityService->hasLastActivity()) { - return $this->expireSession(); - } - - $lastActivity = (int) $this->activityService->getLastActivity(); - $elapsed = time() - $lastActivity; - - if ($elapsed > SessionTimeout::TIMEOUT_DURATION) { - return $this->expireSession(); - } - - if ($elapsed > SessionTimeout::WARNING_THRESHOLD) { - return [ - 'status' => 'warning', - 'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed, - ]; - } - - return [ - 'status' => 'active', - 'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed, - ]; - } - - public function pingActivity(): array - { - if ( - !$this->activityService->hasLastActivity() - || (time() - (int) $this->activityService->getLastActivity() > SessionTimeout::TIMEOUT_DURATION) - ) { - return $this->expireSession(); - } - - $this->activityService->setLastActivity(time()); - - return [ - 'status' => 'active', - 'time_remaining' => SessionTimeout::TIMEOUT_DURATION, - ]; - } - - private function expireSession(): array - { - Log::info('Session expired due to inactivity.'); - $this->activityService->clearLastActivity(); - $this->activityService->invalidate(); - - return [ - 'status' => 'expired', - 'redirect' => URL::to('/login'), - 'message' => 'Your session has expired due to inactivity.', - 'time_remaining' => 0, - ]; - } -} diff --git a/app/Services/Auth/UserRoleService.php b/app/Services/Auth/UserRoleService.php new file mode 100644 index 00000000..b1f8abc8 --- /dev/null +++ b/app/Services/Auth/UserRoleService.php @@ -0,0 +1,54 @@ +where('user_id', $userId) + ->whereNull('deleted_at') + ->pluck('role_id') + ->map(fn ($id) => (int) $id) + ->all(); + } + + public function hasPermissionForCrud(array $roleIds, string $permissionName, string $crudAction = 'read'): bool + { + if (empty($roleIds)) { + return false; + } + + $rows = DB::table('role_permissions as rp') + ->join('permissions as p', 'p.id', '=', 'rp.permission_id') + ->whereIn('rp.role_id', $roleIds) + ->where('p.name', $permissionName) + ->select('rp.can_create', 'rp.can_read', 'rp.can_update', 'rp.can_delete', 'rp.can_manage') + ->get(); + + foreach ($rows as $row) { + $manage = (bool) ($row->can_manage ?? false); + if ($manage) { + return true; + } + + if ($crudAction === 'create' && !empty($row->can_create)) { + return true; + } + if ($crudAction === 'read' && !empty($row->can_read)) { + return true; + } + if ($crudAction === 'update' && !empty($row->can_update)) { + return true; + } + if ($crudAction === 'delete' && !empty($row->can_delete)) { + return true; + } + } + + return false; + } +} diff --git a/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php b/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php new file mode 100644 index 00000000..fb2b0942 --- /dev/null +++ b/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php @@ -0,0 +1,51 @@ + $adjustment) { + $item = (string) $itemName; + if ($item === '' || !isset($allowed[$item])) { + continue; + } + + $row = ClassPrepAdjustment::query() + ->where('class_section_id', $classSectionId) + ->where('school_year', $schoolYear) + ->where('item_name', $item) + ->first(); + + if ($row) { + $row->update([ + 'adjustment' => (int) $adjustment, + 'adjustable' => 1, + ]); + $count++; + continue; + } + + ClassPrepAdjustment::query()->create([ + 'class_section_id' => $classSectionId, + 'item_name' => $item, + 'adjustment' => (int) $adjustment, + 'school_year' => $schoolYear, + 'adjustable' => 1, + ]); + $count++; + } + + return $count; + }); + } +} diff --git a/app/Services/ClassPreparation/ClassPreparationPrintService.php b/app/Services/ClassPreparation/ClassPreparationPrintService.php new file mode 100644 index 00000000..ae9128f2 --- /dev/null +++ b/app/Services/ClassPreparation/ClassPreparationPrintService.php @@ -0,0 +1,60 @@ +context->getSemester(); + $limitToSemester = $this->context->hasRosterForSemester($schoolYear, $semester); + + $studentCount = $this->roster->getStudentCountForSection($schoolYear, $semester, $limitToSemester, $classSectionId); + $classLevel = $this->calculator->getClassLevelBySection($classSectionId); + $className = ClassSection::getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; + + $items = $this->calculator->calculatePrepItems($studentCount, $classLevel, $classSectionId); + [$items, $adjMap] = $this->adjustments->applyAdjustments($items, $classSectionId, $schoolYear); + + $printedAt = $this->utcNow(); + $ok = $this->logs->createLog($classSectionId, $className, $schoolYear, $items, $printedAt); + + if (!$ok) { + Log::warning('Failed to store class preparation log.', [ + 'class_section_id' => $classSectionId, + 'school_year' => $schoolYear, + ]); + } + + return [ + 'ok' => $ok, + 'class_section_id' => $classSectionId, + 'class_section' => $className, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'prep_items' => $items, + 'adjustments' => $adjMap, + 'printed_at' => $ok ? $printedAt : null, + ]; + } + + private function utcNow(): string + { + if (function_exists('utc_now')) { + return (string) utc_now(); + } + return now('UTC')->toDateTimeString(); + } +} diff --git a/app/Services/ClassPreparation/ClassPreparationService.php b/app/Services/ClassPreparation/ClassPreparationService.php index dc20a7f4..d24e210e 100644 --- a/app/Services/ClassPreparation/ClassPreparationService.php +++ b/app/Services/ClassPreparation/ClassPreparationService.php @@ -12,6 +12,8 @@ class ClassPreparationService private ClassPreparationCalculatorService $calculator; private ClassPreparationAdjustmentService $adjustments; private ClassPreparationLogService $logs; + private ClassPreparationAdjustmentWriterService $adjustmentWriter; + private ClassPreparationPrintService $printService; public function __construct( ClassPreparationContextService $context, @@ -19,7 +21,9 @@ class ClassPreparationService ClassPreparationInventoryService $inventory, ClassPreparationCalculatorService $calculator, ClassPreparationAdjustmentService $adjustments, - ClassPreparationLogService $logs + ClassPreparationLogService $logs, + ClassPreparationAdjustmentWriterService $adjustmentWriter, + ClassPreparationPrintService $printService ) { $this->context = $context; $this->roster = $roster; @@ -27,6 +31,8 @@ class ClassPreparationService $this->calculator = $calculator; $this->adjustments = $adjustments; $this->logs = $logs; + $this->adjustmentWriter = $adjustmentWriter; + $this->printService = $printService; } public function listPrep(?string $schoolYear = null, ?string $semester = null): array @@ -111,6 +117,23 @@ class ClassPreparationService return $count; } + public function saveAdjustments(string $classSectionId, string $schoolYear, array $adjustments): int + { + $schoolYear = $schoolYear !== '' ? $schoolYear : $this->context->getSchoolYear(); + + return $this->adjustmentWriter->saveAdjustments( + $classSectionId, + $schoolYear, + $adjustments, + $this->calculator->getAllowedCategories() + ); + } + + public function printPrep(string $classSectionId, string $schoolYear, ?string $semester = null): array + { + return $this->printService->printPrep($classSectionId, $schoolYear, $semester); + } + private function utcNow(): string { if (function_exists('utc_now')) { diff --git a/app/Services/ClassSections/ClassAttendanceService.php b/app/Services/ClassSections/ClassAttendanceService.php new file mode 100644 index 00000000..46e28922 --- /dev/null +++ b/app/Services/ClassSections/ClassAttendanceService.php @@ -0,0 +1,32 @@ +find($teacherId); + $teacherName = trim((string) ($teacher?->firstname ?? '') . ' ' . (string) ($teacher?->lastname ?? '')); + + $className = ClassSection::getClassSectionNameBySectionId($classSectionId) ?? (string) $classSectionId; + $students = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, $teacherId); + + return [ + 'teacher_name' => $teacherName, + 'class_name' => $className, + 'class_section_id' => $classSectionId, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'students' => $students, + ]; + } +} diff --git a/app/Services/ClassSections/ClassSectionCommandService.php b/app/Services/ClassSections/ClassSectionCommandService.php new file mode 100644 index 00000000..db0d68e3 --- /dev/null +++ b/app/Services/ClassSections/ClassSectionCommandService.php @@ -0,0 +1,26 @@ +create($data); + } + + public function update(ClassSection $section, array $data): ClassSection + { + $section->fill($data); + $section->save(); + + return $section->refresh(); + } + + public function delete(ClassSection $section): bool + { + return (bool) $section->delete(); + } +} diff --git a/app/Services/ClassSections/ClassSectionQueryService.php b/app/Services/ClassSections/ClassSectionQueryService.php new file mode 100644 index 00000000..517d4922 --- /dev/null +++ b/app/Services/ClassSections/ClassSectionQueryService.php @@ -0,0 +1,62 @@ +leftJoin('classes', 'classSection.class_id', '=', 'classes.id') + ->select('classSection.*', 'classes.class_name'); + + if (!empty($filters['search'])) { + $term = '%' . strtolower((string) $filters['search']) . '%'; + $query->where(function ($q) use ($term) { + $q->whereRaw('LOWER(classSection.class_section_name) LIKE ?', [$term]) + ->orWhereRaw('LOWER(classes.class_name) LIKE ?', [$term]); + }); + } + + if (!empty($filters['class_id'])) { + $query->where('classSection.class_id', (int) $filters['class_id']); + } + + if (!empty($filters['school_year'])) { + $query->where('classSection.school_year', (string) $filters['school_year']); + } + + if (!empty($filters['semester'])) { + $query->where('classSection.semester', (string) $filters['semester']); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'class_section_name'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; + + $allowedSorts = [ + 'class_section_name' => 'classSection.class_section_name', + 'class_section_id' => 'classSection.class_section_id', + 'class_id' => 'classSection.class_id', + 'school_year' => 'classSection.school_year', + 'semester' => 'classSection.semester', + 'class_name' => 'classes.class_name', + ]; + + $sortColumn = $allowedSorts[$sortBy] ?? $allowedSorts['class_section_name']; + $query->orderBy($sortColumn, $sortDir); + + return $query->paginate($perPage, ['*'], 'page', $page); + } + + public function find(int $id): ?ClassSection + { + return ClassSection::query() + ->leftJoin('classes', 'classSection.class_id', '=', 'classes.id') + ->select('classSection.*', 'classes.class_name') + ->where('classSection.id', $id) + ->first(); + } +} diff --git a/app/Services/ClassSections/ClassSectionSeedService.php b/app/Services/ClassSections/ClassSectionSeedService.php new file mode 100644 index 00000000..9fa45d44 --- /dev/null +++ b/app/Services/ClassSections/ClassSectionSeedService.php @@ -0,0 +1,52 @@ + 'Class 1', 'schedule' => 'Monday 9:00 AM - 10:00 AM', 'capacity' => 30], + ['class_name' => 'Class 2', 'schedule' => 'Tuesday 9:00 AM - 10:00 AM', 'capacity' => 30], + ['class_name' => 'Class 3', 'schedule' => 'Wednesday 9:00 AM - 10:00 AM', 'capacity' => 30], + ['class_name' => 'Class 4', 'schedule' => 'Thursday 9:00 AM - 10:00 AM', 'capacity' => 30], + ['class_name' => 'Class 5', 'schedule' => 'Friday 9:00 AM - 10:00 AM', 'capacity' => 30], + ['class_name' => 'Class 6', 'schedule' => 'Monday 10:00 AM - 11:00 AM', 'capacity' => 30], + ['class_name' => 'Class 7', 'schedule' => 'Tuesday 10:00 AM - 11:00 AM', 'capacity' => 30], + ['class_name' => 'Class 8', 'schedule' => 'Wednesday 10:00 AM - 11:00 AM', 'capacity' => 30], + ['class_name' => 'Class 9', 'schedule' => 'Thursday 10:00 AM - 11:00 AM', 'capacity' => 30], + ['class_name' => 'Youth', 'schedule' => 'Friday 10:00 AM - 11:00 AM', 'capacity' => 30], + ]; + + return DB::transaction(function () use ($classes, $semester, $schoolYear) { + $created = 0; + foreach ($classes as $class) { + $row = SchoolClass::query()->firstOrCreate( + [ + 'class_name' => $class['class_name'], + 'semester' => $semester, + 'school_year' => $schoolYear, + ], + [ + 'schedule' => $class['schedule'], + 'capacity' => $class['capacity'], + ] + ); + + if ($row->wasRecentlyCreated) { + $created++; + } + } + + return $created; + }); + } +} diff --git a/app/Services/Dashboard/DashboardRouteService.php b/app/Services/Dashboard/DashboardRouteService.php new file mode 100644 index 00000000..cbd2b9a1 --- /dev/null +++ b/app/Services/Dashboard/DashboardRouteService.php @@ -0,0 +1,69 @@ + self::FALLBACK_ROUTE, + 'role' => null, + 'role_slug' => null, + 'roles' => [], + ]; + } + + $roles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->toArray(); + + if (empty($roles)) { + Log::warning('Dashboard route requested with no roles assigned.', [ + 'user_id' => $user->id, + ]); + + return [ + 'route' => self::FALLBACK_ROUTE, + 'role' => null, + 'role_slug' => null, + 'roles' => [], + ]; + } + + $rows = Role::findByNamesOrSlugs($roles); + + if (!empty($rows)) { + $role = $rows[0]; + $route = $role->dashboard_route ?: self::FALLBACK_ROUTE; + + return [ + 'route' => $route, + 'role' => $role->name ?? null, + 'role_slug' => $role->slug ?? null, + 'roles' => $roles, + ]; + } + + Log::warning('No matching role dashboard route found.', [ + 'user_id' => $user->id, + 'roles' => $roles, + ]); + + return [ + 'route' => self::FALLBACK_ROUTE, + 'role' => null, + 'role_slug' => null, + 'roles' => $roles, + ]; + } +} diff --git a/app/Services/Docs/OpenApiRouteExporter.php b/app/Services/Docs/OpenApiRouteExporter.php index 883e1ccb..82dec7ed 100644 --- a/app/Services/Docs/OpenApiRouteExporter.php +++ b/app/Services/Docs/OpenApiRouteExporter.php @@ -29,7 +29,7 @@ class OpenApiRouteExporter private function mergeRoute(array &$paths, Route $route): void { - if (!$this->isApiControllerRoute($route)) { + if (!$this->isDocumentableRoute($route)) { return; } @@ -70,14 +70,27 @@ class OpenApiRouteExporter } } - private function isApiControllerRoute(Route $route): bool + private function isDocumentableRoute(Route $route): bool { + $uri = ltrim($route->uri(), '/'); + if (!str_starts_with($uri, 'api/v1') && !str_starts_with($uri, 'v1/')) { + return false; + } + $action = $route->getActionName(); if (!is_string($action)) { return false; } - return str_starts_with($action, 'App\\Http\\Controllers\\Api\\'); + if (str_contains($action, 'DocsController')) { + return false; + } + + if ($action === 'Closure') { + return true; + } + + return true; } private function resolveTag(string $uri): string diff --git a/app/Services/Frontend/ContactSubmissionService.php b/app/Services/Frontend/ContactSubmissionService.php new file mode 100644 index 00000000..71ce244f --- /dev/null +++ b/app/Services/Frontend/ContactSubmissionService.php @@ -0,0 +1,62 @@ +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, + ]; + } +} diff --git a/app/Services/Frontend/FrontendPageService.php b/app/Services/Frontend/FrontendPageService.php new file mode 100644 index 00000000..360c3f3c --- /dev/null +++ b/app/Services/Frontend/FrontendPageService.php @@ -0,0 +1,13 @@ + $name, + ]; + } +} diff --git a/app/Services/Frontend/LandingPageContextService.php b/app/Services/Frontend/LandingPageContextService.php new file mode 100644 index 00000000..3aa528b9 --- /dev/null +++ b/app/Services/Frontend/LandingPageContextService.php @@ -0,0 +1,18 @@ + (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'), + ]; + } +} diff --git a/app/Services/Frontend/LandingPageParentDashboardService.php b/app/Services/Frontend/LandingPageParentDashboardService.php new file mode 100644 index 00000000..04b4b260 --- /dev/null +++ b/app/Services/Frontend/LandingPageParentDashboardService.php @@ -0,0 +1,148 @@ +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(); + } +} diff --git a/app/Services/Frontend/LandingPageRoleService.php b/app/Services/Frontend/LandingPageRoleService.php new file mode 100644 index 00000000..54f82436 --- /dev/null +++ b/app/Services/Frontend/LandingPageRoleService.php @@ -0,0 +1,31 @@ +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'; + } +} diff --git a/app/Services/Frontend/LandingPageService.php b/app/Services/Frontend/LandingPageService.php new file mode 100644 index 00000000..7bdb6376 --- /dev/null +++ b/app/Services/Frontend/LandingPageService.php @@ -0,0 +1,90 @@ +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', + ]; + } +} diff --git a/app/Services/Frontend/LandingPageTeacherSummaryService.php b/app/Services/Frontend/LandingPageTeacherSummaryService.php new file mode 100644 index 00000000..bd2fe95a --- /dev/null +++ b/app/Services/Frontend/LandingPageTeacherSummaryService.php @@ -0,0 +1,595 @@ +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), + ]; + } +} diff --git a/app/Services/Frontend/ProfileIconService.php b/app/Services/Frontend/ProfileIconService.php new file mode 100644 index 00000000..97be1848 --- /dev/null +++ b/app/Services/Frontend/ProfileIconService.php @@ -0,0 +1,35 @@ +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 : '??', + ]; + } +} diff --git a/app/Services/Frontend/StaticPageService.php b/app/Services/Frontend/StaticPageService.php new file mode 100644 index 00000000..eea1eca6 --- /dev/null +++ b/app/Services/Frontend/StaticPageService.php @@ -0,0 +1,33 @@ + 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, + ]; + } +} diff --git a/app/Services/Messaging/MessageCommandService.php b/app/Services/Messaging/MessageCommandService.php new file mode 100644 index 00000000..d4a8b54a --- /dev/null +++ b/app/Services/Messaging/MessageCommandService.php @@ -0,0 +1,126 @@ +recipientIdFromRole($payload['recipient_role']); + } + + if (!$recipientId) { + throw new RuntimeException('Recipient is required.'); + } + + if ((int) $recipientId === $senderId) { + throw new RuntimeException('Cannot send a message to yourself.'); + } + + return DB::transaction(function () use ($senderId, $payload, $recipientId, $attachment) { + $path = null; + if ($attachment) { + $path = $attachment->store('messages', ['disk' => 'public']); + } + + $message = Message::query()->create([ + 'sender_id' => $senderId, + 'recipient_id' => (int) $recipientId, + 'subject' => (string) $payload['subject'], + 'message' => (string) $payload['message'], + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-' . now()->format('YmdHis'), + 'priority' => $payload['priority'] ?? 'normal', + 'attachment' => $path, + 'status' => $payload['status'] ?? 'sent', + 'semester' => $payload['semester'] ?? $this->configService->getSemester() ?? '', + 'school_year' => $payload['school_year'] ?? $this->configService->getSchoolYear() ?? '', + ]); + + if (!$message) { + throw new RuntimeException('Failed to send message.'); + } + + return $message; + }); + } + + public function update(Message $message, array $payload): Message + { + return DB::transaction(function () use ($message, $payload) { + $updates = []; + if (array_key_exists('status', $payload)) { + $updates['status'] = $payload['status']; + } + if (array_key_exists('priority', $payload)) { + $updates['priority'] = $payload['priority']; + } + if (array_key_exists('read_status', $payload)) { + $read = (int) $payload['read_status']; + $updates['read_status'] = $read; + if ($read === 1) { + $updates['read_datetime'] = now(); + } + } + + if (!empty($updates)) { + $message->fill($updates); + $message->save(); + } + + return $message->fresh(); + }); + } + + public function trash(Message $message): bool + { + return DB::transaction(function () use ($message) { + $message->status = 'trashed'; + return (bool) $message->save(); + }); + } + + public function markRead(Message $message): bool + { + if ($message->read_status) { + return true; + } + + $message->read_status = 1; + $message->read_datetime = now(); + + return (bool) $message->save(); + } + + private function recipientIdFromRole(string $roleName): int + { + $map = [ + 'teacher' => 1, + 'parent' => 2, + 'admin' => 3, + 'student' => 4, + 'guest' => 5, + 'administrator' => 6, + ]; + + $key = strtolower(trim($roleName)); + if (!isset($map[$key])) { + throw new RuntimeException('Invalid recipient role.'); + } + + return $map[$key]; + } +} diff --git a/app/Services/Messaging/MessageQueryService.php b/app/Services/Messaging/MessageQueryService.php new file mode 100644 index 00000000..ed90bfb0 --- /dev/null +++ b/app/Services/Messaging/MessageQueryService.php @@ -0,0 +1,115 @@ +with('roles')->find($userId); + if (!$user) { + return null; + } + + $role = $user->roles->sortBy('id')->first(); + return $role ? (string) $role->name : null; + } + + public function inbox(int $userId, array $filters, int $page, int $perPage): LengthAwarePaginator + { + return $this->baseQuery($filters) + ->where('recipient_id', $userId) + ->whereIn('status', ['sent', 'received']) + ->paginate($perPage, ['*'], 'page', $page); + } + + public function sent(int $userId, array $filters, int $page, int $perPage): LengthAwarePaginator + { + return $this->baseQuery($filters) + ->where('sender_id', $userId) + ->where('status', 'sent') + ->paginate($perPage, ['*'], 'page', $page); + } + + public function drafts(int $userId, array $filters, int $page, int $perPage): LengthAwarePaginator + { + return $this->baseQuery($filters) + ->where('sender_id', $userId) + ->where('status', 'draft') + ->paginate($perPage, ['*'], 'page', $page); + } + + public function trash(int $userId, array $filters, int $page, int $perPage): LengthAwarePaginator + { + return $this->baseQuery($filters) + ->where('status', 'trashed') + ->where(function ($q) use ($userId) { + $q->where('sender_id', $userId) + ->orWhere('recipient_id', $userId); + }) + ->paginate($perPage, ['*'], 'page', $page); + } + + public function receivedAndMarkRead(int $userId): array + { + $messages = Message::query() + ->with(['sender', 'recipient']) + ->where('recipient_id', $userId) + ->orderByDesc('sent_datetime') + ->get(); + + $ids = $messages + ->where('read_status', false) + ->pluck('id') + ->all(); + + if (!empty($ids)) { + Message::query() + ->whereIn('id', $ids) + ->update([ + 'read_status' => 1, + 'read_datetime' => now(), + ]); + } + + return $messages->all(); + } + + public function findForUser(int $messageId): ?Message + { + return Message::query() + ->with(['sender', 'recipient']) + ->find($messageId); + } + + private function baseQuery(array $filters) + { + $query = Message::query()->with(['sender', 'recipient']); + + if (!empty($filters['priority'])) { + $query->where('priority', $filters['priority']); + } + + if (isset($filters['read_status'])) { + $query->where('read_status', (int) $filters['read_status']); + } + + if (!empty($filters['search'])) { + $term = '%' . $filters['search'] . '%'; + $query->where(function ($q) use ($term) { + $q->where('subject', 'like', $term) + ->orWhere('message', 'like', $term); + }); + } + + $sortBy = $filters['sort_by'] ?? 'sent_datetime'; + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + $query->orderBy($sortBy, $sortDir); + + return $query; + } +} diff --git a/app/Services/Messaging/MessageRecipientService.php b/app/Services/Messaging/MessageRecipientService.php new file mode 100644 index 00000000..f75c6cdf --- /dev/null +++ b/app/Services/Messaging/MessageRecipientService.php @@ -0,0 +1,90 @@ +select('teacher_class.teacher_id', 'users.firstname', 'users.lastname') + ->join('users', 'users.id', '=', 'teacher_class.teacher_id') + ->orderBy('users.firstname') + ->orderBy('users.lastname') + ->get(); + + return $rows + ->map(fn ($row) => [ + 'id' => (int) $row->teacher_id, + 'name' => trim($row->firstname . ' ' . $row->lastname), + ]) + ->unique('id') + ->values() + ->all(); + } + + public function parents(): array + { + $sectionIds = TeacherClass::query()->pluck('class_section_id')->filter()->unique()->values()->all(); + if (empty($sectionIds)) { + return []; + } + + $studentIds = StudentClass::query() + ->active() + ->whereIn('student_class.class_section_id', $sectionIds) + ->pluck('student_class.student_id') + ->filter() + ->unique() + ->values() + ->all(); + + if (empty($studentIds)) { + return []; + } + + $parentIds = Student::query() + ->whereIn('id', $studentIds) + ->where('is_active', 1) + ->pluck('parent_id') + ->filter() + ->unique() + ->values() + ->all(); + + if (empty($parentIds)) { + return []; + } + + $secondParentIds = ParentModel::query() + ->whereIn('firstparent_id', $parentIds) + ->pluck('secondparent_id') + ->filter() + ->unique() + ->values() + ->all(); + + $allIds = collect($parentIds)->merge($secondParentIds)->unique()->values()->all(); + + $users = User::query() + ->whereIn('id', $allIds) + ->orderBy('firstname') + ->orderBy('lastname') + ->get(); + + return $users + ->map(fn ($row) => [ + 'id' => (int) $row->id, + 'name' => trim($row->firstname . ' ' . $row->lastname), + ]) + ->unique('id') + ->values() + ->all(); + } +} diff --git a/app/Services/Navigation/NavBuilderService.php b/app/Services/Navigation/NavBuilderService.php new file mode 100644 index 00000000..24dda604 --- /dev/null +++ b/app/Services/Navigation/NavBuilderService.php @@ -0,0 +1,216 @@ +orderBy('menu_parent_id', 'asc') + ->orderBy('label', 'asc') + ->orderBy('id', 'asc') + ->get() + ->toArray(); + + $byId = []; + foreach ($all as $row) { + $row['children'] = []; + $byId[$row['id']] = $row; + } + + $tree = []; + foreach ($byId as $id => &$node) { + $pid = $node['menu_parent_id'] ?: null; + if ($pid && isset($byId[$pid])) { + $byId[$pid]['children'][] = &$node; + } else { + $tree[] = &$node; + } + } + unset($node); + + $this->sortTreeAlpha($tree); + + $flatAlpha = $all; + usort($flatAlpha, fn ($a, $b) => strnatcasecmp($this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? ''))); + + $roleAssignments = $this->buildRoleAssignments(); + $flattened = $this->flattenTreeForResponse($tree, $roleAssignments); + + $parentOptions = array_map(static function ($row) { + return [ + 'id' => (int) ($row['id'] ?? 0), + 'label' => (string) ($row['label'] ?? ''), + ]; + }, $flatAlpha); + + $roles = Role::query()->orderBy('name')->get(['id', 'name'])->toArray(); + + return [ + 'items' => $flattened, + 'roles' => $roles, + 'parentOptions' => $parentOptions, + ]; + } + + public function save(array $payload): array + { + $id = (int) ($payload['id'] ?? 0); + $menuParentId = $payload['menu_parent_id'] ?? $payload['parent_id'] ?? null; + $menuParentId = ($menuParentId === '' || $menuParentId === null) ? null : (int) $menuParentId; + if ($id && $menuParentId === $id) { + $menuParentId = null; + } + + if ($menuParentId !== null && !NavItem::query()->whereKey($menuParentId)->exists()) { + return ['ok' => false, 'message' => 'Selected parent does not exist.']; + } + + $data = [ + 'menu_parent_id' => $menuParentId, + 'label' => trim((string) ($payload['label'] ?? '')), + 'url' => trim((string) ($payload['url'] ?? '')) ?: null, + 'icon_class' => trim((string) ($payload['icon_class'] ?? '')) ?: null, + 'target' => trim((string) ($payload['target'] ?? '')) ?: null, + 'sort_order' => (int) ($payload['sort_order'] ?? 0), + 'is_enabled' => !empty($payload['is_enabled']) ? 1 : 0, + ]; + + $roleIds = array_values(array_unique(array_filter( + array_map('intval', (array) ($payload['roles'] ?? [])), + fn ($v) => $v > 0 + ))); + + $navItemId = null; + + DB::transaction(function () use ($id, $data, $roleIds, &$navItemId): void { + if ($id > 0) { + NavItem::query()->whereKey($id)->update($data); + $navItemId = $id; + } else { + $navItemId = (int) NavItem::query()->create($data)->id; + } + + RoleNavItem::query()->where('nav_item_id', $navItemId)->delete(); + foreach ($roleIds as $rid) { + RoleNavItem::query()->create([ + 'role_id' => $rid, + 'nav_item_id' => $navItemId, + ]); + } + }); + + $this->navbarService->clearCache(); + + return ['ok' => true, 'id' => $navItemId]; + } + + public function delete(int $id): bool + { + $deleted = (bool) NavItem::query()->whereKey($id)->delete(); + $this->navbarService->clearCache(); + + return $deleted; + } + + public function reorder(array $orders): void + { + DB::transaction(function () use ($orders): void { + foreach ($orders as $id => $order) { + NavItem::query()->whereKey((int) $id)->update(['sort_order' => (int) $order]); + } + }); + + $this->navbarService->clearCache(); + } + + private function buildRoleAssignments(): array + { + $rows = RoleNavItem::query() + ->select('role_nav_items.nav_item_id', 'roles.id as role_id', 'roles.name as role_name') + ->join('roles', 'roles.id', '=', 'role_nav_items.role_id') + ->get() + ->toArray(); + + $roleAssignments = []; + foreach ($rows as $row) { + $navId = (int) ($row['nav_item_id'] ?? 0); + if ($navId <= 0) { + continue; + } + + $roleId = (int) ($row['role_id'] ?? 0); + $roleName = $row['role_name'] ?? ($roleId ? ('#' . $roleId) : null); + + if ($roleId > 0) { + $roleAssignments[$navId]['ids'][] = $roleId; + } + if ($roleName !== null) { + $roleAssignments[$navId]['names'][] = $roleName; + } + } + + return $roleAssignments; + } + + private function flattenTreeForResponse(array $tree, array $roleAssignments, int $depth = 0): array + { + $output = []; + foreach ($tree as $node) { + $id = (int) ($node['id'] ?? 0); + $roles = $roleAssignments[$id] ?? ['ids' => [], 'names' => []]; + + $output[] = [ + 'id' => $id, + 'label' => (string) ($node['label'] ?? ''), + 'url' => $node['url'] ?? null, + 'icon_class' => $node['icon_class'] ?? null, + 'target' => $node['target'] ?? null, + 'menu_parent_id' => $node['menu_parent_id'] ?? null, + 'sort_order' => (int) ($node['sort_order'] ?? 0), + 'is_enabled' => (int) ($node['is_enabled'] ?? 0), + 'depth' => $depth, + 'roles' => [ + 'ids' => array_values(array_unique($roles['ids'] ?? [])), + 'names' => array_values(array_unique($roles['names'] ?? [])), + ], + ]; + + if (!empty($node['children'])) { + $output = array_merge($output, $this->flattenTreeForResponse($node['children'], $roleAssignments, $depth + 1)); + } + } + + return $output; + } + + private function labelKey(string $s): string + { + $s = trim(preg_replace('/\s+/', ' ', $s)); + $s = preg_replace('/^[^[:alnum:]]+/u', '', $s); + $s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); + return mb_strtolower($s, 'UTF-8'); + } + + private function sortTreeAlpha(array &$nodes): void + { + usort($nodes, fn ($a, $b) => strnatcasecmp($this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? ''))); + foreach ($nodes as &$node) { + if (!empty($node['children'])) { + $this->sortTreeAlpha($node['children']); + } + } + unset($node); + } +} diff --git a/app/Services/Navigation/NavbarService.php b/app/Services/Navigation/NavbarService.php new file mode 100644 index 00000000..6ee84f6c --- /dev/null +++ b/app/Services/Navigation/NavbarService.php @@ -0,0 +1,55 @@ + strtolower(trim((string) $r)), $roles))); + $cacheKey = 'navbar_' . md5(json_encode($roles)); + + return Cache::remember($cacheKey, 300, function () use ($roles) { + $allowedIds = RoleNavItem::getNavItemIdsForRoles($roles); + if (empty($allowedIds)) { + return []; + } + + $rows = NavItem::query() + ->where('is_enabled', 1) + ->orderBy('sort_order', 'asc') + ->get() + ->toArray(); + + $byId = []; + foreach ($rows as $row) { + if (in_array((int) $row['id'], $allowedIds, true)) { + $row['children'] = []; + $byId[$row['id']] = $row; + } + } + + $tree = []; + foreach ($byId as $id => &$node) { + $pid = $node['menu_parent_id'] ?? null; + if ($pid && isset($byId[$pid])) { + $byId[$pid]['children'][] = &$node; + } else { + $tree[] = &$node; + } + } + unset($node); + + return $tree; + }); + } + + public function clearCache(): void + { + Cache::flush(); + } +} diff --git a/app/Services/Payments/PaypalPaymentService.php b/app/Services/Payments/PaypalPaymentService.php index 0fdf387c..a1ada492 100644 --- a/app/Services/Payments/PaypalPaymentService.php +++ b/app/Services/Payments/PaypalPaymentService.php @@ -3,6 +3,7 @@ namespace App\Services\Payments; use App\Models\Payment; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; class PaypalPaymentService @@ -55,12 +56,13 @@ class PaypalPaymentService $paypalPayment->setRedirectUrls($redirectUrls); $paypalPayment->create($apiContext); - session()->put('paypalPaymentId', $paypalPayment->getId()); - session()->put('paymentId', $paymentId); + Cache::put($this->cacheKey((string) $paypalPayment->getId()), $paymentId, now()->addHours(2)); return [ 'redirect_url' => $paypalPayment->getApprovalLink(), 'mode' => 'sdk', + 'paypal_payment_id' => $paypalPayment->getId(), + 'payment_id' => $paymentId, ]; } catch (\Throwable $e) { Log::error('PayPal create payment failed: ' . $e->getMessage()); @@ -68,27 +70,33 @@ class PaypalPaymentService } } - public function executePayment(string $payerId, ?string $paypalPaymentId = null): void + public function executePayment(string $payerId, ?string $paypalPaymentId = null, ?int $paymentId = null): void { if (!$this->sdkAvailable()) { throw new \RuntimeException('PayPal SDK not installed.'); } - $paymentId = $paypalPaymentId ?: (string) session()->get('paypalPaymentId'); - if (!$paymentId) { + $paypalId = $paypalPaymentId ?: ''; + if ($paypalId === '') { throw new \RuntimeException('Missing PayPal payment ID.'); } $apiContext = $this->buildApiContext(); - $payment = \PayPal\Api\Payment::get($paymentId, $apiContext); + $payment = \PayPal\Api\Payment::get($paypalId, $apiContext); $execution = new \PayPal\Api\PaymentExecution(); $execution->setPayerId($payerId); $payment->execute($execution, $apiContext); - $internalPaymentId = (int) session()->get('paymentId'); + $internalPaymentId = $paymentId; + if (!$internalPaymentId && $paypalId !== '') { + $internalPaymentId = (int) Cache::get($this->cacheKey($paypalId)); + } if ($internalPaymentId) { Payment::query()->whereKey($internalPaymentId)->update(['status' => 'Completed']); + if ($paypalId !== '') { + Cache::forget($this->cacheKey($paypalId)); + } } } @@ -120,4 +128,9 @@ class PaypalPaymentService return $apiContext; } + + private function cacheKey(string $paypalPaymentId): string + { + return 'paypal_payment:' . $paypalPaymentId; + } } diff --git a/app/Services/Phone/PhoneFormatterService.php b/app/Services/Phone/PhoneFormatterService.php new file mode 100644 index 00000000..aa7a6597 --- /dev/null +++ b/app/Services/Phone/PhoneFormatterService.php @@ -0,0 +1,22 @@ + 'school_policy.html', + 'picture' => 'picture_policy.html', + ]; + + public function getPolicy(string $type): array + { + $type = strtolower(trim($type)); + if (!isset(self::TYPES[$type])) { + throw new \InvalidArgumentException('Unsupported policy type.'); + } + + $filename = self::TYPES[$type]; + $path = resource_path('policies/' . $filename); + + if (!is_file($path)) { + Log::warning('Policy file missing.', ['type' => $type, 'path' => $path]); + return [ + 'type' => $type, + 'title' => $this->titleFor($type), + 'content' => '', + 'format' => 'html', + 'source' => $path, + 'updated_at' => null, + ]; + } + + return [ + 'type' => $type, + 'title' => $this->titleFor($type), + 'content' => (string) file_get_contents($path), + 'format' => 'html', + 'source' => $path, + 'updated_at' => date('Y-m-d H:i:s', filemtime($path)), + ]; + } + + private function titleFor(string $type): string + { + return $type === 'picture' ? 'Picture Policy' : 'School Policy'; + } +} diff --git a/app/Services/Preferences/PreferencesCommandService.php b/app/Services/Preferences/PreferencesCommandService.php new file mode 100644 index 00000000..ff629909 --- /dev/null +++ b/app/Services/Preferences/PreferencesCommandService.php @@ -0,0 +1,56 @@ +mapPayload($payload); + $data['user_id'] = $userId; + + return DB::transaction(function () use ($userId, $data) { + $existing = Preferences::query()->where('user_id', $userId)->first(); + + if ($existing) { + $existing->fill($data); + $existing->save(); + return $existing->refresh(); + } + + return Preferences::query()->create($data); + }); + } + + public function delete(Preferences $preferences): bool + { + return (bool) $preferences->delete(); + } + + private function mapPayload(array $payload): array + { + $email = $payload['receive_email_notifications'] ?? $payload['notification_email'] ?? null; + $sms = $payload['receive_sms_notifications'] ?? $payload['notification_sms'] ?? null; + + $style = $this->options->normalizeStyle($payload['style_color'] ?? null); + $menu = $this->options->normalizeMenu($payload['menu_color'] ?? null); + + $data = [ + 'notification_email' => $email !== null ? (int) filter_var($email, FILTER_VALIDATE_BOOLEAN) : null, + 'notification_sms' => $sms !== null ? (int) filter_var($sms, FILTER_VALIDATE_BOOLEAN) : null, + 'theme' => $payload['theme'] ?? null, + 'language' => $payload['language'] ?? null, + 'style_color' => $style, + 'menu_color' => $menu, + ]; + + return array_filter($data, static fn ($value) => $value !== null); + } +} diff --git a/app/Services/Preferences/PreferencesOptionsService.php b/app/Services/Preferences/PreferencesOptionsService.php new file mode 100644 index 00000000..09cf286f --- /dev/null +++ b/app/Services/Preferences/PreferencesOptionsService.php @@ -0,0 +1,48 @@ + true, + 'receive_sms_notifications' => true, + 'theme' => 'light', + 'language' => 'en', + 'style_color' => 'blue', + 'menu_color' => 'white', + ]; + } + + public function styleOptions(): array + { + return ['blue', 'green', 'red', 'orange', 'gray', 'black', 'white']; + } + + public function menuOptions(): array + { + return ['white', 'gray', 'black', 'blue', 'green']; + } + + public function normalizeStyle(?string $style): ?string + { + if (!$style) { + return null; + } + + $style = trim($style); + return in_array($style, $this->styleOptions(), true) ? $style : null; + } + + public function normalizeMenu(?string $menu): ?string + { + if (!$menu) { + return null; + } + + $menu = trim($menu); + return in_array($menu, $this->menuOptions(), true) ? $menu : null; + } +} diff --git a/app/Services/Preferences/PreferencesQueryService.php b/app/Services/Preferences/PreferencesQueryService.php new file mode 100644 index 00000000..bd546226 --- /dev/null +++ b/app/Services/Preferences/PreferencesQueryService.php @@ -0,0 +1,83 @@ +where('user_id', $userId)->first(); + + $base = $this->options->defaultPreferences(); + if (!$row) { + return [ + 'preferences' => $base, + 'options' => $this->optionsPayload(), + ]; + } + + return [ + 'preferences' => array_merge($base, $this->mapRow($row)), + 'options' => $this->optionsPayload(), + ]; + } + + public function paginate(array $filters, int $page = 1, int $perPage = 20): LengthAwarePaginator + { + $query = Preferences::query() + ->leftJoin('users', 'users.id', '=', 'user_preferences.user_id') + ->select('user_preferences.*', 'users.firstname', 'users.lastname', 'users.email'); + + if (!empty($filters['user_id'])) { + $query->where('user_preferences.user_id', (int) $filters['user_id']); + } + + if (!empty($filters['q'])) { + $term = '%' . strtolower(trim((string) $filters['q'])) . '%'; + $query->where(function ($q) use ($term) { + $q->whereRaw('LOWER(users.firstname) LIKE ?', [$term]) + ->orWhereRaw('LOWER(users.lastname) LIKE ?', [$term]) + ->orWhereRaw('LOWER(users.email) LIKE ?', [$term]); + }); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'updated_at'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + $allowedSorts = ['updated_at', 'created_at', 'user_id']; + + if (in_array($sortBy, $allowedSorts, true)) { + $query->orderBy('user_preferences.' . $sortBy, $sortDir); + } else { + $query->orderBy('user_preferences.updated_at', 'desc'); + } + + return $query->paginate($perPage, ['*'], 'page', $page); + } + + private function optionsPayload(): array + { + return [ + 'style_options' => $this->options->styleOptions(), + 'menu_options' => $this->options->menuOptions(), + ]; + } + + private function mapRow(Preferences $row): array + { + return [ + 'receive_email_notifications' => (bool) ($row->notification_email ?? false), + 'receive_sms_notifications' => (bool) ($row->notification_sms ?? false), + 'theme' => $row->theme ?? null, + 'language' => $row->language ?? null, + 'style_color' => $row->style_color ?? null, + 'menu_color' => $row->menu_color ?? null, + ]; + } +} diff --git a/app/Services/Roles/RoleSwitchService.php b/app/Services/Roles/RoleSwitchService.php index 57eba73a..0a0ecd10 100644 --- a/app/Services/Roles/RoleSwitchService.php +++ b/app/Services/Roles/RoleSwitchService.php @@ -3,7 +3,6 @@ namespace App\Services\Roles; use App\Models\UserRole; -use Illuminate\Support\Facades\Session; class RoleSwitchService { @@ -19,7 +18,6 @@ class RoleSwitchService public function switchRole(string $role): string { - Session::put('active_role', $role); return $this->dashboardService->bestDashboardRouteFor([$role]); } } diff --git a/app/Services/School/SemesterSelectionService.php b/app/Services/School/SemesterSelectionService.php index 77ead4c6..228f9907 100644 --- a/app/Services/School/SemesterSelectionService.php +++ b/app/Services/School/SemesterSelectionService.php @@ -8,16 +8,6 @@ class SemesterSelectionService { public function selectedTeacherSemester(): string { - $selected = trim((string) (session()->get('teacher_scores_selected_semester') ?? '')); - if ($selected !== '') { - return $this->normalize($selected); - } - - $fallback = trim((string) (session()->get('semester') ?? '')); - if ($fallback !== '') { - return $this->normalize($fallback); - } - $configSemester = trim((string) (Configuration::getConfig('semester') ?? '')); if ($configSemester !== '') { return $this->normalize($configSemester); diff --git a/app/Services/Scores/ScoreCommentService.php b/app/Services/Scores/ScoreCommentService.php index b4a91cf8..4204be4a 100644 --- a/app/Services/Scores/ScoreCommentService.php +++ b/app/Services/Scores/ScoreCommentService.php @@ -328,13 +328,28 @@ class ScoreCommentService private function isReviewer(): bool { - $currentUserRole = session()->get('role'); + $user = auth()->user(); + if (!$user) { + return false; + } + + $currentUserRoles = $user->roles() + ->pluck('roles.name') + ->map(fn ($name) => strtolower((string) $name)) + ->unique() + ->values() + ->toArray(); + $reviewerRoles = (string) (Configuration::getConfig('comment_reviewer') ?? ''); if ($reviewerRoles === '') { return false; } - $allowedRoles = array_map('trim', explode(',', $reviewerRoles)); - return in_array($currentUserRole, $allowedRoles, true); + $allowedRoles = array_map( + static fn ($role) => strtolower(trim((string) $role)), + explode(',', $reviewerRoles) + ); + + return !empty(array_intersect($currentUserRoles, $allowedRoles)); } } diff --git a/app/Services/Security/IpBanCommandService.php b/app/Services/Security/IpBanCommandService.php new file mode 100644 index 00000000..26b92a0f --- /dev/null +++ b/app/Services/Security/IpBanCommandService.php @@ -0,0 +1,81 @@ +findRow($id, $ip); + if (!$row) { + return null; + } + + $blockedUntil = $this->addHours($hours); + $attempts = max((int) $row->attempts, 10); + + return DB::transaction(function () use ($row, $blockedUntil, $attempts) { + $row->blocked_until = $blockedUntil; + $row->attempts = $attempts; + $row->save(); + return $row->refresh(); + }); + } + + public function unbanOne(?int $id, ?string $ip): ?IpAttempt + { + $row = $this->findRow($id, $ip); + if (!$row) { + return null; + } + + return DB::transaction(function () use ($row) { + $row->blocked_until = null; + $row->attempts = 0; + $row->save(); + return $row->refresh(); + }); + } + + public function unbanAll(): int + { + return (int) IpAttempt::query() + ->where('blocked_until', '>', $this->utcNow()) + ->update([ + 'blocked_until' => null, + 'attempts' => 0, + ]); + } + + private function findRow(?int $id, ?string $ip): ?IpAttempt + { + if ($id && $id > 0) { + return IpAttempt::query()->find($id); + } + if ($ip !== null && $ip !== '') { + return IpAttempt::query()->where('ip_address', $ip)->first(); + } + + return null; + } + + private function utcNow(): string + { + if (function_exists('utc_now')) { + return (string) utc_now(); + } + return now('UTC')->toDateTimeString(); + } + + private function addHours(int $hours): string + { + $hours = max(1, $hours); + if (function_exists('utc_now')) { + return (string) date('Y-m-d H:i:s', strtotime("+{$hours} hours", strtotime((string) utc_now()))); + } + return now('UTC')->addHours($hours)->toDateTimeString(); + } +} diff --git a/app/Services/Security/IpBanQueryService.php b/app/Services/Security/IpBanQueryService.php new file mode 100644 index 00000000..d3468abb --- /dev/null +++ b/app/Services/Security/IpBanQueryService.php @@ -0,0 +1,46 @@ +orderBy('updated_at', 'desc'); + + $status = strtolower(trim((string) ($filters['status'] ?? 'all'))); + if ($status === 'active') { + $query->where('blocked_until', '>', $this->utcNow()); + } + + if (!empty($filters['search'])) { + $term = '%' . trim((string) $filters['search']) . '%'; + $query->where('ip_address', 'like', $term); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'updated_at'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + $allowedSorts = ['updated_at', 'blocked_until', 'attempts', 'ip_address', 'last_attempt_at']; + if (in_array($sortBy, $allowedSorts, true)) { + $query->orderBy($sortBy, $sortDir); + } + + return $query->paginate($perPage, ['*'], 'page', $page); + } + + public function find(int $id): ?IpAttempt + { + return IpAttempt::query()->find($id); + } + + private function utcNow(): string + { + if (function_exists('utc_now')) { + return (string) utc_now(); + } + return now('UTC')->toDateTimeString(); + } +} diff --git a/app/Services/Settings/SettingsService.php b/app/Services/Settings/SettingsService.php new file mode 100644 index 00000000..045ae1a4 --- /dev/null +++ b/app/Services/Settings/SettingsService.php @@ -0,0 +1,48 @@ + $settings->name, + 'timezone' => $settings->timezone, + 'administrator_email' => Configuration::getConfig('administrator_email') ?? '', + ]; + } + + public function update(array $payload, int $userId): array + { + return DB::transaction(function () use ($payload, $userId) { + $settings = Setting::singleton(); + + $updates = []; + if (array_key_exists('site_name', $payload)) { + $updates['name'] = $payload['site_name']; + } + if (array_key_exists('timezone', $payload)) { + $updates['timezone'] = $payload['timezone']; + } + + if (!empty($updates)) { + $updates['updated_by'] = $userId; + $settings->fill($updates); + $settings->save(); + } + + if (array_key_exists('administrator_email', $payload)) { + Configuration::setConfigValueByKey('administrator_email', (string) $payload['administrator_email']); + } + + return $this->get(); + }); + } +} diff --git a/app/Services/Staff/StaffCommandService.php b/app/Services/Staff/StaffCommandService.php new file mode 100644 index 00000000..9aab50c5 --- /dev/null +++ b/app/Services/Staff/StaffCommandService.php @@ -0,0 +1,100 @@ +resolveUserId($payload); + + $roleName = (string) ($payload['role_name'] ?? ''); + $activeRole = strtolower($roleName); + if (!empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { + $activeRole = 'inactive'; + } + + $schoolYear = (string) ($payload['school_year'] ?? $this->configService->getSchoolYear() ?? ''); + + $staff = Staff::query()->create([ + 'user_id' => $userId, + 'firstname' => (string) $payload['firstname'], + 'lastname' => (string) $payload['lastname'], + 'email' => (string) $payload['email'], + 'phone' => $payload['phone'] ?? null, + 'role_name' => $roleName, + 'active_role' => $activeRole, + 'school_year' => $schoolYear, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + if (!$staff) { + throw new RuntimeException('Failed to create staff member.'); + } + + return $staff; + }); + } + + public function update(Staff $staff, array $payload): Staff + { + return DB::transaction(function () use ($staff, $payload) { + $updates = []; + foreach (['firstname', 'lastname', 'email', 'phone', 'role_name', 'school_year'] as $field) { + if (array_key_exists($field, $payload) && $payload[$field] !== '') { + $updates[$field] = $payload[$field]; + } + } + + if (array_key_exists('role_name', $updates)) { + $updates['active_role'] = strtolower((string) $updates['role_name']); + } + if (!empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { + $updates['active_role'] = 'inactive'; + } + + $updates['updated_at'] = now(); + + if (!empty($updates)) { + $staff->fill($updates); + $staff->save(); + } + + return $staff->fresh(); + }); + } + + public function delete(Staff $staff): bool + { + return (bool) $staff->delete(); + } + + private function resolveUserId(array $payload): int + { + if (!empty($payload['user_id'])) { + return (int) $payload['user_id']; + } + + $email = $payload['email'] ?? null; + if ($email) { + $userId = User::query()->where('email', $email)->value('id'); + if ($userId) { + return (int) $userId; + } + } + + throw new RuntimeException('A valid user_id or existing email is required.'); + } +} diff --git a/app/Services/Staff/StaffQueryService.php b/app/Services/Staff/StaffQueryService.php new file mode 100644 index 00000000..5e465d95 --- /dev/null +++ b/app/Services/Staff/StaffQueryService.php @@ -0,0 +1,112 @@ +whereNotIn(DB::raw('LOWER(active_role)'), $excluded); + + if (!empty($filters['role'])) { + $role = strtolower(trim((string) $filters['role'])); + $query->whereRaw('LOWER(active_role) = ?', [$role]); + } + + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + + $paginator = $query + ->orderBy($sortBy, $sortDir) + ->paginate($perPage, ['*'], 'page', $page); + + $schoolYear = (string) ($filters['school_year'] ?? $this->configService->getSchoolYear() ?? ''); + $semester = (string) ($filters['semester'] ?? $this->configService->getSemester() ?? ''); + + $assignByTeacher = $this->buildAssignments($schoolYear); + + $issuesCount = 0; + $items = $paginator->getCollection()->map(function ($staff) use ($assignByTeacher, &$issuesCount) { + $staff->school_id = User::getSchoolIdByUserId((int) ($staff->user_id ?? 0)); + + $role = strtolower((string) ($staff->active_role ?? '')); + if (in_array($role, ['teacher', 'teacher_assistant'], true)) { + $tid = (int) ($staff->user_id ?? 0); + $labels = $assignByTeacher[$tid] ?? []; + if (!empty($labels)) { + $staff->class_section = implode(', ', array_unique($labels)); + $staff->verification_issue = false; + } else { + $staff->class_section = 'No class assigned'; + $staff->verification_issue = true; + $issuesCount++; + } + } else { + $staff->class_section = '—'; + $staff->verification_issue = false; + } + + return $staff; + }); + + $paginator->setCollection($items); + + return [ + 'paginator' => $paginator, + 'issues_count' => $issuesCount, + 'semester' => $semester, + 'school_year' => $schoolYear, + ]; + } + + public function find(int $id): ?Staff + { + return Staff::query()->find($id); + } + + private function buildAssignments(string $schoolYear): array + { + if ($schoolYear === '') { + return []; + } + + $rows = TeacherClass::query() + ->select('teacher_class.teacher_id', 'teacher_class.position', 'classSection.class_section_name') + ->leftJoin('classSection', 'classSection.class_section_id', '=', 'teacher_class.class_section_id') + ->where('teacher_class.school_year', $schoolYear) + ->get(); + + $assignByTeacher = []; + foreach ($rows as $row) { + $tid = (int) ($row->teacher_id ?? 0); + if ($tid <= 0) { + continue; + } + $name = trim((string) ($row->class_section_name ?? '')); + if ($name === '') { + continue; + } + $pos = strtolower((string) ($row->position ?? '')); + if (!in_array($pos, ['main', 'ta'], true)) { + continue; + } + $assignByTeacher[$tid][] = $name . ' (' . $pos . ')'; + } + + return $assignByTeacher; + } +} diff --git a/app/Services/Support/ContactMessageService.php b/app/Services/Support/ContactMessageService.php new file mode 100644 index 00000000..4ca3882d --- /dev/null +++ b/app/Services/Support/ContactMessageService.php @@ -0,0 +1,41 @@ +From: {$name} ({$email})

"; + $formatted .= "

Subject: {$subject}

"; + $formatted .= '

' . nl2br($message) . '

'; + + $recipient = (string) config('support.contact_email', 'support@alrahmaisgl.org'); + + $emailSent = false; + if (!app()->runningUnitTests()) { + $emailSent = $this->emailService->send($recipient, $subject, $formatted, null, $email, $name); + } + + if (!$emailSent) { + Log::warning('Contact email failed for ' . $email); + } + + return [ + 'email_sent' => $emailSent, + 'recipient' => $recipient, + ]; + } +} diff --git a/app/Services/Support/SupportRequestService.php b/app/Services/Support/SupportRequestService.php new file mode 100644 index 00000000..a70e90cf --- /dev/null +++ b/app/Services/Support/SupportRequestService.php @@ -0,0 +1,80 @@ +with('user'); + + if (!$asAdmin) { + $query->where('user_id', $userId); + } elseif (!empty($filters['user_id'])) { + $query->where('user_id', (int) $filters['user_id']); + } + + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; + + return $query->orderBy($sortBy, $sortDir) + ->paginate($perPage, ['*'], 'page', $page); + } + + public function create(int $userId, array $payload): array + { + $semester = $this->configService->getSemester() ?? ''; + $schoolYear = $this->configService->getSchoolYear() ?? ''; + + return DB::transaction(function () use ($userId, $payload, $semester, $schoolYear) { + $record = SupportRequest::query()->create([ + 'user_id' => $userId, + 'subject' => (string) $payload['subject'], + 'message' => (string) $payload['message'], + 'status' => 'open', + 'semester' => $semester ?: 'Fall', + 'school_year' => $schoolYear ?: '2025-2026', + ]); + + $recipient = (string) config('support.contact_email', 'support@alrahmaisgl.org'); + $emailSent = false; + if (!app()->runningUnitTests()) { + $emailSent = $this->emailService->send( + $recipient, + (string) $payload['subject'], + nl2br((string) $payload['message']), + null, + (string) ($payload['user_email'] ?? null), + (string) ($payload['user_name'] ?? null) + ); + } + + if (!$emailSent) { + Log::warning('Support email failed for user ' . $userId); + } + + return [ + 'record' => $record, + 'email_sent' => $emailSent, + 'recipient' => $recipient, + ]; + }); + } +} diff --git a/app/Services/System/CleanupSchedulerService.php b/app/Services/System/CleanupSchedulerService.php new file mode 100644 index 00000000..31e299e7 --- /dev/null +++ b/app/Services/System/CleanupSchedulerService.php @@ -0,0 +1,29 @@ +diffInMinutes($lastRun) >= $minutes; + } + + public function markRun(string $cacheKey): void + { + Cache::put($cacheKey, now(), now()->addDay()); + } + + public function runUnverifiedCleanup(): void + { + Artisan::call('users:delete-unverified'); + } +} diff --git a/app/Services/System/DatabaseHealthService.php b/app/Services/System/DatabaseHealthService.php new file mode 100644 index 00000000..53895117 --- /dev/null +++ b/app/Services/System/DatabaseHealthService.php @@ -0,0 +1,24 @@ + false, + 'error' => $e->getMessage(), + ]; + } + + return [ + 'ok' => true, + ]; + } +} diff --git a/app/Services/System/HealthCheckService.php b/app/Services/System/HealthCheckService.php new file mode 100644 index 00000000..1ecb2c31 --- /dev/null +++ b/app/Services/System/HealthCheckService.php @@ -0,0 +1,64 @@ + $uploadsBase, + 'uploads/reimbursements' => $uploadsBase . DIRECTORY_SEPARATOR . 'reimbursements', + 'uploads/receipts' => $uploadsBase . DIRECTORY_SEPARATOR . 'receipts', + 'uploads/checks' => $uploadsBase . DIRECTORY_SEPARATOR . 'checks', + 'uploads/early_dismissal_signatures' => $uploadsBase . DIRECTORY_SEPARATOR . 'early_dismissal_signatures', + ]; + + $pathsStatus = []; + foreach ($paths as $label => $path) { + $pathsStatus[] = [ + 'label' => $label, + 'path' => $path, + 'exists' => is_dir($path), + 'writable' => is_writable($path), + ]; + } + + $dbChecks = [ + 'user_preferences_exists' => Schema::hasTable('user_preferences'), + 'settings_exists' => Schema::hasTable('settings'), + 'migrations_exists' => Schema::hasTable('migrations'), + ]; + + $dbChecks['user_preferences_has_timezone'] = $dbChecks['user_preferences_exists'] + ? Schema::hasColumn('user_preferences', 'timezone') + : false; + + $dbChecks['settings_has_timezone'] = $dbChecks['settings_exists'] + ? Schema::hasColumn('settings', 'timezone') + : false; + + $okPaths = array_reduce($pathsStatus, function (bool $carry, array $row) { + return $carry && $row['exists'] && $row['writable']; + }, true); + + $okDb = true; + if ($dbChecks['user_preferences_exists'] && !$dbChecks['user_preferences_has_timezone']) { + $okDb = false; + } + if ($dbChecks['settings_exists'] && !$dbChecks['settings_has_timezone']) { + $okDb = false; + } + + return [ + 'ok' => $okPaths && $okDb, + 'paths' => $pathsStatus, + 'database' => $dbChecks, + 'write_path' => storage_path(), + 'timestamp' => now()->toIso8601String(), + ]; + } +} diff --git a/app/Services/System/TimeService.php b/app/Services/System/TimeService.php index 87190d6d..2a90a6ed 100644 --- a/app/Services/System/TimeService.php +++ b/app/Services/System/TimeService.php @@ -20,6 +20,18 @@ class TimeService return (string) (config('app.timezone') ?: 'UTC'); } + public function primeFromRequest($request): void + { + $headerTz = (string) ($request?->header('X-Timezone') ?? ''); + if ($headerTz === '') { + $headerTz = (string) ($request?->header('Time-Zone') ?? ''); + } + + if ($headerTz !== '' && in_array($headerTz, \DateTimeZone::listIdentifiers(), true)) { + // No session-backed storage; header is read per request if needed. + } + } + public function serverTimezone(): string { return (string) (config('app.timezone') ?: 'UTC'); diff --git a/app/Services/Ui/UiStyleService.php b/app/Services/Ui/UiStyleService.php new file mode 100644 index 00000000..76483dcf --- /dev/null +++ b/app/Services/Ui/UiStyleService.php @@ -0,0 +1,94 @@ +buildUpdates($payload); + + $prefs = Preferences::query()->where('user_id', $userId)->first(); + if ($prefs) { + $prefs->fill($updates)->save(); + return $prefs->fresh(); + } + + $updates['user_id'] = $userId; + return Preferences::query()->create($updates); + } + + public function buildUpdates(array $payload): array + { + $updates = []; + $accent = $this->sanitizePaletteKey($payload['accent'] ?? null); + $menu = $this->sanitizePaletteKey($payload['menu'] ?? null); + $menuBg = $this->normalizeHex($payload['menu_bg'] ?? null); + $menuTx = $this->normalizeHex($payload['menu_text'] ?? null); + $menuMode = $this->normalizeMenuMode($payload['menu_mode'] ?? null); + + $stylePalettes = (array) config('ui.style_palettes', []); + $menuPalettes = (array) config('ui.menu_palettes', []); + + if ($accent !== null && (empty($stylePalettes) || array_key_exists($accent, $stylePalettes))) { + $updates['style_color'] = $accent; + } + + if ($menu !== null && (empty($menuPalettes) || array_key_exists($menu, $menuPalettes))) { + $updates['menu_color'] = $menu; + $updates['menu_custom_bg'] = null; + $updates['menu_custom_text'] = null; + $updates['menu_custom_mode'] = null; + } + + if ($menu === 'custom' || $menuBg !== null || $menuTx !== null) { + if ($menuBg === null && $menuTx === null) { + return $updates; + } + $updates['menu_color'] = 'custom'; + $updates['menu_custom_bg'] = $menuBg ?: '#0F172A'; + $updates['menu_custom_text'] = $menuTx ?: '#FFFFFF'; + $updates['menu_custom_mode'] = $menuMode; + } + + return $updates; + } + + private function sanitizePaletteKey(?string $value): ?string + { + $value = trim((string) $value); + return $value === '' ? null : $value; + } + + private function normalizeMenuMode(?string $mode): string + { + $mode = strtolower(trim((string) $mode)); + if (in_array($mode, ['light', 'dark', 'auto'], true)) { + return $mode; + } + return 'auto'; + } + + private function normalizeHex(?string $hex): ?string + { + $h = trim((string) $hex); + if ($h === '') { + return null; + } + if ($h[0] !== '#') { + $h = '#' . $h; + } + if (preg_match('/^#([0-9a-fA-F]{3})$/', $h, $m)) { + $r = $m[1][0]; + $g = $m[1][1]; + $b = $m[1][2]; + return strtoupper('#' . $r . $r . $g . $g . $b . $b); + } + if (preg_match('/^#([0-9a-fA-F]{6})$/', $h)) { + return strtoupper($h); + } + return null; + } +} diff --git a/app/old/ApiClient.php b/app/old/ApiClient.php deleted file mode 100644 index 8dc3ae8a..00000000 --- a/app/old/ApiClient.php +++ /dev/null @@ -1,107 +0,0 @@ -http = $http; - $this->config = $config; - } - - public function get(string $uri, array $query = [], array $headers = []): array - { - return $this->request('get', $uri, [ - 'headers' => $this->mergeHeaders($headers), - 'query' => $query, - ]); - } - - public function post(string $uri, array $data = [], array $headers = []): array - { - return $this->request('post', $uri, [ - 'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']), - 'json' => $data, - ]); - } - - public function put(string $uri, array $data = [], array $headers = []): array - { - return $this->request('put', $uri, [ - 'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']), - 'json' => $data, - ]); - } - - public function delete(string $uri, array $data = [], array $headers = []): array - { - return $this->request('delete', $uri, [ - 'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']), - 'json' => $data, - ]); - } - - /** - * Low-level access for uncommon HTTP methods or options - */ - public function request(string $method, string $uri, array $options = []): array - { - $url = $this->fullUrl($uri); - $options['timeout'] = $options['timeout'] ?? $this->config->timeout; - - $response = $this->http->request(strtoupper($method), $url, $options); - - $status = $response->getStatusCode(); - $body = (string) $response->getBody(); - - $decoded = null; - if ($this->isJson($body)) { - $decoded = json_decode($body, true); - } - - return [ - 'ok' => $status >= 200 && $status < 300, - 'status' => $status, - 'headers' => $response->getHeaderLine('Content-Type'), - 'data' => $decoded, - 'raw' => $decoded === null ? $body : null, - ]; - } - - protected function fullUrl(string $uri): string - { - if (preg_match('#^https?://#i', $uri)) { - return $uri; - } - - $base = rtrim($this->config->baseURL ?? '', '/'); - if ($base === '') { - return '/' . ltrim($uri, '/'); - } - return $base . '/' . ltrim($uri, '/'); - } - - protected function mergeHeaders(array $override = [], array $extra = []): array - { - $base = $this->config->defaultHeaders ?? []; - return array_filter(array_merge($base, $extra, $override), static function ($v) { - return $v !== null && $v !== ''; - }); - } - - protected function isJson(string $string): bool - { - json_decode($string); - return json_last_error() === JSON_ERROR_NONE; - } -} - -?> - diff --git a/app/old/ClassController.php b/app/old/ClassController.php deleted file mode 100644 index 413e349b..00000000 --- a/app/old/ClassController.php +++ /dev/null @@ -1,157 +0,0 @@ -db = \Config\Database::connect(); - $this->classsectionModel = new ClassSectionModel(); - - // Load necessary models - $this->studentModel = new StudentModel(); - $this->teacherModel = new TeacherModel(); - $this->configModel = new ConfigurationModel(); - - // Get the semester from the configuration table - $this->semester = $this->configModel->getConfig('semester'); - $this->schoolYear = $this->configModel->getConfig('school_year'); - } - - public function index() - { - // Retrieve all classes from the database - $classsection = $this->classsectionModel->getAllClasses(); - // Pass the classes to the view - return view('administrator/class_section', ['classsection' => $classsection]); - } - - public function create() - { - // Show the form to create a new class - return view('administrator/create_class'); - } - - public function store() - { - // Handle the form submission to create a new class - $data = [ - 'name' => $this->request->getPost('name'), - 'description' => $this->request->getPost('description'), - ]; - //$this->classModel->createClass($data); - return redirect()->to('/administrator/classes'); - } - - public function edit($id) - { - // Retrieve the class details to edit - //$class = $this->ClassSectionModel->getClassById($id); - //return view('administrator/edit_class', ['class' => $class]); - } - - public function updateClass($id) - { - // Handle the form submission to update an existing class - $data = [ - 'name' => $this->request->getPost('name'), - 'description' => $this->request->getPost('description'), - ]; - //$this->classModel->updateClass($id, $data); - return redirect()->to('/administrator/classes'); - } - - public function destroyClass($id) - { - // Delete the class - //$this->classModel->deleteClass($id); - return redirect()->to('/administrator/classes'); - } - - public function addClasses() - { - // Include the shared database connection file - $file = __DIR__ . '/../db_connection.php'; - - if (file_exists($file)) { - require_once $file; - } else { - die("Error: Could not find the required file '$file'."); - } - - - $classes = [ - ['class_name' => 'Class 1', 'teacher_id' => 1, 'schedule' => 'Monday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 2', 'teacher_id' => 2, 'schedule' => 'Tuesday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 3', 'teacher_id' => 3, 'schedule' => 'Wednesday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 4', 'teacher_id' => 4, 'schedule' => 'Thursday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 5', 'teacher_id' => 5, 'schedule' => 'Friday 9:00 AM - 10:00 AM', 'capacity' => 30], - ['class_name' => 'Class 6', 'teacher_id' => 6, 'schedule' => 'Monday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 7', 'teacher_id' => 7, 'schedule' => 'Tuesday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 8', 'teacher_id' => 8, 'schedule' => 'Wednesday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Class 9', 'teacher_id' => 9, 'schedule' => 'Thursday 10:00 AM - 11:00 AM', 'capacity' => 30], - ['class_name' => 'Youth', 'teacher_id' => 10, 'schedule' => 'Friday 10:00 AM - 11:00 AM', 'capacity' => 30], - ]; - - foreach ($classes as $class) { - $stmt = $conn->prepare("INSERT INTO classes (class_name, teacher_id, schedule, capacity) VALUES (?, ?, ?, ?)"); - if (!$stmt) { - die("Prepare failed: (" . $conn->errno . ") " . $conn->error); - } - $stmt->bind_param("sisi", $class['class_name'], $class['teacher_id'], $class['schedule'], $class['capacity']); - $stmt->execute(); - $stmt->close(); - } - - $conn->close(); - - return redirect()->to('/parent/classes')->with('success', 'Classes added successfully.'); - } - - public function classAttendance($class_section_id) - { - // Get the teacher's ID from the session - $teacherId = session()->get('user_id'); - - // Fetch teacher data - $teacherData = $this->teacherModel->find($teacherId); - - // Fetch students for the class - $studentsData = $this->studentModel->join('student_class', 'students.id = student_class.student_id') - ->where('student_class.class_section_id', $class_section_id) - ->select('students.*, student_class.class_section_id') - ->findAll(); - - // Get class name (assuming it's stored in the students data) - $className = !empty($studentsData) && isset($studentsData[0]['class_name']) ? $studentsData[0]['class_name'] : 'Class Name Not Available'; - - // Check if the teacher data and students data are available - if (empty($teacherData) || empty($studentsData)) { - return redirect()->to('/teacher/classes')->with('error', 'No data found for this class.'); - } - - // Pass data to the view - return view('/teacher/classes', [ - 'teacher_name' => $teacherData['teacher_first'] . ' ' . $teacherData['teacher_last'], - 'students' => $studentsData, - 'class_name' => $className, - 'semester' => $this->semester, - 'class_section_id' => $class_section_id, // Pass the class_id for attendance update forms - ]); - } -} diff --git a/app/old/ContactController.php b/app/old/ContactController.php deleted file mode 100644 index 1f0a109f..00000000 --- a/app/old/ContactController.php +++ /dev/null @@ -1,66 +0,0 @@ -setRules([ - 'name' => 'required|min_length[3]', - 'email' => 'required|valid_email', - 'subject' => 'required|min_length[3]', - 'message' => 'required|min_length[10]' - ]); - - if (!$validation->withRequest($this->request)->run()) { - return view('/parent/contact', [ - 'validation' => $validation - ]); - } - - // Process form data - $name = $this->request->getPost('name'); - $email = strtolower($this->request->getPost('email')); - $subject = $this->request->getPost('subject'); - $message = $this->request->getPost('message'); - - // Initialize the EmailController - $emailController = new \App\Controllers\View\EmailController(); - - // Prepare the message to send - $formattedMessage = " -

From: {$name} ({$email})

-

Subject: {$subject}

-

{$message}

- "; - - // Send the email using EmailController - if ($emailController->sendEmail('support@alrahmaisgl.org', $subject, $formattedMessage)) { - $session = session(); - $session->setFlashdata('success', 'Thank you for contacting us! We will get back to you soon.'); - } else { - $session = session(); - $session->setFlashdata('error', 'There was an error sending your message. Please try again later.'); - } - - return redirect()->to('/parent/contact'); - } -} \ No newline at end of file diff --git a/app/old/DashboardRedirectController.php b/app/old/DashboardRedirectController.php deleted file mode 100644 index 2e00b040..00000000 --- a/app/old/DashboardRedirectController.php +++ /dev/null @@ -1,48 +0,0 @@ -get('roles') ?? []; - - // Fallback: if only a single role is stored - if (empty($roles) && $session->get('role')) { - $roles = [$session->get('role')]; - } - - // Use your existing method - return $this->redirectToDashboard($roles); - } - - // Your existing function (unchanged) - -private function redirectToDashboard(array $roles) -{ - if (empty($roles)) { - log_message('error', 'Empty roles array passed to redirectToDashboard.'); - return redirect()->to('/landing_page/guest_dashboard'); - } - - $roleModel = new RoleModel(); - - // Resolve all candidate roles (by name or slug), ordered by priority ASC - $rows = $roleModel->findByNamesOrSlugs($roles); - - if (!empty($rows)) { - $route = $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard'; - log_message('debug', 'Redirecting user to: ' . $route); - return redirect()->to($route); - } - - log_message('warning', 'No matching role found. Redirecting to guest dashboard.'); - return redirect()->to('/landing_page/guest_dashboard'); -} - -} \ No newline at end of file diff --git a/app/old/DocsController.php b/app/old/DocsController.php deleted file mode 100644 index 093ff067..00000000 --- a/app/old/DocsController.php +++ /dev/null @@ -1,25 +0,0 @@ - 'Administrator API', - 'url' => '/docs/openapi_administrator_controller.yaml', - ], - [ - 'name' => 'Parent API', - 'url' => '/docs/openapi_parent_controller.yaml', - ], - ]; - - return view('swagger_ui', ['specs' => $specs]); - } -} diff --git a/app/old/Filters/.gitkeep b/app/old/Filters/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/old/Filters/ApiAuthFilter.php b/app/old/Filters/ApiAuthFilter.php deleted file mode 100644 index a91c3f9c..00000000 --- a/app/old/Filters/ApiAuthFilter.php +++ /dev/null @@ -1,59 +0,0 @@ -getHeaderLine('Authorization'); - if (!$authorization || stripos($authorization, 'Bearer ') !== 0) { - return $this->unauthorized('Missing or invalid Authorization header'); - } - - $token = trim(substr($authorization, 7)); - if ($token === '') { - return $this->unauthorized('Bearer token is required'); - } - - $secret = env('JWT_SECRET', 'change-me-in-env'); - $payload = jwt_decode($token, $secret); - - if (!$payload || empty($payload['sub'])) { - return $this->unauthorized('Invalid or malformed token'); - } - - if (isset($payload['exp']) && (int) $payload['exp'] < time()) { - return $this->unauthorized('Token has expired'); - } - - $session = Services::session(); - $session->start(); - - $session->set([ - 'user_id' => (int) $payload['sub'], - 'roles' => (array) ($payload['roles'] ?? []), - ]); - - return null; - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - return $response; - } - - private function unauthorized(string $message) - { - $response = Services::response(); - return $response->setStatusCode(401)->setJSON([ - 'status' => false, - 'message' => $message, - ]); - } -} diff --git a/app/old/Filters/ApiDocsAuthFilter.php b/app/old/Filters/ApiDocsAuthFilter.php deleted file mode 100644 index 4938b9ed..00000000 --- a/app/old/Filters/ApiDocsAuthFilter.php +++ /dev/null @@ -1,55 +0,0 @@ -getHeaderLine('Authorization'); - - if ($authHeader && str_starts_with($authHeader, 'Bearer ')) { - $token = trim(substr($authHeader, 7)); - - try { - $key = getenv('JWT_SECRET') ?: 'your_default_secret'; - $decoded = JWT::decode($token, new Key($key, 'HS256')); - - if (!isset($decoded->roles) || empty($decoded->roles->admin)) { - return $this->denyAccess('Admin privileges required.', ResponseInterface::HTTP_FORBIDDEN); - } - } catch (\Throwable $e) { - return $this->denyAccess('Invalid or expired token.', ResponseInterface::HTTP_UNAUTHORIZED); - } - } else { - $session = session(); - if (!$session->get('isLoggedIn') || $session->get('role') !== 'admin') { - if (strpos($request->getHeaderLine('Accept'), 'text/html') !== false) { - return redirect()->to(base_url('login'))->with('error', 'Please log in to access API documentation.'); - } - return $this->denyAccess('Unauthorized access to documentation.', ResponseInterface::HTTP_UNAUTHORIZED); - } - } - } - - private function denyAccess(string $message, int $status) - { - return Services::response() - ->setJSON([ - 'status' => false, - 'message' => $message, - ]) - ->setStatusCode($status); - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - } -} diff --git a/app/old/Filters/AuthFilter.php b/app/old/Filters/AuthFilter.php deleted file mode 100644 index f3fcae66..00000000 --- a/app/old/Filters/AuthFilter.php +++ /dev/null @@ -1,200 +0,0 @@ - ['permission' => 'nav_builder_access', 'crud' => 'read'], - // 'admin/enrollment/new-students' => ['permission' => 'view_new_students', 'crud' => 'read'], - // 'administrator/contact_information' => ['permission' => 'view_contact_info', 'crud' => 'read'], - // You can also allow specific role IDs directly: - // 'some/admin/page' => ['roles' => [1, 2]], // role_id 1 or 2 - ]; - - public function __construct() - { - $this->db = \Config\Database::connect(); - } - - public function before(RequestInterface $request, $arguments = null) - { - $session = session(); - $userRoles = $session->get('roles'); // not used here, but keep if other code expects it - $userId = $session->get('user_id'); - $loginTime = (int) $session->get('login_time'); - - // Must be logged in - if (empty($userRoles) || empty($userId)) { - return redirect()->to('/login'); - } - - // Enforce 12-hour session lifetime - if ($loginTime <= 0 || (time() - $loginTime) >= self::LOGIN_TTL) { - return $this->handleExpiredSession($request); - } - - // Load role IDs for this user - $roleIdRows = $this->db->table('user_roles') - ->select('role_id') - ->where('user_id', $userId) - ->get()->getResultArray(); - - $roleIds = array_map('intval', array_column($roleIdRows, 'role_id')); - if (empty($roleIds)) { - return $this->deny($request, "You don't have permission to use this feature."); - } - - // Route arguments: ['filter' => 'auth:permission_name|alt_permission,update'] - // Route arguments patterns supported: - // auth -> default 'read' - // auth:read -> CRUD only - // auth:create -> CRUD only - // auth:update -> CRUD only - // auth:delete -> CRUD only - // auth:permA|permB,read -> permission + CRUD - $requiredPermission = null; - $crudAction = 'read'; - - if (!empty($arguments)) { - $arg0 = strtolower((string) $arguments[0]); - - // If only one argument and it's a CRUD keyword, treat as CRUD-only - if (count($arguments) === 1 && in_array($arg0, ['create', 'read', 'update', 'delete'], true)) { - $crudAction = $arg0; - } else { - // Otherwise, first arg is permission(s); optional second is CRUD - $requiredPermission = $arguments[0]; // e.g., 'edit_student|manage_students' - $crudAction = isset($arguments[1]) ? strtolower((string) $arguments[1]) : 'read'; - if (!in_array($crudAction, ['create', 'read', 'update', 'delete'], true)) { - $crudAction = 'read'; - } - } - } - - - // No explicit route permission: fall back to menu rules - if ($this->isAllowedByMenu($request, $roleIds)) { - return; // ✅ allowed - } - - return $this->deny($request, "You don't have permission to use this feature."); - } - - private function handleExpiredSession(RequestInterface $request) - { - session()->destroy(); - - if ($request->isAJAX() || $request->getHeaderLine('Accept') === 'application/json') { - return service('response') - ->setStatusCode(401) - ->setJSON([ - 'status' => 'expired', - 'message' => 'Your session has expired. Please log in again.', - 'redirect' => '/login', - ]); - } - - return redirect()->to('/login')->with('error', 'Your session has expired. Please log in again.'); - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - // No-op - } - - /** - * JSON for AJAX, redirect for normal requests. - * Use RequestInterface here (your version used IncomingRequest which was too specific). - */ - public function deny(RequestInterface $request, string $message) - { - if ($request->isAJAX() || $request->getHeaderLine('Accept') === 'application/json') { - return service('response') - ->setStatusCode(403) - ->setJSON(['status' => 'error', 'message' => $message]); - } - - session()->setFlashdata('error', $message); - return redirect()->to('/access_denied'); - } - - /** - * Checks roles against CRUD flags on a role_permissions row. - */ - private function hasPermission(array $rolePermission, string $crudAction): bool - { - switch ($crudAction) { - case 'create': - return !empty($rolePermission['can_create']); - case 'read': - return !empty($rolePermission['can_read']); - case 'update': - return !empty($rolePermission['can_update']); - case 'delete': - return !empty($rolePermission['can_delete']); - default: - return false; - } - } - - /** - * ✅ Missing method added: - * Allow/deny based on $menuRules and the user's role IDs / permissions. - * Defaults to ALLOW if the path has no rule. - */ - private function isAllowedByMenu(RequestInterface $request, array $roleIds): bool - { - // ❌ was: $request->uri->getPath() - $path = ltrim(preg_replace('#/+#', '/', $request->getUri()->getPath()), '/'); - - $rule = $this->menuRules[$path] ?? null; - - if ($rule === null && str_starts_with($path, 'admin/')) { - $rule = $this->menuRules[substr($path, 6)] ?? null; - } - - if ($rule === null) { - return true; // or false if you prefer default-deny - } - - if (!empty($rule['roles']) && is_array($rule['roles'])) { - foreach ($roleIds as $rid) { - if (in_array((int)$rid, $rule['roles'], true)) return true; - } - // fall through to permission check (if present) - } - - if (!empty($rule['permission'])) { - $crud = $rule['crud'] ?? 'read'; - $rpRows = $this->db->table('role_permissions rp') - ->join('permissions p', 'p.id = rp.permission_id') - ->select('rp.*') - ->whereIn('rp.role_id', $roleIds) - ->where('p.name', $rule['permission']) - ->get()->getResultArray(); - - foreach ($rpRows as $rp) { - if ($this->hasPermission($rp, $crud)) return true; - } - return false; - } - - return true; - } -} diff --git a/app/old/Filters/CleanupScheduler.php b/app/old/Filters/CleanupScheduler.php deleted file mode 100644 index 555a8d71..00000000 --- a/app/old/Filters/CleanupScheduler.php +++ /dev/null @@ -1,36 +0,0 @@ -difference($lastRun)->getMinutes() >= 2) { - // Trigger the cleanup - $this->runCleanup(); - - // Update the last run time - cache()->save('last_cleanup_run', Time::now()); - } - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - // Do nothing after the request - } - - private function runCleanup() - { - // Call the cleanup controller method - \CodeIgniter\CLI\CLI::init(); - command('cleanup:unverified_users'); - } -} diff --git a/app/old/Filters/PermissionFilter.php b/app/old/Filters/PermissionFilter.php deleted file mode 100644 index 69b77165..00000000 --- a/app/old/Filters/PermissionFilter.php +++ /dev/null @@ -1,31 +0,0 @@ -get('is_logged_in')) { - return redirect()->to('/user/login'); - } - - $required = $arguments[0] ?? null; - if ($required) { - $perms = session('permissions') ?? []; - if (! in_array($required, $perms, true)) { - return redirect()->to('/')->with('error', 'Access denied.'); - } - } - - return null; - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - } -} diff --git a/app/old/Filters/TimezoneFilter.php b/app/old/Filters/TimezoneFilter.php deleted file mode 100644 index f11b8cc7..00000000 --- a/app/old/Filters/TimezoneFilter.php +++ /dev/null @@ -1,26 +0,0 @@ -primeFromRequest($request); - } catch (\Throwable $e) { - // ignore; fallback detection will still work lazily - } - } - - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - // No-op - } -} - diff --git a/app/old/FrontendController.php b/app/old/FrontendController.php deleted file mode 100644 index 9a70350c..00000000 --- a/app/old/FrontendController.php +++ /dev/null @@ -1,84 +0,0 @@ - 'User not logged in']); - exit; - } - - $userId = $_SESSION['user_id']; - - // Fetch user data from the database - $sql = "SELECT firstname, lastname FROM users WHERE id = ?"; - $stmt = $conn->prepare($sql); - $stmt->bind_param("i", $userId); - $stmt->execute(); - $result = $stmt->get_result(); - $user = $result->fetch_assoc(); - - if ($user) { - echo json_encode($user); - } else { - echo json_encode(['error' => 'User not found']); - } - - $stmt->close(); - $conn->close(); - } -} \ No newline at end of file diff --git a/app/old/HealthController.php b/app/old/HealthController.php deleted file mode 100644 index 675a6968..00000000 --- a/app/old/HealthController.php +++ /dev/null @@ -1,75 +0,0 @@ - $label, - 'path' => $path, - 'exists' => is_dir($path), - 'writable' => is_writable($path), - ]; - } - - public function index() - { - $uploadsBase = rtrim(WRITEPATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'uploads'; - $paths = [ - 'uploads' => $uploadsBase, - 'uploads/reimbursements' => $uploadsBase . DIRECTORY_SEPARATOR . 'reimbursements', - 'uploads/receipts' => $uploadsBase . DIRECTORY_SEPARATOR . 'receipts', - 'uploads/checks' => $uploadsBase . DIRECTORY_SEPARATOR . 'checks', - 'uploads/early_dismissal_signatures' => $uploadsBase . DIRECTORY_SEPARATOR . 'early_dismissal_signatures', - ]; - - $pathsStatus = []; - foreach ($paths as $label => $p) { - $pathsStatus[] = $this->checkPath($label, $p); - } - - $db = \Config\Database::connect(); - $dbChecks = [ - 'user_preferences_exists' => $db->tableExists('user_preferences'), - 'settings_exists' => $db->tableExists('settings'), - 'migrations_exists' => $db->tableExists('migrations'), - ]; - - $dbChecks['user_preferences_has_timezone'] = $dbChecks['user_preferences_exists'] - ? $db->fieldExists('timezone', 'user_preferences') - : false; - - $dbChecks['settings_has_timezone'] = $dbChecks['settings_exists'] - ? $db->fieldExists('timezone', 'settings') - : false; - - $okPaths = array_reduce($pathsStatus, function ($carry, $row) { - return $carry && $row['exists'] && $row['writable']; - }, true); - - $okDb = true; - if ($dbChecks['user_preferences_exists'] && !$dbChecks['user_preferences_has_timezone']) { - $okDb = false; - } - if ($dbChecks['settings_exists'] && !$dbChecks['settings_has_timezone']) { - $okDb = false; - } - - $ok = $okPaths && $okDb; - - $payload = [ - 'ok' => $ok, - 'paths' => $pathsStatus, - 'database' => $dbChecks, - 'write_path' => WRITEPATH, - 'timestamp' => date('c'), - ]; - - $status = $ok ? 200 : 503; - return $this->response->setStatusCode($status)->setJSON($payload); - } -} diff --git a/app/old/InfoIconController.php b/app/old/InfoIconController.php deleted file mode 100644 index 82e78d2a..00000000 --- a/app/old/InfoIconController.php +++ /dev/null @@ -1,39 +0,0 @@ -get('user_id'); - - if (!$userId) { - return view('errors/html/error_401', ['message' => 'User not logged in']); - } - - $userModel = new UserModel(); - $user = $userModel->getUserInfoById($userId); - - if ($user) { - $data = [ - 'userName' => $user['firstname'] . ' ' . $user['lastname'], - 'userInitials' => strtoupper($user['firstname'][0] . $user['lastname'][0]) - ]; - } else { - $data = [ - 'userName' => 'User not found', - 'userInitials' => '??' - ]; - } - - return view('partials/header', $data); -// return view('dashboard', ['content' => view('partials/user_icon', $data)]); - - } -} \ No newline at end of file diff --git a/app/old/IpBanController.php b/app/old/IpBanController.php deleted file mode 100644 index ee963155..00000000 --- a/app/old/IpBanController.php +++ /dev/null @@ -1,148 +0,0 @@ -ipAttemptModel = new IpAttemptModel(); - helper(['url', 'form']); - } - - public function index() - { - $now = utc_now(); - $status = strtolower(trim((string)($this->request->getGet('status') ?? 'all'))); - - $builder = $this->ipAttemptModel->orderBy('updated_at', 'DESC'); - if ($status === 'active') { - $builder = $builder->where('blocked_until >', $now); - } - - $banned = $builder->findAll(); - - return view('administrator/ip_bans', [ - 'banned' => $banned, - 'status' => $status, - 'now' => $now, - ]); - } - - public function unban() - { - $isAjax = $this->request->isAJAX(); - $json = function(array $p, int $code=200){ - $p['csrfTokenName'] = csrf_token(); - $p['csrfHash'] = csrf_hash(); - $p[csrf_token()] = csrf_hash(); - return $this->response->setStatusCode($code)->setJSON($p); - }; - - $id = (int) $this->request->getPost('id'); - $ip = trim((string) $this->request->getPost('ip')); - $all = (int) $this->request->getPost('all') === 1; - - try { - if ($all) { - // Clear all active bans - $now = utc_now(); - $builder = $this->ipAttemptModel->builder(); - $builder->where('blocked_until >', $now) - ->set(['blocked_until' => null, 'attempts' => 0]) - ->update(); - - $cnt = $this->ipAttemptModel->db->affectedRows(); - $msg = $cnt . ' IP(s) unbanned.'; - return $isAjax ? $json(['ok'=>true,'message'=>$msg,'count'=>$cnt]) - : redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg); - } - - $row = null; - if ($id > 0) { - $row = $this->ipAttemptModel->find($id); - } elseif ($ip !== '') { - $row = $this->ipAttemptModel->where('ip_address', $ip)->first(); - } - - if (!$row) { - $msg = 'IP record not found.'; - return $isAjax ? $json(['ok'=>false,'message'=>$msg],404) - : redirect()->back()->with('error', $msg); - } - - $ok = $this->ipAttemptModel->update($row['id'], [ - 'blocked_until' => null, - 'attempts' => 0, - ]); - - if (!$ok) { - throw new \RuntimeException('Failed to update record.'); - } - - $msg = 'IP ' . $row['ip_address'] . ' unbanned.'; - return $isAjax ? $json(['ok'=>true,'message'=>$msg]) - : redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg); - } catch (\Throwable $e) { - $msg = 'Unable to unban: ' . $e->getMessage(); - return $isAjax ? $json(['ok'=>false,'message'=>$msg],500) - : redirect()->back()->with('error', $msg); - } - } - - public function banNow() - { - $isAjax = $this->request->isAJAX(); - $json = function(array $p, int $code=200){ - $p['csrfTokenName'] = csrf_token(); - $p['csrfHash'] = csrf_hash(); - $p[csrf_token()] = csrf_hash(); - return $this->response->setStatusCode($code)->setJSON($p); - }; - - // Accept both POST and GET params - $id = (int) ($this->request->getPost('id') ?? $this->request->getGet('id') ?? 0); - $ip = trim((string) ($this->request->getPost('ip') ?? $this->request->getGet('ip') ?? '')); - $hours = (int) ($this->request->getPost('hours') ?? $this->request->getGet('hours') ?? 24); - if ($hours <= 0) $hours = 24; - - try { - $row = null; - if ($id > 0) { - $row = $this->ipAttemptModel->find($id); - } elseif ($ip !== '') { - $row = $this->ipAttemptModel->where('ip_address', $ip)->first(); - } - - if (!$row) { - $msg = 'IP record not found.'; - return $isAjax ? $json(['ok'=>false,'message'=>$msg],404) - : redirect()->to(site_url('administrator/ip_bans'))->with('error', $msg); - } - - $blockedUntil = date('Y-m-d H:i:s', strtotime("+{$hours} hours")); - $ok = $this->ipAttemptModel->update($row['id'], [ - 'blocked_until' => $blockedUntil, - // Optionally set attempts to threshold for clarity - 'attempts' => max((int)$row['attempts'], 10), - ]); - - if (!$ok) { - throw new \RuntimeException('Failed to update record.'); - } - - $msg = 'IP ' . $row['ip_address'] . ' banned for ' . $hours . 'h.'; - return $isAjax ? $json(['ok'=>true,'message'=>$msg]) - : redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg); - } catch (\Throwable $e) { - $msg = 'Unable to ban: ' . $e->getMessage(); - return $isAjax ? $json(['ok'=>false,'message'=>$msg],500) - : redirect()->back()->with('error', $msg); - } - } -} diff --git a/app/old/LandingPageController.php b/app/old/LandingPageController.php deleted file mode 100644 index f7a707cb..00000000 --- a/app/old/LandingPageController.php +++ /dev/null @@ -1,882 +0,0 @@ -db = \Config\Database::connect(); - $this->configModel = new ConfigurationModel(); - $this->studentClassModel = new StudentClassModel(); - $this->invoiceModel = new InvoiceModel(); - $this->userRoleModel = new UserRoleModel(); - $this->teacherClassModel = new TeacherClassModel(); - $this->semesterScoreModel = new SemesterScoreModel(); - $this->scoreCommentModel = new ScoreCommentModel(); - $this->attendanceRecordModel = new AttendanceRecordModel(); - $this->attendanceDayModel = new AttendanceDayModel(); - $this->classSectionModel = new ClassSectionModel(); - $this->calendarModel = new CalendarModel(); - - // Fetch Enrollment and Refund Deadlines from Configuration - $this->schoolYear = $this->configModel->getConfig('school_year'); - $this->semester = $this->configModel->getConfig('semester'); - $this->lastDayOfRegistration = $this->configModel->getConfig('enrollment_deadline') ?? 'Not set'; - $this->refundDeadline = $this->configModel->getConfig('refund_deadline') ?? 'Not set'; - - // Get class_id and store it in session - $this->classSectionId = $this->classSectionId(); - session()->set('class_section_id', $this->classSectionId); - } - - public function index() - { - $userRole = $this->getUserRole(); // Assume this function gets the user role - - switch (strtolower($userRole)) { - case 'administrator': - return $this->administrator(); - case 'admin': - return $this->admin(); - case 'teacher': - return $this->teacher(); - case 'student': - return $this->student(); - case 'parent': - case 'authorized_user': - return $this->parentDashboard(); - default: - return $this->guest(); - } - } - - public function classSectionId() - { - // Get user_id from the session - $user_id = session()->get('user_id'); - - if (!$user_id) { - return null; - } - - // Get all class assignments for this teacher in the current term - $assignments = $this->teacherClassModel->getClassAssignmentsByUserId( - (int)$user_id, - (string)$this->schoolYear, - (string)$this->semester - ); - - $ids = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments); - $ids = array_values(array_filter(array_unique($ids))); - - if (empty($ids)) { - log_message('error', "No class section found for user ID: $user_id"); - return null; - } - - $current = (int)(session()->get('class_section_id') ?? 0); - $chosen = in_array($current, $ids, true) ? $current : $ids[0]; - - session()->set([ - 'class_section_id' => $chosen, - 'class_section_ids' => $ids, - ]); - - return $chosen; - } - - - public function administrator() - { - return view('administrator/administratordashboard'); - } - - public function admin() - { - return view('/landing_page/admin_dashboard'); - } - - public function teacher() - { - $userId = (int)(session()->get('user_id') ?? 0); - $assignments = $this->teacherClassModel->getClassAssignmentsByUserId( - $userId, - (string)$this->schoolYear, - (string)$this->semester - ); - - $availableIds = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments); - $availableIds = array_values(array_filter(array_unique($availableIds))); - - $requestedId = (int)($this->request->getGet('class_section_id') ?? 0); - $activeId = null; - - if ($requestedId && in_array($requestedId, $availableIds, true)) { - $activeId = $requestedId; - } elseif (!empty($availableIds)) { - $activeId = in_array((int)$this->classSectionId, $availableIds, true) - ? (int)$this->classSectionId - : $availableIds[0]; - } - - if ($activeId) { - session()->set('class_section_id', $activeId); - } - - $activeName = null; - foreach ($assignments as $a) { - if ((int)$a['class_section_id'] === (int)$activeId) { - $activeName = $a['class_section_name']; - break; - } - } - - $normalizedSemester = ucfirst(strtolower(trim((string)($this->semester ?? '')))); - $classSectionIds = array_values(array_filter(array_unique(array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments)), static fn($id) => $id > 0)); - $dashboardSummary = $this->buildTeacherDashboardSummary($classSectionIds, $normalizedSemester); - - return view('/landing_page/teacher_dashboard', [ - 'class_section_id' => $activeId, - 'active_class_name' => $activeName, - 'classes' => $assignments, - 'jobs' => $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' => (string)$this->schoolYear, - 'semester' => (string)$this->semester, - ]); - } - - private function buildTeacherDashboardSummary(array $classSectionIds, string $semester): array - { - $normalizedSemester = ucfirst(strtolower(trim((string)$semester))); - if ($normalizedSemester === '' || !in_array($normalizedSemester, ['Fall', 'Spring'], true)) { - $normalizedSemester = 'Fall'; - } - - $scoreField = ($normalizedSemester === 'Spring') ? 'final_exam_score' : 'midterm_exam_score'; - $scoreLabel = ($normalizedSemester === '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' => [], - ]; - - if (!empty($classSectionIds)) { - $studentRows = $this->db->table('student_class sc') - ->select('sc.student_id, sc.class_section_id') - ->join('students s', 's.id = sc.student_id', 'inner') - ->whereIn('sc.class_section_id', $classSectionIds) - ->where('s.is_active', 1) - ->where('sc.school_year', (string)$this->schoolYear) - ->get() - ->getResultArray(); - - $studentIds = []; - foreach ($studentRows as $row) { - $studentId = (int)($row['student_id'] ?? 0); - $sectionId = (int)($row['class_section_id'] ?? 0); - if ($studentId <= 0 || $sectionId <= 0) { - continue; - } - $studentIds[] = $studentId; - } - $uniqueStudentIds = array_values(array_unique($studentIds)); - } else { - $uniqueStudentIds = []; - } - - $totalStudents = count($uniqueStudentIds); - $summary['attendanceSummary']['students'] = $totalStudents; - - $scoreRows = []; - if (!empty($classSectionIds)) { - $scoreRows = $this->semesterScoreModel - ->select('student_id, class_section_id, midterm_exam_score, final_exam_score, participation_score') - ->whereIn('class_section_id', $classSectionIds) - ->where('semester', $normalizedSemester) - ->where('school_year', (string)$this->schoolYear) - ->findAll(); - } - - $scoreTypesConfig = [ - 'participation' => ['label' => 'Participation', 'field' => 'participation_score'], - ]; - if ($normalizedSemester === 'Fall') { - $scoreTypesConfig = array_merge([ - 'midterm' => ['label' => 'Midterm', 'field' => 'midterm_exam_score'], - ], $scoreTypesConfig); - } - if ($normalizedSemester === '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 ($normalizedSemester === 'Fall') { - $scoreTypeByField['midterm_exam_score'] = 'midterm'; - } - if ($normalizedSemester === 'Spring') { - $scoreTypeByField['final_exam_score'] = 'final'; - } - $activeScoreType = $scoreTypeByField[$scoreField] ?? 'midterm'; - $activeScoreDetails = $scoreDetails[$activeScoreType] ?? $scoreDetails['midterm']; - - $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'], - ]; - - if (!empty($uniqueStudentIds)) { - $commentRows = $this->scoreCommentModel - ->select('student_id, score_type, comment, comment_review') - ->whereIn('student_id', $uniqueStudentIds) - ->where('semester', $normalizedSemester) - ->where('school_year', (string)$this->schoolYear) - ->findAll(); - } else { - $commentRows = []; - } - - $expectedCommentTypes = array_values(array_filter([ - 'ptap', - $normalizedSemester === 'Fall' ? 'midterm' : null, - $normalizedSemester === '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 = $this->attendanceRecordModel - ->select('student_id, class_section_id, total_absence') - ->whereIn('class_section_id', $classSectionIds) - ->where('semester', $normalizedSemester) - ->where('school_year', (string)$this->schoolYear) - ->findAll(); - } - - $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, $normalizedSemester, $today); - - $summary['deadlineEvents'] = $this->calendarModel - ->where('notify_teacher', 1) - ->where('school_year', (string)$this->schoolYear) - ->where('semester', $normalizedSemester) - ->where('date >=', $today) - ->orderBy('date', 'ASC') - ->limit(5) - ->findAll(); - - $summary['attendanceStatus'] = $attendanceStatus; - 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 = []; - $baseScoreLink = base_url('/teacher/scores'); - $buildScoreLink = static function (?string $focus) use ($baseScoreLink) { - if (empty($focus)) { - return $baseScoreLink; - } - return $baseScoreLink . '?focus=' . urlencode($focus); - }; - $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 = base_url('/teacher/calendar'); - $attendanceLink = base_url('/teacher/showupdate_attendance'); - $timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC'); - $todayDate = (new DateTimeImmutable('now', $timezone))->setTime(0, 0); - $today = $todayDate->format('Y-m-d'); - - $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 $date): array - { - if (empty($classSectionIds)) { - return [ - 'date' => $date, - 'missingSections' => [], - 'missingNames' => [], - 'submitted' => true, - ]; - } - - $rows = $this->attendanceDayModel - ->select('class_section_id, status') - ->whereIn('class_section_id', $classSectionIds) - ->where('date', $date) - ->where('semester', $semester) - ->where('school_year', (string)$this->schoolYear) - ->findAll(); - - $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 = $this->classSectionModel - ->select('class_section_id, class_section_name') - ->whereIn('class_section_id', $missingSections) - ->findAll(); - 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), - ]; - } - - - public function student() - { - return view('/landing_page/student_dashboard'); - } - - public function parentDashboard() - { - $parentId = session()->get('user_id'); - $userType = $_SESSION['user_type']; - - // Map user type to roles - if ($userType === 'primary') { - // If user type is 'Primary', they are the firstparent - $parentId = $parentId; - } elseif ($userType === 'secondary') { - // If user type is 'Secondary', find the parent_id from the parents table - $parentData = $this->db->table('parents') - ->select('parent_id') - ->where('secondparent_user_id', $parentId) - ->get() - ->getRowArray(); - - if ($parentData) { - $parentId = $parentData['parent_id']; - } - } elseif ($userType === 'tertiary') { - // If user type is 'Tertiary', find the parent_id from the authorized_users table - $authUserData = $this->db->table('authorized_users') - ->select('user_id as parent_id') - ->where('authorized_user_id', $parentId) - ->get() - ->getRowArray(); - - if ($authUserData) { - $parentId = $authUserData['parent_id']; - } - } - - // If no firstparent ID is found, show an error or redirect - if (!$parentId) { - return redirect()->back()->with( - 'error', - 'Unable to retrieve student data. Please contact support.' - ); - } - - - // Fetch Notifications (only active, non-expired, non-deleted) - $notifications = $this->db->table('notifications') - ->select([ - 'notifications.id', - 'notifications.title', - 'notifications.message', - 'notifications.target_group', - 'notifications.created_at', - 'notifications.expires_at', - 'user_notifications.user_id', - "CASE - WHEN user_notifications.user_id IS NOT NULL THEN 'personal' - ELSE 'broadcast' - END as notification_type" - ]) - ->join( - 'user_notifications', - 'user_notifications.notification_id = notifications.id AND user_notifications.user_id = ' . (int) $parentId, - 'left' - ) - ->groupStart() - ->where('notifications.target_group', 'parent') - ->orWhere('user_notifications.user_id', $parentId) - ->groupEnd() - ->where('notifications.deleted_at IS NULL') // Exclude soft-deleted notifications - ->groupStart() - ->where('notifications.expires_at IS NULL') - ->orWhere('notifications.expires_at > NOW()') // Exclude expired - ->groupEnd() - ->orderBy('notifications.created_at', 'DESC') - ->get() - ->getResultArray(); - - // Fetch Student Information (no filtering needed by school year or semester) - - $students = $this->db->table('students') - ->where('parent_id', $parentId) - ->get() - ->getResultArray(); - - foreach ($students as &$student) { - // Get class_section_name and treat it as grade - $classSection = $this->studentClassModel->getClassSectionsByStudentId($student['id'], $this->schoolYear); - $student['class_section'] = $classSection; - $student['grade'] = $classSection; // grade same as section - } - unset($student); - - - // Fetch Attendance Records (filtered by most recent school year and semester) - $attendanceData = $this->db->table('attendance_data') - ->select('attendance_data.*, students.firstname, students.lastname') - ->join('students', 'students.id = attendance_data.student_id') - ->where('students.parent_id', $parentId) - // ->orWhere('students.secondparent_user_id', $parentId) - ->where('attendance_data.school_year', $this->schoolYear) - ->where('attendance_data.semester', $this->semester) - ->orderBy('attendance_data.date', 'DESC') - ->get() - ->getResultArray(); - - // Fetch Grades (filtered by most recent school year and semester) - $grades = $this->db->table('final_score') // Correct table name - ->select('final_score.*, students.firstname, students.lastname') // Ensure table name is correct - ->join('students', 'students.id = final_score.student_id') - ->where('students.parent_id', $parentId) - // ->orWhere('students.secondparent_user_id', $parentId) - ->where('final_score.school_year', $this->schoolYear) // Ensure correct table column names - ->where('final_score.semester', $this->semester) - ->orderBy('final_score.created_at', 'DESC') - ->get() - ->getResultArray(); - - // Fetch Enrollments (filtered by most recent school year and semester) - $enrollments = $this->db->table('enrollments') - ->select('enrollments.*, classes.class_name') - ->join('students', 'students.id = enrollments.student_id') - ->join('classes', 'classes.id = enrollments.class_section_id') // Ensure this join is correct - ->where('students.parent_id', $parentId) - // ->orWhere('students.secondparent_user_id', $parentId) - ->where('enrollments.school_year', $this->schoolYear) - ->where('enrollments.semester', $this->semester) - ->orderBy('enrollments.enrollment_date', 'DESC') - ->get() - ->getResultArray(); - - // Fetch latest invoice balance - $paymentBalance = $this->invoiceModel->getLatestInvoiceTotalAmount($parentId); - - // Pass data to the view, including the deadlines - return view('/landing_page/parent_dashboard', [ - 'notifications' => $notifications, - 'students' => $students, - 'attendance' => $attendanceData, - 'grades' => $grades, - 'enrollments' => $enrollments, - 'lastDayOfRegistration' => $this->lastDayOfRegistration, // Add the enrollment deadline to the view - 'withdrawalDeadline' => $this->refundDeadline, // Add the refund deadline to the view - 'paymentBalance' => $paymentBalance, // 🔹 New - ]); - } - - public function guest() - { - return view('/landing_page/guest_dashboard'); - } - - protected function getUserRole() - { - // Assuming you have a session variable storing the user role - return session()->get('user_role', 'guest'); // Default to guest if not set - } - - protected function getUserRoleFromDatabase($user_id) - { - // Fetching user role from the database - $role = $this->userRoleModel->getRoleByUserId($user_id); - - return $role ?? 'guest'; - } -} diff --git a/app/old/LateSlipLogsController.php b/app/old/LateSlipLogsController.php deleted file mode 100644 index 443442d9..00000000 --- a/app/old/LateSlipLogsController.php +++ /dev/null @@ -1,63 +0,0 @@ -logModel = new LateSlipLogModel(); - $this->configModel = new ConfigurationModel(); - } - - public function index() - { - $req = $this->request; - - $defaultYear = (string) ($this->configModel->getConfig('school_year') ?? ''); - $defaultSem = (string) ($this->configModel->getConfig('semester') ?? ''); - $schoolYear = trim((string) ($req->getGet('school_year') ?? $defaultYear)); - $semester = trim((string) ($req->getGet('semester') ?? $defaultSem)); - $q = trim((string) ($req->getGet('q') ?? '')); - $dateFrom = trim((string) ($req->getGet('date_from') ?? '')); - $dateTo = trim((string) ($req->getGet('date_to') ?? '')); - - // Normalize dates to Y-m-d for filtering - $toDbDate = static function (?string $s): ?string { - $s = trim((string) $s); - if ($s === '') return null; - $ts = strtotime($s); - return $ts ? date('Y-m-d', $ts) : null; - }; - $df = $toDbDate($dateFrom); - $dt = $toDbDate($dateTo); - - $model = $this->logModel; - if ($schoolYear !== '') $model = $model->where('school_year', $schoolYear); - if ($semester !== '') $model = $model->where('semester', $semester); - if ($q !== '') $model = $model->like('student_name', $q); - if ($df) $model = $model->where('slip_date >=', $df); - if ($dt) $model = $model->where('slip_date <=', $dt); - - // Limit to most recent 200 entries - $logs = $model->orderBy('id', 'DESC')->findAll(200); - - return view('administrator/late_slip_logs', [ - 'schoolYear' => $schoolYear, - 'semester' => $semester, - 'filters' => [ - 'q' => $q, - 'date_from' => $dateFrom, - 'date_to' => $dateTo, - ], - 'logs' => $logs, - ]); - } -} diff --git a/app/old/MessagesController.php b/app/old/MessagesController.php deleted file mode 100644 index 4849191d..00000000 --- a/app/old/MessagesController.php +++ /dev/null @@ -1,249 +0,0 @@ -messageModel = new MessageModel(); - } - - public function inbox() - { - $userId = session()->get('user_id'); - $receivedMessages = $this->messageModel->getInboxMessages($userId); - - return view('messages/inbox', [ - 'receivedMessages' => $receivedMessages - ]); - } - - public function sent() - { - $userId = session()->get('user_id'); - $sentMessages = $this->messageModel->getSentMessages($userId); - - return view('messages/sent', [ - 'sentMessages' => $sentMessages - ]); - } - - public function drafts() - { - $userId = session()->get('user_id'); - $draftMessages = $this->messageModel->getDraftMessages($userId); - - return view('messages/drafts', [ - 'draftMessages' => $draftMessages - ]); - } - - public function trash() - { - $userId = session()->get('user_id'); - $trashedMessages = $this->messageModel->getTrashedMessages($userId); - - return view('messages/trash', [ - 'trashedMessages' => $trashedMessages - ]); - } - - public function index() - { - $userRoleModel = new UserRoleModel(); - //$role = session()->get('role'); - $userId = session()->get('user_id'); - // Fetch the user role from the user_roles and roles tables - $role = $userRoleModel->select('roles.name') - ->join('roles', 'roles.id = user_roles.role_id') - ->where('user_roles.user_id', $userId) - ->get() - ->getRowArray(); - - // Fetch received messages based on role - $db = \Config\Database::connect(); - $builder = $db->table('messages'); - $builder->select('messages.*, users.firstname AS sender_name'); - $builder->join('users', 'messages.sender_id = users.id'); - $builder->where('messages.recipient_id', $userId); - $query = $builder->get(); - $receivedMessages = $query->getResultArray(); - - return view('/messages', [ - 'receivedMessages' => $receivedMessages, - 'role' => $role['name'] - ]); - } - - public function send() - { - $db = \Config\Database::connect(); - $userRoleModel = new UserRoleModel(); - //$role = session()->get('role'); - $userId = session()->get('user_id'); - // Fetch the user role from the user_roles and roles tables - $role = $userRoleModel->select('roles.name') - ->join('roles', 'roles.id = user_roles.role_id') - ->where('user_roles.user_id', $userId) - ->get() - ->getRowArray(); - - // Handle file upload (if any) - $attachmentPath = null; - $file = $this->request->getFile('attachment'); - if ($file && $file->isValid() && !$file->hasMoved()) { - $attachmentPath = $file->store(); - } - - // Determine recipient based on role or input - $recipientId = $this->getRecipientId($role); - - // Prepare data for insertion - $data = [ - 'sender_id' => $userId, - 'recipient_id' => $recipientId, - 'subject' => $this->request->getPost('subject'), - 'message' => $this->request->getPost('message'), - 'sent_datetime' => utc_now(), - 'message_number' => $this->generateMessageNumber(), - 'priority' => $this->request->getPost('priority') ?: 'normal', - 'attachment' => $attachmentPath, - 'status' => 'sent' - ]; - - $builder = $db->table('messages'); - $builder->insert($data); - - return redirect()->to(base_url('messages'))->with('status', 'Message sent successfully'); - } - - public function receive() - { - $userId = session()->get('user_id'); - - $db = \Config\Database::connect(); - $builder = $db->table('messages'); - $builder->select('messages.*, users.firstname AS sender_name'); - $builder->join('users', 'messages.sender_id = users.id'); - $builder->where('messages.recipient_id', $userId); - $builder->orderBy('sent_datetime', 'DESC'); - $query = $builder->get(); - $receivedMessages = $query->getResultArray(); - - // Mark messages as read - foreach ($receivedMessages as $message) { - if ($message['read_status'] == 0) { - $this->markAsRead($message['id']); - } - } - // Assuming $role['name'] contains the role name - //$roleName = strtolower($role['name']); // Convert role name to lowercase if needed - - // Construct the view path dynamically - // $viewPath = '/' . $roleName . '/' . $roleName . '_messages'; - - // Return the view - //return view($viewPath, ['receivedMessages' => $receivedMessages]); - } - - private function generateMessageNumber() - { - return 'MSG-' . date('YmdHis'); - } - - private function markAsRead($messageId) - { - $db = \Config\Database::connect(); - $builder = $db->table('messages'); - $builder->where('id', $messageId); - $builder->update([ - 'read_status' => 1, - 'read_datetime' => utc_now() - ]); - } - - private function getRecipientId($role) - { - // You can customize this logic to retrieve recipient_id based on the role or input - switch (strtolower($role['name'])) { - case 'teacher': - return 1; // Example: return teacher's ID - case 'parent': - return 2; // Example: return parent's ID - case 'admin': - return 3; // Example: return admin's ID - case 'student': - return 4; // Example: return student's ID - case 'guest': - return 5; // Example: return guest's ID - case 'administrator': - return 6; // Example: return administrator's ID - default: - throw new \Exception('Invalid role: ' . $role['name']); - } - } - - public function getRecipients($type) - { - $recipients = []; - $teacherClassModel = new TeacherClassModel(); - $studentClassModel = new StudentClassModel(); - $studentsModel = new StudentModel(); - $teacherClasses = $teacherClassModel->findAll(); - - if ($type === 'teacher') { - $userModel = new UserModel(); // Assuming the UserModel is in the App\Models namespace - - foreach ($teacherClasses as $teacher) { - $user = $userModel->find($teacher['teacher_id']); - if ($user) { - $recipients[] = [ - 'id' => $teacher['teacher_id'], - 'name' => $user['firstname'] . ' ' . $user['lastname'] - ]; - } - } - } elseif ($type === 'parent') { - foreach ($teacherClasses as $class) { - $students = $studentClassModel - ->active() - ->where('student_class.class_section_id', $class['class_section_id']) - ->findAll(); - - foreach ($students as $student) { - $studentData = $studentsModel - ->where('id', $student['student_id']) - ->where('is_active', 1) - ->first(); - if ($studentData) { - if ($studentData['firstparent']) { - $recipients[] = [ - 'id' => $studentData['parent_id'], - 'name' => $studentData['firstparent'] - ]; - } - if ($studentData['secondparent']) { - $recipients[] = [ - 'id' => $studentData['secondparent_user_id'], - 'name' => $studentData['secondparent'] - ]; - } - } - } - } - } - - return $this->response->setJSON($recipients); - } -} diff --git a/app/old/NavBuilderController.php b/app/old/NavBuilderController.php deleted file mode 100644 index 6493f216..00000000 --- a/app/old/NavBuilderController.php +++ /dev/null @@ -1,303 +0,0 @@ -items = new NavItemModel(); - $this->maps = new RoleNavItemModel(); - $this->service = new NavbarService(); - } - -protected function ensureAdmin(): void -{ - $sessionRole = session()->get('role'); // could be a string or array in your app - $roleNames = is_array($sessionRole) ? $sessionRole : [$sessionRole]; - $roleNames = array_values(array_filter(array_map('strval', $roleNames))); - - if (empty($roleNames)) { - throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); - } - - $db = \Config\Database::connect(); - - // Map role names -> ids - $roleIdRows = $db->table('roles')->select('id')->whereIn('name', $roleNames)->get()->getResultArray(); - $roleIds = array_map('intval', array_column($roleIdRows, 'id')); - if (empty($roleIds)) { - throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); - } - - // Is this route allowed for any of the user's roles? - $allowed = $db->table('role_nav_items AS rni') - ->select('1') - ->join('nav_items AS ni', 'ni.id = rni.nav_item_id') - ->where('ni.url', 'nav-builder') // IMPORTANT: your current route path - ->whereIn('rni.role_id', $roleIds) - ->get(1)->getFirstRow(); - - if (!$allowed) { - // You can show a nicer "Access Denied" view if you prefer - throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); - } -} - - public function index(){ - $this->ensureAdmin(); - - helper(['url', 'form']); - - return view('nav_builder/index'); -} - -/** Case-insensitive, natural sort; ignores leading punctuation & articles (“a/an/the”) */ -private function labelKey(string $s): string -{ - $s = trim(preg_replace('/\s+/', ' ', $s)); - $s = preg_replace('/^[^[:alnum:]]+/u', '', $s); // strip leading punctuation - $s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); // drop leading articles - return mb_strtolower($s, 'UTF-8'); -} - -private function sortTreeAlpha(array &$nodes): void -{ - usort($nodes, fn($a,$b) => strnatcasecmp( - $this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? '') - )); - foreach ($nodes as &$n) { - if (!empty($n['children'])) { - $this->sortTreeAlpha($n['children']); - } - } - unset($n); -} - - -public function save() -{ - $this->ensureAdmin(); - - $id = (int) $this->request->getPost('id'); - - // Accept either "menu_parent_id" or "parent_id" from the form - $menuParentRaw = $this->request->getPost('menu_parent_id'); - if ($menuParentRaw === null) { - $menuParentRaw = $this->request->getPost('parent_id'); - } - - $menuParentId = ($menuParentRaw === '' || $menuParentRaw === null) ? null : (int) $menuParentRaw; - if ($id && $menuParentId === $id) { - // item cannot be its own parent - $menuParentId = null; - } - - // Validate parent exists (optional but helpful) - if ($menuParentId !== null) { - $parent = $this->items->select('id')->where('id', $menuParentId)->first(); - if (!$parent) { - return redirect()->back()->with('error', 'Selected parent does not exist.')->withInput(); - } - } - - $data = [ - 'menu_parent_id' => $menuParentId, - 'label' => trim((string) $this->request->getPost('label')), - 'url' => trim((string) $this->request->getPost('url')) ?: null, - 'icon_class' => trim((string) $this->request->getPost('icon_class')) ?: null, - 'target' => trim((string) $this->request->getPost('target')) ?: null, - 'sort_order' => (int) $this->request->getPost('sort_order'), - 'is_enabled' => (int) ($this->request->getPost('is_enabled') ? 1 : 0), - ]; - - // Save (force insert to return ID) - if ($id) { - $this->items->update($id, $data); - } else { - $id = (int) $this->items->insert($data, true); // ensure insertID is returned - } - - // Roles - $roleIds = array_values(array_unique(array_filter( - array_map('intval', (array) ($this->request->getPost('roles') ?? [])), - fn ($v) => $v > 0 - ))); - - $this->maps->where('nav_item_id', $id)->delete(); - foreach ($roleIds as $rid) { - $this->maps->insert(['role_id' => $rid, 'nav_item_id' => $id]); - } - - $this->service->clearCache(); - return redirect()->back()->with('success', 'Menu saved.'); -} - - - public function delete($id) - { - $this->ensureAdmin(); - $id = (int) $id; - - $this->items->delete($id); // children handled by FK (SET NULL on parent) - $this->service->clearCache(); - - return redirect()->back()->with('success', 'Menu item deleted.'); - } - - public function reorder() - { - $this->ensureAdmin(); - - $orders = $this->request->getPost('orders') ?? []; - foreach ($orders as $id => $order) { - $this->items->update((int) $id, ['sort_order' => (int) $order]); - } - $this->service->clearCache(); - return $this->response->setJSON(['ok' => true]); - } - - protected function distinctRoles(): array - { - $db = \Config\Database::connect(); - return $db->table('roles') - ->select('id, name') - ->orderBy('name') - ->get() - ->getResultArray(); - } - - public function data() - { - $this->ensureAdmin(); - return $this->response->setJSON($this->buildNavPayload()); - } - - private function buildNavPayload(): array - { - $all = $this->items - ->orderBy('menu_parent_id', 'ASC') - ->orderBy('label', 'ASC') - ->orderBy('id', 'ASC') - ->findAll(); - - $byId = []; - foreach ($all as $a) { - $a['children'] = []; - $byId[$a['id']] = $a; - } - - $tree = []; - foreach ($byId as $id => &$node) { - $pid = $node['menu_parent_id'] ?: null; - if ($pid && isset($byId[$pid])) { - $byId[$pid]['children'][] = &$node; - } else { - $tree[] = &$node; - } - } - unset($node); - - $this->sortTreeAlpha($tree); - - $flatAlpha = $all; - usort($flatAlpha, fn($a, $b) => strnatcasecmp( - $this->labelKey($a['label'] ?? ''), - $this->labelKey($b['label'] ?? '') - )); - - $roleAssignments = []; - $roleRows = $this->maps - ->select('role_nav_items.nav_item_id, roles.id AS role_id, roles.name AS role_name') - ->join('roles', 'roles.id = role_nav_items.role_id', 'left') - ->findAll(); - - foreach ($roleRows as $row) { - $navId = (int) ($row['nav_item_id'] ?? 0); - if ($navId <= 0) continue; - - $roleId = (int) ($row['role_id'] ?? 0); - $roleName = $row['role_name'] ?? ($roleId ? ('#' . $roleId) : null); - - if ($roleId > 0) { - $roleAssignments[$navId]['ids'][] = $roleId; - } - if ($roleName !== null) { - $roleAssignments[$navId]['names'][] = $roleName; - } - } - - $flattened = $this->flattenTreeForResponse($tree, $roleAssignments); - - $parentOptions = array_map(static function ($row) { - return [ - 'id' => (int) ($row['id'] ?? 0), - 'label' => (string) ($row['label'] ?? ''), - ]; - }, $flatAlpha); - - $roles = array_map(static function ($role) { - return [ - 'id' => (int) ($role['id'] ?? 0), - 'name' => (string) ($role['name'] ?? ''), - ]; - }, $this->distinctRoles()); - - return [ - 'items' => $flattened, - 'roles' => $roles, - 'parentOptions' => $parentOptions, - ]; - } - - private function flattenTreeForResponse(array $nodes, array $roleAssignments, ?string $parentLabel = null, int $depth = 0, array &$rows = []): array - { - foreach ($nodes as $node) { - $raw = $node; - unset($raw['children']); - - $navId = (int) ($raw['id'] ?? 0); - $roles = $roleAssignments[$navId] ?? ['ids' => [], 'names' => []]; - - $rows[] = [ - 'id' => $navId, - 'label' => (string) ($raw['label'] ?? ''), - 'url' => $raw['url'] ?? null, - 'parent_label' => $parentLabel ?? '—', - 'parent_id' => isset($raw['menu_parent_id']) && (int) $raw['menu_parent_id'] !== 0 - ? (int) $raw['menu_parent_id'] - : null, - 'order' => (int) ($raw['sort_order'] ?? 0), - 'enabled' => (int) ($raw['is_enabled'] ?? 0) === 1, - 'target' => $raw['target'] ?? null, - 'depth' => $depth, - 'roles' => [ - 'ids' => array_values(array_unique($roles['ids'] ?? [])), - 'names' => array_values(array_unique($roles['names'] ?? [])), - ], - 'raw' => $raw, - ]; - - if (!empty($node['children'])) { - $this->flattenTreeForResponse( - $node['children'], - $roleAssignments, - (string) ($raw['label'] ?? ''), - $depth + 1, - $rows - ); - } - } - - return $rows; - } -} diff --git a/app/old/NavbarService.php b/app/old/NavbarService.php deleted file mode 100644 index 183184b1..00000000 --- a/app/old/NavbarService.php +++ /dev/null @@ -1,66 +0,0 @@ -cache ??= cache(); - } - - public function getMenuForRoles(array $roles): array - { - $roles = array_map(fn($r)=>strtolower(trim((string)$r)), $roles); - $cacheKey = 'navbar_' . md5(json_encode($roles)); - - if ($menu = $this->cache->get($cacheKey)) { - return $menu; - } - - // Which items this user can see - $allowedIds = $this->mapModel->getNavItemIdsForRoles($roles); - if (empty($allowedIds)) return []; - - // Load all enabled items, then filter - $rows = $this->navModel->where('is_enabled', 1) - ->orderBy('sort_order', 'ASC') - ->findAll(); - - // index by id - $byId = []; - foreach ($rows as $r) { - if (in_array($r['id'], $allowedIds, true)) { - $r['children'] = []; - $byId[$r['id']] = $r; - } - } - - // Build tree - $tree = []; - foreach ($byId as $id => &$node) { - $pid = $node['menu_parent_id']; - if ($pid && isset($byId[$pid])) { - $byId[$pid]['children'][] = &$node; - } else { - $tree[] = &$node; - } - } - - $this->cache->save($cacheKey, $tree, 300); // 5 minutes - return $tree; - } - - public function clearCache(): void - { - // simplest: flush; or if you have a tagged cache, clear only keys - cache()->clean(); - } -} diff --git a/app/old/PageController.php b/app/old/PageController.php deleted file mode 100644 index 4bc87db8..00000000 --- a/app/old/PageController.php +++ /dev/null @@ -1,64 +0,0 @@ -response->setBody(file_get_contents(ROOTPATH . 'public/html/privacy_policy.html')) - ->setContentType('text/html'); - } - - public function termsOfService() - { - return $this->response->setBody(file_get_contents(ROOTPATH . 'public/html/terms_of_service.html')) - ->setContentType('text/html'); - } - - public function helpCenter() - { - return $this->response->setBody(file_get_contents(ROOTPATH . 'public/html/help_center.html')) - ->setContentType('text/html'); - } - - public function submitContactForm() - { - try { - $email = strtolower($this->request->getPost('email')); - $message = $this->request->getPost('message'); - - // Save the message to the database - $contactModel = new ContactUsModel(); - $contactData = [ - 'email' => $email, - 'message' => $message, - ]; - - if (!$contactModel->save($contactData)) { - log_message('error', 'Failed to save contact data: ' . implode(', ', $contactModel->errors())); - return redirect()->back()->with('error', 'Failed to save message. Please try again.'); - } - - // Prepare the email content - $recipient = 'alrahma.isgl@gmail.com'; // Change to the communication head's email - $subject = 'Contact Us Form Submission'; - $emailMessage = "You have received a new message from $email:\n\n$message"; - - // Use EmailController to send the email - $emailController = new \App\Controllers\View\EmailController(); - if (!$emailController->sendEmail($recipient, $subject, $emailMessage)) { - log_message('error', 'Failed to send email.'); - return redirect()->back()->with('error', 'Failed to send message. Please try again.'); - } - - return redirect()->to('/thank_you')->with('success', 'Message sent successfully.'); - } catch (\Exception $e) { - log_message('error', 'Exception occurred: ' . $e->getMessage()); - return redirect()->back()->with('error', 'An unexpected error occurred. Please try again.'); - } - } -} \ No newline at end of file diff --git a/app/old/PhoneFormatterService.php b/app/old/PhoneFormatterService.php deleted file mode 100644 index 9cd47f76..00000000 --- a/app/old/PhoneFormatterService.php +++ /dev/null @@ -1,31 +0,0 @@ -preferencesModel = new PreferencesModel(); - } - - /** - * Display preferences page - * GET /preferences/{userId} - */ - public function index($userId = null) - { - // Get user ID from parameter or session - $userId = $userId ?? (int) session()->get('user_id'); - - if (!$userId) { - return redirect()->to('/login')->with('error', 'Please log in to view preferences'); - } - - // Fetch preferences for the current user - $preferences = $this->preferencesModel->where('user_id', $userId)->first(); - - // If preferences do not exist, set default preferences - if (!$preferences) { - $preferences = [ - 'receive_email_notifications' => 1, - 'receive_sms_notifications' => 1, - 'theme' => 'light', - 'language' => 'en', - ]; - } - - // Append style/menu selections from session or defaults - $styleConfig = config('Style'); - $preferences['style_color'] = $preferences['style_color'] ?? (session()->get('style_color') ?? ($styleConfig->defaultStyle ?? 'blue')); - $preferences['menu_color'] = $preferences['menu_color'] ?? (session()->get('menu_color') ?? ($styleConfig->defaultMenu ?? 'white')); - - // Options for selectors - $styleOptions = array_keys($styleConfig->stylePalettes ?? []); - $menuOptions = array_keys($styleConfig->menuPalettes ?? []); - - // Load the view with the user's preferences - return view('/preferences', [ - 'preferences' => $preferences, - 'styleOptions' => $styleOptions, - 'menuOptions' => $menuOptions, - 'userId' => $userId, - ]); - } - - /** - * Update preferences - * POST /preferences/update/{userId} - */ - public function updatePreferences($userId = null) - { - // Get user ID from parameter or session - $userId = $userId ?? (int) session()->get('user_id'); - - if (!$userId) { - return redirect()->to('/login')->with('error', 'Please log in to update preferences'); - } - - // Validation rules - $validation = \Config\Services::validation(); - - $styleConfig = config('Style'); - $styleOptions = implode(',', array_keys($styleConfig->stylePalettes ?? [])); - $menuOptions = implode(',', array_keys($styleConfig->menuPalettes ?? [])); - - $validation->setRules([ - 'receive_email_notifications' => 'permit_empty|in_list[0,1]', - 'receive_sms_notifications' => 'permit_empty|in_list[0,1]', - 'theme' => 'permit_empty|in_list[light,dark]', - 'language' => 'permit_empty|in_list[en,fr,es]', - 'style_color' => $styleOptions ? 'permit_empty|in_list[' . $styleOptions . ']' : 'permit_empty', - 'menu_color' => $menuOptions ? 'permit_empty|in_list[' . $menuOptions . ']' : 'permit_empty', - ]); - - if (!$validation->run($this->request->getPost())) { - $preferences = $this->preferencesModel->where('user_id', $userId)->first(); - if (!$preferences) { - $preferences = [ - 'receive_email_notifications' => 1, - 'receive_sms_notifications' => 1, - 'theme' => 'light', - 'language' => 'en', - ]; - } - $preferences['style_color'] = $this->request->getPost('style_color') ?? ($preferences['style_color'] ?? (session()->get('style_color') ?? ($styleConfig->defaultStyle ?? 'blue'))); - $preferences['menu_color'] = $this->request->getPost('menu_color') ?? ($preferences['menu_color'] ?? (session()->get('menu_color') ?? ($styleConfig->defaultMenu ?? 'white'))); - - return view('/preferences', [ - 'preferences' => $preferences, - 'styleOptions' => array_keys($styleConfig->stylePalettes ?? []), - 'menuOptions' => array_keys($styleConfig->menuPalettes ?? []), - 'validation' => $validation, - 'userId' => $userId, - ]); - } - - // Prepare updated data (map form fields to database fields) - $updateData = [ - 'receive_email_notifications' => $this->request->getPost('receive_email_notifications') ?? $this->request->getPost('notification_email') ?? 1, - 'receive_sms_notifications' => $this->request->getPost('receive_sms_notifications') ?? $this->request->getPost('notification_sms') ?? 1, - 'theme' => $this->request->getPost('theme') ?? 'light', - 'language' => $this->request->getPost('language') ?? 'en', - ]; - - // Store style/menu selections in session (no DB column dependency) - $styleColor = $this->request->getPost('style_color'); - $menuColor = $this->request->getPost('menu_color'); - if ($styleColor && isset(($styleConfig->stylePalettes ?? [])[$styleColor])) { - session()->set('style_color', $styleColor); - $updateData['style_color'] = $styleColor; - } - if ($menuColor && isset(($styleConfig->menuPalettes ?? [])[$menuColor])) { - session()->set('menu_color', $menuColor); - $updateData['menu_color'] = $menuColor; - } - - // Check if preferences already exist, update if they do, otherwise insert new preferences - $existing = $this->preferencesModel->where('user_id', $userId)->first(); - if ($existing) { - $this->preferencesModel->update($existing['id'], $updateData); - } else { - $updateData['user_id'] = $userId; - $this->preferencesModel->insert($updateData); - } - - // Redirect back to preferences page with success message - return redirect()->to('/preferences/' . $userId)->with('success', 'Preferences updated successfully'); - } -} diff --git a/app/old/RFIDController.php b/app/old/RFIDController.php deleted file mode 100644 index d4b50f0f..00000000 --- a/app/old/RFIDController.php +++ /dev/null @@ -1,71 +0,0 @@ -request->getPost('rfid'); - - // Load the user model - $userModel = new UserModel(); - - // Find the user by RFID tag - $user = $userModel->where('rfid_tag', $rfidTag)->first(); - - if ($user) { - // Log the scan (optional: store in a 'scan_log' table) - $db = \Config\Database::connect(); - $db->table('scan_log')->insert([ - 'user_id' => $user->id, - 'card_id' => $rfidTag, - 'scan_time' => Time::now(), - ]); - - // Redirect with success message - return redirect()->to('/rfid/log')->with('message', 'RFID recognized: ' . $user->name); - } else { - // Redirect with error message if RFID not found - return redirect()->to('/')->with('error', 'RFID tag not recognized.'); - } - } - - public function log() - { - $db = \Config\Database::connect(); - - // Retrieve scan logs and join with users table to display user names - $query = $db->table('scan_log') - ->select('users.firstname as user_firstname, users.lastname as user_lastname, students.firstname as student_firstname, students.lastname as student_lastname, scan_log.card_id, scan_log.scan_time') - ->join('users', 'users.id = scan_log.user_id', 'left') - ->join('students', 'students.rfid_tag = scan_log.card_id', 'left') // Join students table by RFID tag - ->orderBy('scan_time', 'DESC') - ->get(); - - // Fetch user and student data for display purposes - $userModel = new UserModel(); - $studentModel = new StudentModel(); - - // Optional: Fetch all users and students (if needed elsewhere in the view) - $users = $userModel->findAll(); - $students = $studentModel->findAll(); - - // Pass scan logs, users, and students data to the view - $data['logs'] = $query->getResult(); // Logs with user and student info - $data['users'] = $users; // All users data - $data['students'] = $students; // All students data - - return view('/rfid/rfid_coming_soon', $data); - } - - public function rfidComingSoon() - { - return view('rfid/rfid_coming_soon'); - } -} \ No newline at end of file diff --git a/app/old/SendSMSController.php b/app/old/SendSMSController.php deleted file mode 100644 index 9dbef1a6..00000000 --- a/app/old/SendSMSController.php +++ /dev/null @@ -1,54 +0,0 @@ -Major U.S. Providers: - -AT&T -SMS: <10-digit-number>@txt.att.net -MMS: <10-digit-number>@mms.att.net - -Verizon -SMS: <10-digit-number>@vtext.com -MMS: <10-digit-number>@vzwpix.com - -T-Mobile -SMS: <10-digit-number>@tmomail.net -MMS: <10-digit-number>@tmomail.net - -Sprint (now merged with T-Mobile, but some legacy users may still use Sprint domains) -SMS: <10-digit-number>@messaging.sprintpcs.com -MMS: <10-digit-number>@pm.sprint.com - -Boost Mobile -SMS: <10-digit-number>@sms.myboostmobile.com -MMS: <10-digit-number>@myboostmobile.com - -U.S. Cellular -SMS: <10-digit-number>@email.uscc.net -MMS: <10-digit-number>@mms.uscc.net - -Metro by T-Mobile (formerly MetroPCS) -SMS: <10-digit-number>@mymetropcs.com -MMS: <10-digit-number>@mymetropcs.com - -Cricket Wireless -SMS: <10-digit-number>@sms.cricketwireless.net -MMS: <10-digit-number>@mms.cricketwireless.net - -Google Fi -SMS/MMS: <10-digit-number>@msg.fi.google.com - -Virgin Mobile USA -SMS: <10-digit-number>@vmobl.com -MMS: <10-digit-number>@vmpix.com - - - -Carrier SMS Gateway MMS Gateway -AT&T number@txt.att.net number@mms.att.net -Verizon number@vtext.com number@vzwpix.com -T-Mobile number@tmomail.net number@tmomail.net -Sprint number@messaging.sprintpcs.com number@pm.sprint.com -Boost Mobile number@sms.myboostmobile.com number@myboostmobile.com -Cricket number@sms.cricketwireless.net number@mms.cricketwireless.net -US Cellular number@email.uscc.net number@mms.uscc.net -Google Fi number@msg.fi.google.com number@msg.fi.google.com -Virgin Mobile number@vmobl.com number@vmpix.com -Metro by T-Mobile number@mymetropcs.com number@mymetropcs.com diff --git a/app/old/SettingsController.php b/app/old/SettingsController.php deleted file mode 100644 index 47a33450..00000000 --- a/app/old/SettingsController.php +++ /dev/null @@ -1,38 +0,0 @@ -settingsModel = new SettingsModel(); - } - - public function index() - { - // Retrieve current settings from the database - $settings = $this->settingsModel->getSettings(); - $timezones = timezone_identifiers_list(); - - // Pass the settings and timezones to the view - return view('administrator/settings', ['settings' => $settings, 'timezones' => $timezones]); - } - - public function update() - { - // Handle the form submission to update settings - $data = [ - 'site_name' => $this->request->getPost('site_name'), - 'administrator_email' => strtolower($this->request->getPost('administrator_email')), - 'timezone' => $this->request->getPost('timezone'), - ]; - $this->settingsModel->updateSettings($data); - return redirect()->to('/administrator/settings'); - } -} -?> \ No newline at end of file diff --git a/app/old/StaffController.php b/app/old/StaffController.php deleted file mode 100644 index 3221ceb8..00000000 --- a/app/old/StaffController.php +++ /dev/null @@ -1,156 +0,0 @@ -configModel = new ConfigurationModel(); // Assuming ConfigModel is the model handling configurations - $this->teacherClassModel = new TeacherClassModel(); - $this->userModel = new UserModel(); - $this->staffModel = new StaffModel(); - - // Retrieve the configuration values - $this->semester = $this->configModel->getConfig('semester'); - $this->schoolYear = $this->configModel->getConfig('school_year'); - } - - public function index() - { - // roles we never show - $excludedRoles = ['student', 'parent', 'guest', 'inactive']; // ← add inactive here - - $staffList = $this->staffModel - ->whereNotIn('LOWER(active_role)', array_map('strtolower', $excludedRoles)) - ->orderBy('created_at', 'DESC') - ->findAll(); - - // Preload assignments for the selected school year (across all semesters), - // mirroring the logic used in teacher_class_assignment. - $assignRows = $this->teacherClassModel - ->select('teacher_class.teacher_id, teacher_class.position, classSection.class_section_name') - ->join('classSection', 'classSection.class_section_id = teacher_class.class_section_id', 'left') - ->where('teacher_class.school_year', (string)$this->schoolYear) - ->findAll(); - - $assignByTeacher = []; - foreach ($assignRows as $r) { - $tid = (int)($r['teacher_id'] ?? 0); - if ($tid <= 0) { continue; } - $name = trim((string)($r['class_section_name'] ?? '')); - if ($name === '') { continue; } - $pos = strtolower((string)($r['position'] ?? '')); - if (!in_array($pos, ['main','ta'], true)) { continue; } - $assignByTeacher[$tid][] = $name . ' (' . $pos . ')'; - } - - $issuesCount = 0; - foreach ($staffList as &$staff) { - // attach school_id - $staff['school_id'] = $this->userModel->getSchoolIdByUserId($staff['user_id'] ?? null); - - // Verify and attach class assignments for teacher/TA roles for the selected school year - $role = strtolower((string)($staff['active_role'] ?? '')); - if (in_array($role, ['teacher', 'teacher_assistant'], true)) { - $tid = (int)($staff['user_id'] ?? 0); - $labels = $assignByTeacher[$tid] ?? []; - if (!empty($labels)) { - $staff['class_section'] = implode(', ', array_unique($labels)); - $staff['verification_issue'] = false; - } else { - $staff['class_section'] = 'No class assigned'; - $staff['verification_issue'] = true; - $issuesCount++; - } - } else { - $staff['class_section'] = '—'; - $staff['verification_issue'] = false; - } - } - unset($staff); // break reference - - return view('staff/index', [ - 'staff' => $staffList, - 'issues_count' => $issuesCount, - 'semester' => $this->semester, - 'school_year' => $this->schoolYear, - ]); - } - - public function create() - { - return view('staff/create'); - } - - public function store() - { - $now = utc_now(); - $payload = [ - 'firstname' => $this->request->getPost('firstname'), - 'lastname' => $this->request->getPost('lastname'), - 'email' => $this->request->getPost('email'), - 'phone' => $this->request->getPost('phone'), - 'role_name' => $this->request->getPost('role_name'), - 'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear, - 'status' => $this->request->getPost('status') ?? 'Active', - 'created_at' => $now, - 'updated_at' => $now, - ]; - - if (!$this->staffModel->insert($payload)) { - return redirect()->back()->withInput()->with('error', 'Failed to create staff.'); - } - - return redirect()->to('/staff')->with('success', 'Staff member added.'); - } - - public function edit($id) - { - $user = $this->staffModel->find($id); - - if (!$user) { - return redirect()->to('staff')->with('error', 'Staff member not found.'); - } - - return view('staff/edit', ['user' => $user]); - } - - public function update($id) - { - // Only update fields provided by the form to avoid wiping data - $data = []; - foreach (['firstname','lastname','email','phone','role_name','school_year','status'] as $field) { - $val = $this->request->getPost($field); - if ($val !== null && $val !== '') { - $data[$field] = $val; - } - } - // Always bump updated_at - $data['updated_at'] = utc_now(); - - if (empty($data)) { - return redirect()->back()->with('error', 'No changes detected.'); - } - - if (!$this->staffModel->update($id, $data)) { - return redirect()->back()->withInput()->with('error', 'Failed to update staff.'); - } - - return redirect()->to('/staff')->with('success', 'Staff member updated.'); - } -} diff --git a/app/old/StatsController.php b/app/old/StatsController.php deleted file mode 100644 index 068baf92..00000000 --- a/app/old/StatsController.php +++ /dev/null @@ -1,32 +0,0 @@ -getStats(); - - return view('stats_view', $data); - } - - public function calendar() - { - return view('/calendars'); - } - - public function support() - { - return view('/support'); - } - - public function contactUs() - { - return view('/contact_us'); - } -} diff --git a/app/old/SupportController.php b/app/old/SupportController.php deleted file mode 100644 index 6fd61c06..00000000 --- a/app/old/SupportController.php +++ /dev/null @@ -1,133 +0,0 @@ -get('user_id'); - - // If no user_id is found in the session, redirect with an error - if (!$user_id) { - return redirect()->back()->with('error', 'You must be logged in to submit a support request.'); - } - - // Load the UserModel to retrieve user information - $userModel = new UserModel(); - $user = $userModel->find($user_id); - - // If the user does not exist, redirect with an error - if (!$user) { - return redirect()->back()->with('error', 'User not found. Please contact support.'); - } - - // Retrieve form data - $subject = $this->request->getPost('subject'); - $message = $this->request->getPost('message'); - - // Get the user's full name using firstname and lastname - $full_name = $user['firstname'] . ' ' . $user['lastname']; - - // Call the function to send an email using SMTP (configured via Email.php or .env) - $sendStatus = $this->sendSupportEmail($user['email'], $full_name, $subject, $message); - - if ($sendStatus === true) { - return redirect()->back()->with('success', 'Your support request has been submitted successfully.'); - } else { - // If email fails, log the error and display a message - log_message('error', $sendStatus); - return redirect()->back()->with('error', 'Failed to send your support request. Please try again later.'); - } - } - - /** - * Function to send an email via SMTP from user to support@domain.org - * - * @param string $user_email - The email address of the user sending the request. - * @param string $user_name - The name of the user sending the request. - * @param string $subject - The subject of the support request. - * @param string $message - The body of the support request. - * @return bool|string - Returns true if email sent successfully, otherwise error message. - */ - public function sendSupportEmail($user_email, $full_name, $subject, $message) - { - $mailer = new EmailController(); - $ok = $mailer->sendEmail( - 'support@alrahmaisgl.org', - $subject, - nl2br($message), - null, - $user_email, - $full_name - ); - - return $ok === true ? true : 'Failed to send support email.'; - } - - - - public function requests() - { - $supportModel = new SupportRequestModel(); - $userId = session()->get('user_id'); - $data['requests'] = $supportModel->where('user_id', $userId)->findAll(); - - return view('/support_requests', $data); - } - - public function supportTeacher() - { - return view('/teacher/teacher_support'); - } -} - - -/* - public function submit() - { - $validation = \Config\Services::validation(); - - // Define validation rules - $validation->setRules([ - 'subject' => 'required|min_length[3]', - 'message' => 'required|min_length[10]', - ]); - - if (!$validation->withRequest($this->request)->run()) { - return view('/support', [ - 'validation' => $validation - ]); - } - - $supportModel = new SupportRequestModel(); - $supportModel->save([ - 'user_id' => session()->get('user_id'), - 'subject' => $this->request->getPost('subject'), - 'message' => $this->request->getPost('message'), - 'status' => 'open' - ]); - - session()->setFlashdata('success', 'Your support request has been submitted.'); - - return redirect()->to('/support'); - } -*/ diff --git a/app/old/TestDBController.php b/app/old/TestDBController.php deleted file mode 100644 index 70d30230..00000000 --- a/app/old/TestDBController.php +++ /dev/null @@ -1,27 +0,0 @@ -query('SELECT 1'); - $result = $query->getResult(); - - if ($result) { - echo "Database connection is successful."; - } else { - echo "Database connection failed."; - } - } catch (DatabaseException $e) { - echo "Database connection failed: " . $e->getMessage(); - } - } -} diff --git a/app/old/TimeService.php b/app/old/TimeService.php deleted file mode 100644 index 15cd02c2..00000000 --- a/app/old/TimeService.php +++ /dev/null @@ -1,222 +0,0 @@ -cachedUserTz = $this->detectUserTimezone($request); - } - - /** - * Get the resolved user timezone (detect lazily if needed). - */ - public function userTimezone(?RequestInterface $request = null): string - { - if ($this->cachedUserTz !== null) { - return $this->cachedUserTz; - } - - $this->cachedUserTz = $this->detectUserTimezone($request); - return $this->cachedUserTz; - } - - /** - * Core detection logic (headers > user pref > settings > school config > default). - */ - public function detectUserTimezone(?RequestInterface $request = null): string - { - $request = $request ?: service('request'); - - // 1) Explicit client header - try { - $tzHeader = $request->getHeaderLine('X-Timezone') - ?: $request->getHeaderLine('Timezone') - ?: $request->getHeaderLine('Accept-Timezone') - ?: null; - - if ($tzHeader && in_array($tzHeader, timezone_identifiers_list(), true)) { - return $tzHeader; - } - } catch (\Throwable $e) { - // ignore - } - - // 2) User preference (if logged-in) - try { - $userId = (int) (session('user_id') ?: 0); - if ($userId > 0) { - // Do not select a specific column; some DBs may not have it yet - $pref = (new PreferencesModel()) - ->where('user_id', $userId) - ->first(); - $tz = $pref['timezone'] ?? null; - if ($tz && in_array($tz, timezone_identifiers_list(), true)) { - return $tz; - } - } - } catch (\Throwable $e) { - // ignore - } - - // 3) Global settings timezone (if available) - try { - $settings = (new SettingsModel())->getSettings(); - $tz = $settings['timezone'] ?? null; - if ($tz && in_array($tz, timezone_identifiers_list(), true)) { - return $tz; - } - } catch (\Throwable $e) { - // ignore - } - - // 4) School attendance timezone (if defined) - try { - $tz = (string) (config('School')->attendance['timezone'] ?? ''); - if ($tz && in_array($tz, timezone_identifiers_list(), true)) { - return $tz; - } - } catch (\Throwable $e) { - // ignore - } - - // 5) Default - return $this->defaultUserTimezone; - } - - /** - * Server timezone (for storage/UTC operations). - */ - public function serverTimezone(): string - { - return $this->serverTimezone; - } - - /** - * Current time in user timezone as CI Time. - */ - public function nowLocal(?string $tz = null): Time - { - $tz = $tz ?: $this->userTimezone(); - return Time::now($tz); - } - - /** - * Current time in UTC as CI Time. - */ - public function nowUTC(): Time - { - return Time::now($this->serverTimezone); - } - - /** - * Convert any supported input to UTC string (Y-m-d H:i:s by default). - * - * @param string|Time|\DateTimeInterface|null $value - */ - public function toUTC($value, ?string $fromTz = null, string $format = 'Y-m-d H:i:s'): ?string - { - if ($value === null || $value === '') { - return null; - } - - $fromTz = $fromTz ?: $this->userTimezone(); - - try { - if ($value instanceof Time) { - return $value->setTimezone($this->serverTimezone)->toDateTimeString(); - } - if ($value instanceof \DateTimeInterface) { - return Time::createFromInstance($value) - ->setTimezone($this->serverTimezone) - ->format($format); - } - - // assume string - return Time::parse((string)$value, $fromTz) - ->setTimezone($this->serverTimezone) - ->format($format); - } catch (\Throwable $e) { - return null; - } - } - - /** - * Convert UTC (or provided timezone) to user-local string. - * - * @param string|Time|\DateTimeInterface|null $value - */ - public function toLocal($value, ?string $sourceTz = null, ?string $targetTz = null, string $format = 'Y-m-d H:i:s'): ?string - { - if ($value === null || $value === '') { - return null; - } - - $targetTz = $targetTz ?: $this->userTimezone(); - if ($sourceTz === null && $this->isDateOnlyString($value)) { - // Date-only strings should not shift across timezones. - $sourceTz = $targetTz; - } else { - $sourceTz = $sourceTz ?: $this->serverTimezone; - } - - try { - if ($value instanceof Time) { - return $value->setTimezone($targetTz)->format($format); - } - if ($value instanceof \DateTimeInterface) { - return Time::createFromInstance($value) - ->setTimezone($targetTz) - ->format($format); - } - - // assume string - return Time::parse((string)$value, $sourceTz) - ->setTimezone($targetTz) - ->format($format); - } catch (\Throwable $e) { - return null; - } - } - - /** - * Convenience formatter for user-local. - */ - public function formatLocal($value, string $format = 'Y-m-d H:i', ?string $sourceTz = null): string - { - return (string) ($this->toLocal($value, $sourceTz, null, $format) ?? ''); - } - - /** - * Convenience formatter for UTC. - */ - public function formatUTC($value, string $format = 'Y-m-d H:i:s', ?string $fromTz = null): string - { - return (string) ($this->toUTC($value, $fromTz, $format) ?? ''); - } - - private function isDateOnlyString($value): bool - { - if (!is_string($value)) { - return false; - } - - $value = trim($value); - return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value); - } -} diff --git a/app/old/UiController.php b/app/old/UiController.php deleted file mode 100644 index 78619f8c..00000000 --- a/app/old/UiController.php +++ /dev/null @@ -1,102 +0,0 @@ -request->getGet('accent'); - $menu = (string) $this->request->getGet('menu'); - $menuBg = (string) $this->request->getGet('menu_bg'); - $menuTx = (string) $this->request->getGet('menu_text'); - $menuMd = (string) $this->request->getGet('menu_mode'); // optional: 'light'|'dark'|'auto' - - $updates = []; - if ($accent && isset(($styleCfg->stylePalettes ?? [])[$accent])) { - session()->set('style_color', $accent); - $updates['style_color'] = $accent; - } - if ($menu && isset(($styleCfg->menuPalettes ?? [])[$menu])) { - session()->set('menu_color', $menu); - $updates['menu_color'] = $menu; - $updates['menu_custom_bg'] = null; - $updates['menu_custom_text'] = null; - $updates['menu_custom_mode'] = null; - } elseif (strtolower($menu) === 'custom' || ($menuBg !== '' || $menuTx !== '')) { - // Allow custom menu colors via GET: menu=custom&menu_bg=#hex&menu_text=#hex&menu_mode=dark|light|auto - $bg = $this->normalizeHex($menuBg); - $tx = $this->normalizeHex($menuTx); - if ($bg === '' && $tx === '') { - // no valid values; ignore - } else { - if ($bg === '') $bg = '#0f172a'; - if ($tx === '') $tx = '#ffffff'; - $mode = in_array($menuMd, ['light','dark','auto'], true) ? $menuMd : 'auto'; - session()->set('menu_color', 'custom'); - session()->set('menu_custom_bg', $bg); - session()->set('menu_custom_text', $tx); - session()->set('menu_custom_mode', $mode); - $updates['menu_color'] = 'custom'; - $updates['menu_custom_bg'] = $bg; - $updates['menu_custom_text'] = $tx; - $updates['menu_custom_mode'] = $mode; - } - } - - $userId = (int) session()->get('user_id'); - if ($userId && !empty($updates)) { - $prefsModel = new PreferencesModel(); - $existing = $prefsModel->where('user_id', $userId)->first(); - if ($existing) { - $prefsModel->update($existing['id'], $updates); - } else { - $updates['user_id'] = $userId; - $prefsModel->insert($updates); - } - } - - // Redirect back to referrer or home - $back = (string) $this->request->getGet('back'); - if ($back) return redirect()->to($back); - $ref = $this->request->getServer('HTTP_REFERER'); - if ($ref) return redirect()->to($ref); - return redirect()->to(site_url('/')); - } - - /** - * Normalize a CSS hex color (#RGB or #RRGGBB). Returns '' on failure. - */ - private function normalizeHex(string $hex): string - { - $h = trim($hex); - if ($h === '') return ''; - if ($h[0] !== '#') $h = '#' . $h; - if (preg_match('/^#([0-9a-fA-F]{3})$/', $h, $m)) { - // Expand #RGB to #RRGGBB - $r = $m[1][0]; $g = $m[1][1]; $b = $m[1][2]; - return '#' . $r . $r . $g . $g . $b . $b; - } - if (preg_match('/^#([0-9a-fA-F]{6})$/', $h)) return strtoupper($h); - return ''; - } - - /** - * Choose navbar mode based on background luminance: 'dark' for dark BG, else 'light'. - */ - private function autoModeForBg(string $hexBg): string - { - $hex = ltrim($hexBg, '#'); - if (strlen($hex) !== 6) return 'dark'; - $r = hexdec(substr($hex,0,2)); - $g = hexdec(substr($hex,2,2)); - $b = hexdec(substr($hex,4,2)); - // Relative luminance approximation - $lum = (0.2126*$r + 0.7152*$g + 0.0722*$b) / 255.0; - return ($lum < 0.5) ? 'dark' : 'light'; - } -} diff --git a/bootstrap/app.php b/bootstrap/app.php index 85d31086..f609b86c 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -16,6 +16,11 @@ return Application::configure(basePath: dirname(__DIR__)) 'auth.multi' => \App\Http\Middleware\MultiAuth::class, 'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, 'jwt.refresh' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\RefreshToken::class, + 'api.jwt' => \App\Http\Middleware\ApiJwtAuth::class, + 'perm' => \App\Http\Middleware\RequirePermission::class, + 'timezone' => \App\Http\Middleware\PrimeTimezone::class, + 'cleanup.scheduler' => \App\Http\Middleware\CleanupScheduler::class, + 'auth.docs' => \App\Http\Middleware\ApiDocsAuth::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/database/migrations/2026_02_23_204001_create_users_table.php b/database/migrations/2026_02_23_204001_create_users_table.php index 453d57fd..c9f50842 100644 --- a/database/migrations/2026_02_23_204001_create_users_table.php +++ b/database/migrations/2026_02_23_204001_create_users_table.php @@ -7,6 +7,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('users')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `users` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204002_create_students_table.php b/database/migrations/2026_02_23_204002_create_students_table.php index 3f7c83ef..c078e396 100644 --- a/database/migrations/2026_02_23_204002_create_students_table.php +++ b/database/migrations/2026_02_23_204002_create_students_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('students')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `students` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204030_create_expenses_table.php b/database/migrations/2026_02_23_204030_create_expenses_table.php index 11abfeee..00f8c9fb 100644 --- a/database/migrations/2026_02_23_204030_create_expenses_table.php +++ b/database/migrations/2026_02_23_204030_create_expenses_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('expenses')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `expenses` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204035_create_current_incident_table.php b/database/migrations/2026_02_23_204035_create_current_incident_table.php index f039ff4c..7ef64a45 100644 --- a/database/migrations/2026_02_23_204035_create_current_incident_table.php +++ b/database/migrations/2026_02_23_204035_create_current_incident_table.php @@ -7,6 +7,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('current_incident')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `current_incident` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204036_create_incident_table.php b/database/migrations/2026_02_23_204036_create_incident_table.php index c9272069..20e4e2d7 100644 --- a/database/migrations/2026_02_23_204036_create_incident_table.php +++ b/database/migrations/2026_02_23_204036_create_incident_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('incident')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `incident` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204037_create_grading_locks_table.php b/database/migrations/2026_02_23_204037_create_grading_locks_table.php index df4c89a5..08d1e80d 100644 --- a/database/migrations/2026_02_23_204037_create_grading_locks_table.php +++ b/database/migrations/2026_02_23_204037_create_grading_locks_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('grading_locks')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `grading_locks` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204038_create_homework_table.php b/database/migrations/2026_02_23_204038_create_homework_table.php index 801033d4..c07c13e3 100644 --- a/database/migrations/2026_02_23_204038_create_homework_table.php +++ b/database/migrations/2026_02_23_204038_create_homework_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('homework')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `homework` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204039_create_inventory_categories_table.php b/database/migrations/2026_02_23_204039_create_inventory_categories_table.php index b19e7e2d..0d9bc047 100644 --- a/database/migrations/2026_02_23_204039_create_inventory_categories_table.php +++ b/database/migrations/2026_02_23_204039_create_inventory_categories_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('inventory_categories')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `inventory_categories` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204040_create_inventory_items_table.php b/database/migrations/2026_02_23_204040_create_inventory_items_table.php index 8b6e547d..457d32ce 100644 --- a/database/migrations/2026_02_23_204040_create_inventory_items_table.php +++ b/database/migrations/2026_02_23_204040_create_inventory_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('inventory_items')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `inventory_items` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204041_create_inventory_movements_table.php b/database/migrations/2026_02_23_204041_create_inventory_movements_table.php index db27254c..d114fea5 100644 --- a/database/migrations/2026_02_23_204041_create_inventory_movements_table.php +++ b/database/migrations/2026_02_23_204041_create_inventory_movements_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('inventory_movements')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `inventory_movements` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204042_create_invoice_event_table.php b/database/migrations/2026_02_23_204042_create_invoice_event_table.php index f681882f..7fcceed9 100644 --- a/database/migrations/2026_02_23_204042_create_invoice_event_table.php +++ b/database/migrations/2026_02_23_204042_create_invoice_event_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('invoice_event')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `invoice_event` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204043_create_invoices_table.php b/database/migrations/2026_02_23_204043_create_invoices_table.php index 6020b08f..39d5a618 100644 --- a/database/migrations/2026_02_23_204043_create_invoices_table.php +++ b/database/migrations/2026_02_23_204043_create_invoices_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('invoices')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `invoices` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204044_create_invoice_students_list_table.php b/database/migrations/2026_02_23_204044_create_invoice_students_list_table.php index 43afd2f9..0d7e6f69 100644 --- a/database/migrations/2026_02_23_204044_create_invoice_students_list_table.php +++ b/database/migrations/2026_02_23_204044_create_invoice_students_list_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('invoice_students_list')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `invoice_students_list` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204045_create_ip_attempts_table.php b/database/migrations/2026_02_23_204045_create_ip_attempts_table.php index 4bb8ff04..18e9d66c 100644 --- a/database/migrations/2026_02_23_204045_create_ip_attempts_table.php +++ b/database/migrations/2026_02_23_204045_create_ip_attempts_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('ip_attempts')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `ip_attempts` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204046_create_late_slip_logs_table.php b/database/migrations/2026_02_23_204046_create_late_slip_logs_table.php index 1eb9dc19..3d344e16 100644 --- a/database/migrations/2026_02_23_204046_create_late_slip_logs_table.php +++ b/database/migrations/2026_02_23_204046_create_late_slip_logs_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('late_slip_logs')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `late_slip_logs` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204047_create_login_activity_table.php b/database/migrations/2026_02_23_204047_create_login_activity_table.php index 56abe823..5d9f4355 100644 --- a/database/migrations/2026_02_23_204047_create_login_activity_table.php +++ b/database/migrations/2026_02_23_204047_create_login_activity_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('login_activity')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `login_activity` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204048_create_manual_payments_table.php b/database/migrations/2026_02_23_204048_create_manual_payments_table.php index 4a91dbda..fa543a5b 100644 --- a/database/migrations/2026_02_23_204048_create_manual_payments_table.php +++ b/database/migrations/2026_02_23_204048_create_manual_payments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('manual_payments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `manual_payments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204049_create_messages_table.php b/database/migrations/2026_02_23_204049_create_messages_table.php index 19f95964..65c899a6 100644 --- a/database/migrations/2026_02_23_204049_create_messages_table.php +++ b/database/migrations/2026_02_23_204049_create_messages_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('messages')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `messages` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204050_create_midterm_exam_table.php b/database/migrations/2026_02_23_204050_create_midterm_exam_table.php index 05e0a3e2..c0c66ed4 100644 --- a/database/migrations/2026_02_23_204050_create_midterm_exam_table.php +++ b/database/migrations/2026_02_23_204050_create_midterm_exam_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('midterm_exam')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `midterm_exam` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204051_create_missing_score_overrides_table.php b/database/migrations/2026_02_23_204051_create_missing_score_overrides_table.php index bcd34de3..7d0d5022 100644 --- a/database/migrations/2026_02_23_204051_create_missing_score_overrides_table.php +++ b/database/migrations/2026_02_23_204051_create_missing_score_overrides_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('missing_score_overrides')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `missing_score_overrides` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204052_create_nav_items_table.php b/database/migrations/2026_02_23_204052_create_nav_items_table.php index 430ea69b..e5170668 100644 --- a/database/migrations/2026_02_23_204052_create_nav_items_table.php +++ b/database/migrations/2026_02_23_204052_create_nav_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('nav_items')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `nav_items` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204053_create_notifications_table.php b/database/migrations/2026_02_23_204053_create_notifications_table.php index f4baf204..6edca331 100644 --- a/database/migrations/2026_02_23_204053_create_notifications_table.php +++ b/database/migrations/2026_02_23_204053_create_notifications_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('notifications')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `notifications` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204054_create_parent_attendance_reports_table.php b/database/migrations/2026_02_23_204054_create_parent_attendance_reports_table.php index 547edf4a..40439b55 100644 --- a/database/migrations/2026_02_23_204054_create_parent_attendance_reports_table.php +++ b/database/migrations/2026_02_23_204054_create_parent_attendance_reports_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('parent_attendance_reports')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `parent_attendance_reports` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204055_create_parent_meeting_schedules_table.php b/database/migrations/2026_02_23_204055_create_parent_meeting_schedules_table.php index 0fa278ae..4f00b430 100644 --- a/database/migrations/2026_02_23_204055_create_parent_meeting_schedules_table.php +++ b/database/migrations/2026_02_23_204055_create_parent_meeting_schedules_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('parent_meeting_schedules')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `parent_meeting_schedules` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204056_create_parents_table.php b/database/migrations/2026_02_23_204056_create_parents_table.php index 02b41ff5..50db8e70 100644 --- a/database/migrations/2026_02_23_204056_create_parents_table.php +++ b/database/migrations/2026_02_23_204056_create_parents_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('parents')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `parents` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204057_create_parent_notifications_table.php b/database/migrations/2026_02_23_204057_create_parent_notifications_table.php index 90ece232..36157698 100644 --- a/database/migrations/2026_02_23_204057_create_parent_notifications_table.php +++ b/database/migrations/2026_02_23_204057_create_parent_notifications_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('parent_notifications')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `parent_notifications` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204058_create_participation_table.php b/database/migrations/2026_02_23_204058_create_participation_table.php index 2855cb38..91f54c7a 100644 --- a/database/migrations/2026_02_23_204058_create_participation_table.php +++ b/database/migrations/2026_02_23_204058_create_participation_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('participation')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `participation` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204059_create_password_resets_table.php b/database/migrations/2026_02_23_204059_create_password_resets_table.php index 7e7f4595..daf2c27a 100644 --- a/database/migrations/2026_02_23_204059_create_password_resets_table.php +++ b/database/migrations/2026_02_23_204059_create_password_resets_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('password_resets')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `password_resets` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204100_create_password_reset_requests_table.php b/database/migrations/2026_02_23_204100_create_password_reset_requests_table.php index f5a5ccbd..2d3d7de1 100644 --- a/database/migrations/2026_02_23_204100_create_password_reset_requests_table.php +++ b/database/migrations/2026_02_23_204100_create_password_reset_requests_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('password_reset_requests')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `password_reset_requests` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204100_create_reimbursement_batches_table.php b/database/migrations/2026_02_23_204100_create_reimbursement_batches_table.php index bcfad3d3..098aed8d 100644 --- a/database/migrations/2026_02_23_204100_create_reimbursement_batches_table.php +++ b/database/migrations/2026_02_23_204100_create_reimbursement_batches_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('reimbursement_batches')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `reimbursement_batches` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204101_create_paypal_payments_table.php b/database/migrations/2026_02_23_204101_create_paypal_payments_table.php index 00644235..aed7c3af 100644 --- a/database/migrations/2026_02_23_204101_create_paypal_payments_table.php +++ b/database/migrations/2026_02_23_204101_create_paypal_payments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('paypal_payments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `paypal_payments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204101_create_reimbursements_table.php b/database/migrations/2026_02_23_204101_create_reimbursements_table.php index df1658e2..19fde635 100644 --- a/database/migrations/2026_02_23_204101_create_reimbursements_table.php +++ b/database/migrations/2026_02_23_204101_create_reimbursements_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('reimbursements')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `reimbursements` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204102_create_payment_error_table.php b/database/migrations/2026_02_23_204102_create_payment_error_table.php index 838fcfac..a502cf01 100644 --- a/database/migrations/2026_02_23_204102_create_payment_error_table.php +++ b/database/migrations/2026_02_23_204102_create_payment_error_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('payment_error')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `payment_error` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204103_create_payments_table.php b/database/migrations/2026_02_23_204103_create_payments_table.php index 3a230d8c..32b142d4 100644 --- a/database/migrations/2026_02_23_204103_create_payments_table.php +++ b/database/migrations/2026_02_23_204103_create_payments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('payments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `payments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204104_create_payment_notification_logs_table.php b/database/migrations/2026_02_23_204104_create_payment_notification_logs_table.php index 5b786b5d..8d9b84ae 100644 --- a/database/migrations/2026_02_23_204104_create_payment_notification_logs_table.php +++ b/database/migrations/2026_02_23_204104_create_payment_notification_logs_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('payment_notification_logs')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `payment_notification_logs` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204105_create_payment_transactions_table.php b/database/migrations/2026_02_23_204105_create_payment_transactions_table.php index da834c40..c444fc21 100644 --- a/database/migrations/2026_02_23_204105_create_payment_transactions_table.php +++ b/database/migrations/2026_02_23_204105_create_payment_transactions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('payment_transactions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `payment_transactions` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204106_create_paypal_transactions_table.php b/database/migrations/2026_02_23_204106_create_paypal_transactions_table.php index 0bfbcace..431d6721 100644 --- a/database/migrations/2026_02_23_204106_create_paypal_transactions_table.php +++ b/database/migrations/2026_02_23_204106_create_paypal_transactions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('paypal_transactions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `paypal_transactions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204107_create_permissions_table.php b/database/migrations/2026_02_23_204107_create_permissions_table.php index e4c1fb46..65b8cb5c 100644 --- a/database/migrations/2026_02_23_204107_create_permissions_table.php +++ b/database/migrations/2026_02_23_204107_create_permissions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('permissions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `permissions` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204108_create_placement_batches_table.php b/database/migrations/2026_02_23_204108_create_placement_batches_table.php index 5bb65f41..74d72f93 100644 --- a/database/migrations/2026_02_23_204108_create_placement_batches_table.php +++ b/database/migrations/2026_02_23_204108_create_placement_batches_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('placement_batches')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `placement_batches` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204109_create_placement_levels_table.php b/database/migrations/2026_02_23_204109_create_placement_levels_table.php index 248beba5..b50db61e 100644 --- a/database/migrations/2026_02_23_204109_create_placement_levels_table.php +++ b/database/migrations/2026_02_23_204109_create_placement_levels_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('placement_levels')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `placement_levels` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204110_create_placement_scores_table.php b/database/migrations/2026_02_23_204110_create_placement_scores_table.php index d2c8a316..fc739390 100644 --- a/database/migrations/2026_02_23_204110_create_placement_scores_table.php +++ b/database/migrations/2026_02_23_204110_create_placement_scores_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('placement_scores')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `placement_scores` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204111_create_user_preferences_table.php b/database/migrations/2026_02_23_204111_create_user_preferences_table.php index 26b31108..f348fae1 100644 --- a/database/migrations/2026_02_23_204111_create_user_preferences_table.php +++ b/database/migrations/2026_02_23_204111_create_user_preferences_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('user_preferences')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `user_preferences` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204112_create_print_requests_table.php b/database/migrations/2026_02_23_204112_create_print_requests_table.php index b4964da5..3d229cde 100644 --- a/database/migrations/2026_02_23_204112_create_print_requests_table.php +++ b/database/migrations/2026_02_23_204112_create_print_requests_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('print_requests')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `print_requests` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204113_create_project_table.php b/database/migrations/2026_02_23_204113_create_project_table.php index 0ad94b8c..6678c2f1 100644 --- a/database/migrations/2026_02_23_204113_create_project_table.php +++ b/database/migrations/2026_02_23_204113_create_project_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('project')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `project` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204114_create_promotion_queue_table.php b/database/migrations/2026_02_23_204114_create_promotion_queue_table.php index 9e561000..d619a54f 100644 --- a/database/migrations/2026_02_23_204114_create_promotion_queue_table.php +++ b/database/migrations/2026_02_23_204114_create_promotion_queue_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('promotion_queue')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `promotion_queue` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204117_create_quiz_table.php b/database/migrations/2026_02_23_204117_create_quiz_table.php index 95d1ee5d..a845894c 100644 --- a/database/migrations/2026_02_23_204117_create_quiz_table.php +++ b/database/migrations/2026_02_23_204117_create_quiz_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('quiz')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `quiz` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204118_create_refunds_table.php b/database/migrations/2026_02_23_204118_create_refunds_table.php index 9afac796..bf7cccd3 100644 --- a/database/migrations/2026_02_23_204118_create_refunds_table.php +++ b/database/migrations/2026_02_23_204118_create_refunds_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('refunds')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `refunds` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204119_create_reimbursement_batch_admin_files_table.php b/database/migrations/2026_02_23_204119_create_reimbursement_batch_admin_files_table.php index 27254a2e..645e53c1 100644 --- a/database/migrations/2026_02_23_204119_create_reimbursement_batch_admin_files_table.php +++ b/database/migrations/2026_02_23_204119_create_reimbursement_batch_admin_files_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('reimbursement_batch_admin_files')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `reimbursement_batch_admin_files` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204123_create_roles_table.php b/database/migrations/2026_02_23_204123_create_roles_table.php index 68faec99..2650dd9f 100644 --- a/database/migrations/2026_02_23_204123_create_roles_table.php +++ b/database/migrations/2026_02_23_204123_create_roles_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('roles')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `roles` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204124_create_role_nav_items_table.php b/database/migrations/2026_02_23_204124_create_role_nav_items_table.php index 2d5d1c1d..6239e352 100644 --- a/database/migrations/2026_02_23_204124_create_role_nav_items_table.php +++ b/database/migrations/2026_02_23_204124_create_role_nav_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('role_nav_items')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `role_nav_items` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204125_create_role_permissions_table.php b/database/migrations/2026_02_23_204125_create_role_permissions_table.php index fcc32884..01e068e5 100644 --- a/database/migrations/2026_02_23_204125_create_role_permissions_table.php +++ b/database/migrations/2026_02_23_204125_create_role_permissions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('role_permissions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `role_permissions` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204126_create_score_comments_table.php b/database/migrations/2026_02_23_204126_create_score_comments_table.php index 65be59cf..09f37fb8 100644 --- a/database/migrations/2026_02_23_204126_create_score_comments_table.php +++ b/database/migrations/2026_02_23_204126_create_score_comments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('score_comments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `score_comments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204127_create_sections_table.php b/database/migrations/2026_02_23_204127_create_sections_table.php index d9f0142e..57bf4e14 100644 --- a/database/migrations/2026_02_23_204127_create_sections_table.php +++ b/database/migrations/2026_02_23_204127_create_sections_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('sections')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `sections` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204128_create_semester_scores_table.php b/database/migrations/2026_02_23_204128_create_semester_scores_table.php index 683df47f..c974803e 100644 --- a/database/migrations/2026_02_23_204128_create_semester_scores_table.php +++ b/database/migrations/2026_02_23_204128_create_semester_scores_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('semester_scores')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `semester_scores` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204129_create_settings_table.php b/database/migrations/2026_02_23_204129_create_settings_table.php index ec0cf2fc..78712afc 100644 --- a/database/migrations/2026_02_23_204129_create_settings_table.php +++ b/database/migrations/2026_02_23_204129_create_settings_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('settings')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `settings` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204130_create_staff_attendance_table.php b/database/migrations/2026_02_23_204130_create_staff_attendance_table.php index 57847773..3c797cee 100644 --- a/database/migrations/2026_02_23_204130_create_staff_attendance_table.php +++ b/database/migrations/2026_02_23_204130_create_staff_attendance_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('staff_attendance')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `staff_attendance` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204131_create_staff_table.php b/database/migrations/2026_02_23_204131_create_staff_table.php index bc4be890..7e9539cf 100644 --- a/database/migrations/2026_02_23_204131_create_staff_table.php +++ b/database/migrations/2026_02_23_204131_create_staff_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('staff')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `staff` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204133_create_student_allergies_table.php b/database/migrations/2026_02_23_204133_create_student_allergies_table.php index bec61517..7d89fadd 100644 --- a/database/migrations/2026_02_23_204133_create_student_allergies_table.php +++ b/database/migrations/2026_02_23_204133_create_student_allergies_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('student_allergies')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `student_allergies` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204134_create_student_class_table.php b/database/migrations/2026_02_23_204134_create_student_class_table.php index 9891996b..0a94d28d 100644 --- a/database/migrations/2026_02_23_204134_create_student_class_table.php +++ b/database/migrations/2026_02_23_204134_create_student_class_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('student_class')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `student_class` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204135_create_student_medical_conditions_table.php b/database/migrations/2026_02_23_204135_create_student_medical_conditions_table.php index f4d5a5a3..0bf8f1b1 100644 --- a/database/migrations/2026_02_23_204135_create_student_medical_conditions_table.php +++ b/database/migrations/2026_02_23_204135_create_student_medical_conditions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('student_medical_conditions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `student_medical_conditions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204137_create_subject_curriculum_items_table.php b/database/migrations/2026_02_23_204137_create_subject_curriculum_items_table.php index 84c7c7da..b8500c64 100644 --- a/database/migrations/2026_02_23_204137_create_subject_curriculum_items_table.php +++ b/database/migrations/2026_02_23_204137_create_subject_curriculum_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('subject_curriculum_items')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `subject_curriculum_items` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204138_create_teacher_class_table.php b/database/migrations/2026_02_23_204138_create_teacher_class_table.php index eb81e955..a7cbd2dc 100644 --- a/database/migrations/2026_02_23_204138_create_teacher_class_table.php +++ b/database/migrations/2026_02_23_204138_create_teacher_class_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('teacher_class')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `teacher_class` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204140_create_teacher_submission_notification_history_table.php b/database/migrations/2026_02_23_204140_create_teacher_submission_notification_history_table.php index 7729ec80..efc2977f 100644 --- a/database/migrations/2026_02_23_204140_create_teacher_submission_notification_history_table.php +++ b/database/migrations/2026_02_23_204140_create_teacher_submission_notification_history_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('teacher_submission_notification_history')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `teacher_submission_notification_history` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204142_create_user_notifications_table.php b/database/migrations/2026_02_23_204142_create_user_notifications_table.php index d943fd35..e12ff70a 100644 --- a/database/migrations/2026_02_23_204142_create_user_notifications_table.php +++ b/database/migrations/2026_02_23_204142_create_user_notifications_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('user_notifications')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `user_notifications` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204143_create_user_roles_table.php b/database/migrations/2026_02_23_204143_create_user_roles_table.php index 26c109f5..f1d6384a 100644 --- a/database/migrations/2026_02_23_204143_create_user_roles_table.php +++ b/database/migrations/2026_02_23_204143_create_user_roles_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('user_roles')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `user_roles` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204144_create_whatsapp_group_links_table.php b/database/migrations/2026_02_23_204144_create_whatsapp_group_links_table.php index 105b150a..fcea6509 100644 --- a/database/migrations/2026_02_23_204144_create_whatsapp_group_links_table.php +++ b/database/migrations/2026_02_23_204144_create_whatsapp_group_links_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('whatsapp_group_links')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `whatsapp_group_links` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204145_create_whatsapp_group_memberships_table.php b/database/migrations/2026_02_23_204145_create_whatsapp_group_memberships_table.php index 78cbf6f4..f087535a 100644 --- a/database/migrations/2026_02_23_204145_create_whatsapp_group_memberships_table.php +++ b/database/migrations/2026_02_23_204145_create_whatsapp_group_memberships_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('whatsapp_group_memberships')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `whatsapp_group_memberships` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204146_create_whatsapp_invites_log_table.php b/database/migrations/2026_02_23_204146_create_whatsapp_invites_log_table.php index f08b92f6..3899f034 100644 --- a/database/migrations/2026_02_23_204146_create_whatsapp_invites_log_table.php +++ b/database/migrations/2026_02_23_204146_create_whatsapp_invites_log_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('whatsapp_invites_log')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `whatsapp_invites_log` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_204150_create_class_progress_reports_table.php b/database/migrations/2026_02_23_204150_create_class_progress_reports_table.php index bdf1380b..0867cf2c 100644 --- a/database/migrations/2026_02_23_204150_create_class_progress_reports_table.php +++ b/database/migrations/2026_02_23_204150_create_class_progress_reports_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('class_progress_reports')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `class_progress_reports` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204151_create_report_card_acknowledgements_table.php b/database/migrations/2026_02_23_204151_create_report_card_acknowledgements_table.php index 293735a7..6660f0f4 100644 --- a/database/migrations/2026_02_23_204151_create_report_card_acknowledgements_table.php +++ b/database/migrations/2026_02_23_204151_create_report_card_acknowledgements_table.php @@ -7,6 +7,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('report_card_acknowledgements')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `report_card_acknowledgements` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204160_create_class_progress_attachments_table.php b/database/migrations/2026_02_23_204160_create_class_progress_attachments_table.php index 76cc6967..8859fbda 100644 --- a/database/migrations/2026_02_23_204160_create_class_progress_attachments_table.php +++ b/database/migrations/2026_02_23_204160_create_class_progress_attachments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('class_progress_attachments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `class_progress_attachments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_204200_create_reimbursement_batch_items_table.php b/database/migrations/2026_02_23_204200_create_reimbursement_batch_items_table.php index e5f85ab7..c42c0357 100644 --- a/database/migrations/2026_02_23_204200_create_reimbursement_batch_items_table.php +++ b/database/migrations/2026_02_23_204200_create_reimbursement_batch_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('reimbursement_batch_items')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `reimbursement_batch_items` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900001_create_additional_charges_table.php b/database/migrations/2026_02_23_900001_create_additional_charges_table.php index e051c12c..04752129 100644 --- a/database/migrations/2026_02_23_900001_create_additional_charges_table.php +++ b/database/migrations/2026_02_23_900001_create_additional_charges_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('additional_charges')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `additional_charges` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900002_create_admin_notification_subjects_table.php b/database/migrations/2026_02_23_900002_create_admin_notification_subjects_table.php index ab84f922..b5f10fef 100644 --- a/database/migrations/2026_02_23_900002_create_admin_notification_subjects_table.php +++ b/database/migrations/2026_02_23_900002_create_admin_notification_subjects_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('admin_notification_subjects')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `admin_notification_subjects` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900003_create_attendance_comment_template_table.php b/database/migrations/2026_02_23_900003_create_attendance_comment_template_table.php index 1401ce7a..d5d638cd 100644 --- a/database/migrations/2026_02_23_900003_create_attendance_comment_template_table.php +++ b/database/migrations/2026_02_23_900003_create_attendance_comment_template_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('attendance_comment_template')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `attendance_comment_template` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900004_create_attendance_data_table.php b/database/migrations/2026_02_23_900004_create_attendance_data_table.php index 54e2748a..e31809d3 100644 --- a/database/migrations/2026_02_23_900004_create_attendance_data_table.php +++ b/database/migrations/2026_02_23_900004_create_attendance_data_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('attendance_data')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `attendance_data` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900005_create_attendance_day_table.php b/database/migrations/2026_02_23_900005_create_attendance_day_table.php index a5ca690b..15b9a08a 100644 --- a/database/migrations/2026_02_23_900005_create_attendance_day_table.php +++ b/database/migrations/2026_02_23_900005_create_attendance_day_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('attendance_day')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `attendance_day` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900006_create_attendance_record_table.php b/database/migrations/2026_02_23_900006_create_attendance_record_table.php index 1af8354a..810fd4bf 100644 --- a/database/migrations/2026_02_23_900006_create_attendance_record_table.php +++ b/database/migrations/2026_02_23_900006_create_attendance_record_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('attendance_record')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `attendance_record` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900007_create_attendance_tracking_table.php b/database/migrations/2026_02_23_900007_create_attendance_tracking_table.php index efdb1f0f..0ebb1537 100644 --- a/database/migrations/2026_02_23_900007_create_attendance_tracking_table.php +++ b/database/migrations/2026_02_23_900007_create_attendance_tracking_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('attendance_tracking')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `attendance_tracking` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900008_create_authorized_users_table.php b/database/migrations/2026_02_23_900008_create_authorized_users_table.php index 10a7403b..5baa0d5c 100644 --- a/database/migrations/2026_02_23_900008_create_authorized_users_table.php +++ b/database/migrations/2026_02_23_900008_create_authorized_users_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('authorized_users')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `authorized_users` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900009_create_badge_print_logs_table.php b/database/migrations/2026_02_23_900009_create_badge_print_logs_table.php index 2c2fc113..abd6a9ef 100644 --- a/database/migrations/2026_02_23_900009_create_badge_print_logs_table.php +++ b/database/migrations/2026_02_23_900009_create_badge_print_logs_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('badge_print_logs')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `badge_print_logs` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900010_create_calendar_events_table.php b/database/migrations/2026_02_23_900010_create_calendar_events_table.php index a1fec875..f4768259 100644 --- a/database/migrations/2026_02_23_900010_create_calendar_events_table.php +++ b/database/migrations/2026_02_23_900010_create_calendar_events_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('calendar_events')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `calendar_events` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900011_create_chapters_table.php b/database/migrations/2026_02_23_900011_create_chapters_table.php index 413b724e..c944522f 100644 --- a/database/migrations/2026_02_23_900011_create_chapters_table.php +++ b/database/migrations/2026_02_23_900011_create_chapters_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('chapters')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `chapters` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900012_create_classSection_table.php b/database/migrations/2026_02_23_900012_create_classSection_table.php index a70247b0..f3767c68 100644 --- a/database/migrations/2026_02_23_900012_create_classSection_table.php +++ b/database/migrations/2026_02_23_900012_create_classSection_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('classSection')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `classSection` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900013_create_class_prep_adjustments_table.php b/database/migrations/2026_02_23_900013_create_class_prep_adjustments_table.php index e3845a25..b1c21219 100644 --- a/database/migrations/2026_02_23_900013_create_class_prep_adjustments_table.php +++ b/database/migrations/2026_02_23_900013_create_class_prep_adjustments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('class_prep_adjustments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `class_prep_adjustments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900014_create_class_preparation_log_table.php b/database/migrations/2026_02_23_900014_create_class_preparation_log_table.php index 9b0729f1..2f7cf398 100644 --- a/database/migrations/2026_02_23_900014_create_class_preparation_log_table.php +++ b/database/migrations/2026_02_23_900014_create_class_preparation_log_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('class_preparation_log')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `class_preparation_log` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900017_create_classes_table.php b/database/migrations/2026_02_23_900017_create_classes_table.php index 6c1069ec..535adb04 100644 --- a/database/migrations/2026_02_23_900017_create_classes_table.php +++ b/database/migrations/2026_02_23_900017_create_classes_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('classes')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `classes` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900018_create_competition_class_winners_table.php b/database/migrations/2026_02_23_900018_create_competition_class_winners_table.php index 44ab7a4f..f79f8d80 100644 --- a/database/migrations/2026_02_23_900018_create_competition_class_winners_table.php +++ b/database/migrations/2026_02_23_900018_create_competition_class_winners_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('competition_class_winners')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `competition_class_winners` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900019_create_competition_scores_table.php b/database/migrations/2026_02_23_900019_create_competition_scores_table.php index c7cece93..6d64fe9b 100644 --- a/database/migrations/2026_02_23_900019_create_competition_scores_table.php +++ b/database/migrations/2026_02_23_900019_create_competition_scores_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('competition_scores')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `competition_scores` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900020_create_competition_winners_table.php b/database/migrations/2026_02_23_900020_create_competition_winners_table.php index d995d59a..8a5f3beb 100644 --- a/database/migrations/2026_02_23_900020_create_competition_winners_table.php +++ b/database/migrations/2026_02_23_900020_create_competition_winners_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('competition_winners')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `competition_winners` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900021_create_competitions_table.php b/database/migrations/2026_02_23_900021_create_competitions_table.php index b92a634d..74c6a595 100644 --- a/database/migrations/2026_02_23_900021_create_competitions_table.php +++ b/database/migrations/2026_02_23_900021_create_competitions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('competitions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `competitions` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900022_create_configuration_table.php b/database/migrations/2026_02_23_900022_create_configuration_table.php index c812d0f1..841df351 100644 --- a/database/migrations/2026_02_23_900022_create_configuration_table.php +++ b/database/migrations/2026_02_23_900022_create_configuration_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('configuration')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `configuration` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900023_create_contactus_table.php b/database/migrations/2026_02_23_900023_create_contactus_table.php index d409a261..82714bc9 100644 --- a/database/migrations/2026_02_23_900023_create_contactus_table.php +++ b/database/migrations/2026_02_23_900023_create_contactus_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('contactus')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `contactus` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900024_create_current_flag_table.php b/database/migrations/2026_02_23_900024_create_current_flag_table.php index c99961dc..4ff21969 100644 --- a/database/migrations/2026_02_23_900024_create_current_flag_table.php +++ b/database/migrations/2026_02_23_900024_create_current_flag_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('current_flag')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `current_flag` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900025_create_discount_usages_table.php b/database/migrations/2026_02_23_900025_create_discount_usages_table.php index a76f37bf..f205e993 100644 --- a/database/migrations/2026_02_23_900025_create_discount_usages_table.php +++ b/database/migrations/2026_02_23_900025_create_discount_usages_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('discount_usages')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `discount_usages` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900026_create_discount_vouchers_table.php b/database/migrations/2026_02_23_900026_create_discount_vouchers_table.php index 85be5b45..046a1905 100644 --- a/database/migrations/2026_02_23_900026_create_discount_vouchers_table.php +++ b/database/migrations/2026_02_23_900026_create_discount_vouchers_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('discount_vouchers')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `discount_vouchers` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900027_create_early_dismissal_signatures_table.php b/database/migrations/2026_02_23_900027_create_early_dismissal_signatures_table.php index 706a89a3..5f590f18 100644 --- a/database/migrations/2026_02_23_900027_create_early_dismissal_signatures_table.php +++ b/database/migrations/2026_02_23_900027_create_early_dismissal_signatures_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('early_dismissal_signatures')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `early_dismissal_signatures` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900028_create_email_templates_table.php b/database/migrations/2026_02_23_900028_create_email_templates_table.php index a2dbbe08..9c02c7d5 100644 --- a/database/migrations/2026_02_23_900028_create_email_templates_table.php +++ b/database/migrations/2026_02_23_900028_create_email_templates_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('email_templates')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `email_templates` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900029_create_emergency_contacts_table.php b/database/migrations/2026_02_23_900029_create_emergency_contacts_table.php index 449c8754..08d5c262 100644 --- a/database/migrations/2026_02_23_900029_create_emergency_contacts_table.php +++ b/database/migrations/2026_02_23_900029_create_emergency_contacts_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('emergency_contacts')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `emergency_contacts` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900030_create_enrollments_table.php b/database/migrations/2026_02_23_900030_create_enrollments_table.php index 457ac14e..06b39b99 100644 --- a/database/migrations/2026_02_23_900030_create_enrollments_table.php +++ b/database/migrations/2026_02_23_900030_create_enrollments_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('enrollments')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `enrollments` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900031_create_event_charges_table.php b/database/migrations/2026_02_23_900031_create_event_charges_table.php index 4d90cf2f..3af9ec10 100644 --- a/database/migrations/2026_02_23_900031_create_event_charges_table.php +++ b/database/migrations/2026_02_23_900031_create_event_charges_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('event_charges')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `event_charges` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900032_create_events_table.php b/database/migrations/2026_02_23_900032_create_events_table.php index b57b6cc8..d8927016 100644 --- a/database/migrations/2026_02_23_900032_create_events_table.php +++ b/database/migrations/2026_02_23_900032_create_events_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('events')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `events` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900033_create_exam_drafts_table.php b/database/migrations/2026_02_23_900033_create_exam_drafts_table.php index d5909aed..2ed71c79 100644 --- a/database/migrations/2026_02_23_900033_create_exam_drafts_table.php +++ b/database/migrations/2026_02_23_900033_create_exam_drafts_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('exam_drafts')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `exam_drafts` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900034_create_examquestiondistribution_table.php b/database/migrations/2026_02_23_900034_create_examquestiondistribution_table.php index b1daa6e2..1f305727 100644 --- a/database/migrations/2026_02_23_900034_create_examquestiondistribution_table.php +++ b/database/migrations/2026_02_23_900034_create_examquestiondistribution_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('examquestiondistribution')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `examquestiondistribution` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900035_create_examquestions_table.php b/database/migrations/2026_02_23_900035_create_examquestions_table.php index 02f5dbd2..d0f9a480 100644 --- a/database/migrations/2026_02_23_900035_create_examquestions_table.php +++ b/database/migrations/2026_02_23_900035_create_examquestions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('examquestions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `examquestions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900036_create_exams_table.php b/database/migrations/2026_02_23_900036_create_exams_table.php index 2b593567..24d1d598 100644 --- a/database/migrations/2026_02_23_900036_create_exams_table.php +++ b/database/migrations/2026_02_23_900036_create_exams_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('exams')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `exams` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900038_create_families_table.php b/database/migrations/2026_02_23_900038_create_families_table.php index 8334326f..da256006 100644 --- a/database/migrations/2026_02_23_900038_create_families_table.php +++ b/database/migrations/2026_02_23_900038_create_families_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('families')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `families` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900039_create_family_comm_prefs_table.php b/database/migrations/2026_02_23_900039_create_family_comm_prefs_table.php index 2767b891..3e4a3d6e 100644 --- a/database/migrations/2026_02_23_900039_create_family_comm_prefs_table.php +++ b/database/migrations/2026_02_23_900039_create_family_comm_prefs_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('family_comm_prefs')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `family_comm_prefs` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900040_create_family_guardians_table.php b/database/migrations/2026_02_23_900040_create_family_guardians_table.php index b5da65d9..d5420daf 100644 --- a/database/migrations/2026_02_23_900040_create_family_guardians_table.php +++ b/database/migrations/2026_02_23_900040_create_family_guardians_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('family_guardians')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `family_guardians` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900041_create_family_students_table.php b/database/migrations/2026_02_23_900041_create_family_students_table.php index 9e1316ed..c885c6e7 100644 --- a/database/migrations/2026_02_23_900041_create_family_students_table.php +++ b/database/migrations/2026_02_23_900041_create_family_students_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('family_students')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `family_students` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900042_create_final_exam_table.php b/database/migrations/2026_02_23_900042_create_final_exam_table.php index a1440120..9d62308a 100644 --- a/database/migrations/2026_02_23_900042_create_final_exam_table.php +++ b/database/migrations/2026_02_23_900042_create_final_exam_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('final_exam')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `final_exam` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900043_create_final_score_table.php b/database/migrations/2026_02_23_900043_create_final_score_table.php index 323a955f..517bb6ce 100644 --- a/database/migrations/2026_02_23_900043_create_final_score_table.php +++ b/database/migrations/2026_02_23_900043_create_final_score_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('final_score')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `final_score` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900044_create_longanswerquestions_table.php b/database/migrations/2026_02_23_900044_create_longanswerquestions_table.php index f26e246c..b0747618 100644 --- a/database/migrations/2026_02_23_900044_create_longanswerquestions_table.php +++ b/database/migrations/2026_02_23_900044_create_longanswerquestions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('longanswerquestions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `longanswerquestions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900046_create_notification_recipients_table.php b/database/migrations/2026_02_23_900046_create_notification_recipients_table.php index 42ad9103..6fbdc8e9 100644 --- a/database/migrations/2026_02_23_900046_create_notification_recipients_table.php +++ b/database/migrations/2026_02_23_900046_create_notification_recipients_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('notification_recipients')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `notification_recipients` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900047_create_preferences_table.php b/database/migrations/2026_02_23_900047_create_preferences_table.php index 89221b17..8c0e8001 100644 --- a/database/migrations/2026_02_23_900047_create_preferences_table.php +++ b/database/migrations/2026_02_23_900047_create_preferences_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('preferences')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `preferences` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900048_create_qcmquestions_table.php b/database/migrations/2026_02_23_900048_create_qcmquestions_table.php index ba1e29de..ad1996ea 100644 --- a/database/migrations/2026_02_23_900048_create_qcmquestions_table.php +++ b/database/migrations/2026_02_23_900048_create_qcmquestions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('qcmquestions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `qcmquestions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900049_create_questions_table.php b/database/migrations/2026_02_23_900049_create_questions_table.php index 30d83fe2..37bc6e83 100644 --- a/database/migrations/2026_02_23_900049_create_questions_table.php +++ b/database/migrations/2026_02_23_900049_create_questions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('questions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `questions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900050_create_questionstruefalse_table.php b/database/migrations/2026_02_23_900050_create_questionstruefalse_table.php index a9887a39..3db1d088 100644 --- a/database/migrations/2026_02_23_900050_create_questionstruefalse_table.php +++ b/database/migrations/2026_02_23_900050_create_questionstruefalse_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('questionstruefalse')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `questionstruefalse` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900051_create_questiontypes_table.php b/database/migrations/2026_02_23_900051_create_questiontypes_table.php index 27adce36..49d517fb 100644 --- a/database/migrations/2026_02_23_900051_create_questiontypes_table.php +++ b/database/migrations/2026_02_23_900051_create_questiontypes_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('questiontypes')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `questiontypes` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900052_create_scan_log_table.php b/database/migrations/2026_02_23_900052_create_scan_log_table.php index 81add41f..74b74e86 100644 --- a/database/migrations/2026_02_23_900052_create_scan_log_table.php +++ b/database/migrations/2026_02_23_900052_create_scan_log_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('scan_log')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `scan_log` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900053_create_shortanswerquestions_table.php b/database/migrations/2026_02_23_900053_create_shortanswerquestions_table.php index 4f960550..7d56ca28 100644 --- a/database/migrations/2026_02_23_900053_create_shortanswerquestions_table.php +++ b/database/migrations/2026_02_23_900053_create_shortanswerquestions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('shortanswerquestions')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `shortanswerquestions` ( `id` int NOT NULL, diff --git a/database/migrations/2026_02_23_900054_create_support_requests_table.php b/database/migrations/2026_02_23_900054_create_support_requests_table.php index d7c0a65e..650da0d0 100644 --- a/database/migrations/2026_02_23_900054_create_support_requests_table.php +++ b/database/migrations/2026_02_23_900054_create_support_requests_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('support_requests')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `support_requests` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900055_create_teacher_attendance_data_table.php b/database/migrations/2026_02_23_900055_create_teacher_attendance_data_table.php index 21334af6..a75883a4 100644 --- a/database/migrations/2026_02_23_900055_create_teacher_attendance_data_table.php +++ b/database/migrations/2026_02_23_900055_create_teacher_attendance_data_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('teacher_attendance_data')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `teacher_attendance_data` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_02_23_900056_create_communication_logs_table.php b/database/migrations/2026_02_23_900056_create_communication_logs_table.php index d4083ca4..5aa429b7 100644 --- a/database/migrations/2026_02_23_900056_create_communication_logs_table.php +++ b/database/migrations/2026_02_23_900056_create_communication_logs_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('communication_logs')) { + return; + } + App\Support\SqliteCompat::statement(<<<'SQL' CREATE TABLE `communication_logs` ( `id` int UNSIGNED NOT NULL, diff --git a/database/migrations/2026_03_09_050312_create_personal_access_tokens_table.php b/database/migrations/2026_03_09_050312_create_personal_access_tokens_table.php index 40ff706e..6ac41a5e 100644 --- a/database/migrations/2026_03_09_050312_create_personal_access_tokens_table.php +++ b/database/migrations/2026_03_09_050312_create_personal_access_tokens_table.php @@ -11,6 +11,10 @@ return new class extends Migration */ public function up(): void { + if (Schema::hasTable('personal_access_tokens')) { + return; + } + Schema::create('personal_access_tokens', function (Blueprint $table) { $table->id(); $table->morphs('tokenable'); diff --git a/database/migrations/2026_03_09_064908_create_sessions_table.php b/database/migrations/2026_03_09_064908_create_sessions_table.php index f60625bb..9210f600 100644 --- a/database/migrations/2026_03_09_064908_create_sessions_table.php +++ b/database/migrations/2026_03_09_064908_create_sessions_table.php @@ -11,6 +11,10 @@ return new class extends Migration */ public function up(): void { + if (Schema::hasTable('sessions')) { + return; + } + Schema::create('sessions', function (Blueprint $table) { $table->string('id')->primary(); $table->foreignId('user_id')->nullable()->index(); diff --git a/database/migrations/2026_03_09_070000_create_suppliers_table.php b/database/migrations/2026_03_09_070000_create_suppliers_table.php index 34990c06..187de84e 100644 --- a/database/migrations/2026_03_09_070000_create_suppliers_table.php +++ b/database/migrations/2026_03_09_070000_create_suppliers_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('suppliers')) { + return; + } + Schema::create('suppliers', function (Blueprint $table) { $table->id(); $table->string('name'); diff --git a/database/migrations/2026_03_09_070010_create_supplies_table.php b/database/migrations/2026_03_09_070010_create_supplies_table.php index 01741b29..96e409ef 100644 --- a/database/migrations/2026_03_09_070010_create_supplies_table.php +++ b/database/migrations/2026_03_09_070010_create_supplies_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('supplies')) { + return; + } + Schema::create('supplies', function (Blueprint $table) { $table->id(); $table->string('name'); diff --git a/database/migrations/2026_03_09_070020_create_supply_transactions_table.php b/database/migrations/2026_03_09_070020_create_supply_transactions_table.php index e292f37e..661c0fd4 100644 --- a/database/migrations/2026_03_09_070020_create_supply_transactions_table.php +++ b/database/migrations/2026_03_09_070020_create_supply_transactions_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('supply_transactions')) { + return; + } + Schema::create('supply_transactions', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('supply_id'); diff --git a/database/migrations/2026_03_09_070030_create_purchase_orders_table.php b/database/migrations/2026_03_09_070030_create_purchase_orders_table.php index a676498d..ea9d53c6 100644 --- a/database/migrations/2026_03_09_070030_create_purchase_orders_table.php +++ b/database/migrations/2026_03_09_070030_create_purchase_orders_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('purchase_orders')) { + return; + } + Schema::create('purchase_orders', function (Blueprint $table) { $table->id(); $table->string('po_number')->nullable(); diff --git a/database/migrations/2026_03_09_070040_create_purchase_order_items_table.php b/database/migrations/2026_03_09_070040_create_purchase_order_items_table.php index a589cfbe..24328653 100644 --- a/database/migrations/2026_03_09_070040_create_purchase_order_items_table.php +++ b/database/migrations/2026_03_09_070040_create_purchase_order_items_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('purchase_order_items')) { + return; + } + Schema::create('purchase_order_items', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('purchase_order_id'); diff --git a/database/migrations/2026_03_09_070050_create_supply_categories_table.php b/database/migrations/2026_03_09_070050_create_supply_categories_table.php index aa8d44cd..4a68e8dd 100644 --- a/database/migrations/2026_03_09_070050_create_supply_categories_table.php +++ b/database/migrations/2026_03_09_070050_create_supply_categories_table.php @@ -8,6 +8,10 @@ return new class extends Migration { public function up(): void { + if (Schema::hasTable('supply_categories')) { + return; + } + Schema::create('supply_categories', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); diff --git a/database/migrations/2026_03_11_045324_create_cache_table.php b/database/migrations/2026_03_11_045324_create_cache_table.php index ed758bdf..5799efc1 100644 --- a/database/migrations/2026_03_11_045324_create_cache_table.php +++ b/database/migrations/2026_03_11_045324_create_cache_table.php @@ -11,6 +11,10 @@ return new class extends Migration */ public function up(): void { + if (Schema::hasTable('cache')) { + return; + } + Schema::create('cache', function (Blueprint $table) { $table->string('key')->primary(); $table->mediumText('value'); diff --git a/database/migrations/2026_03_11_205736_create_students_table.php b/database/migrations/2026_03_11_205736_create_students_table.php new file mode 100644 index 00000000..ad7729a6 --- /dev/null +++ b/database/migrations/2026_03_11_205736_create_students_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('students'); + } +}; diff --git a/database/migrations/2026_03_11_205736_create_teachers_table.php b/database/migrations/2026_03_11_205736_create_teachers_table.php new file mode 100644 index 00000000..c59fef41 --- /dev/null +++ b/database/migrations/2026_03_11_205736_create_teachers_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('teachers'); + } +}; diff --git a/database/migrations/2026_03_11_205737_create_attendance_data_table.php b/database/migrations/2026_03_11_205737_create_attendance_data_table.php new file mode 100644 index 00000000..cc540779 --- /dev/null +++ b/database/migrations/2026_03_11_205737_create_attendance_data_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attendance_data'); + } +}; diff --git a/public/html/privacy_policy.html b/public/html/privacy_policy.html new file mode 100644 index 00000000..ff29a22b --- /dev/null +++ b/public/html/privacy_policy.html @@ -0,0 +1 @@ +

Privacy

\ No newline at end of file diff --git a/public/html/terms_of_service.html b/public/html/terms_of_service.html new file mode 100644 index 00000000..e8035da8 --- /dev/null +++ b/public/html/terms_of_service.html @@ -0,0 +1 @@ +

Terms

\ No newline at end of file diff --git a/resources/policies/picture_policy.html b/resources/policies/picture_policy.html new file mode 100644 index 00000000..60906f78 --- /dev/null +++ b/resources/policies/picture_policy.html @@ -0,0 +1,2 @@ +

Picture Policy

+

Picture policy content is provided by the administration. Please review the latest version for updates.

diff --git a/resources/policies/school_policy.html b/resources/policies/school_policy.html new file mode 100644 index 00000000..abb6c8e6 --- /dev/null +++ b/resources/policies/school_policy.html @@ -0,0 +1,2 @@ +

School Policy

+

School policy content is provided by the administration. Please review the latest version for updates.

diff --git a/routes/api.php b/routes/api.php index f3cb36a6..d878b1bf 100755 --- a/routes/api.php +++ b/routes/api.php @@ -24,7 +24,7 @@ use App\Http\Controllers\Api\Parents\ParentAttendanceReportController as ParentA use App\Http\Controllers\Api\AttendanceTracking\AttendanceTrackingController; use App\Http\Controllers\Api\Email\BroadcastEmailController; use App\Http\Controllers\Api\Settings\ConfigurationController; -use App\Http\Controllers\Api\Staff\ContactController; +use App\Http\Controllers\Api\Staff\StaffController; use App\Http\Controllers\Api\ClassPreparation\ClassPreparationController; use App\Http\Controllers\Api\ClassPrep\ClassPrepController; use App\Http\Controllers\Api\Email\EmailController; @@ -36,6 +36,7 @@ use App\Http\Controllers\Api\Expenses\ExpenseController; use App\Http\Controllers\Api\ExtraCharges\ExtraChargesController; use App\Http\Controllers\Api\Parents\EmergencyContactController; use App\Http\Controllers\Api\Settings\EventController; +use App\Http\Controllers\Api\Settings\PolicyController; use App\Http\Controllers\Api\Family\FamilyAdminController; use App\Http\Controllers\Api\Family\FamilyController; use App\Http\Controllers\Api\Reports\FilesController; @@ -44,12 +45,16 @@ use App\Http\Controllers\Api\Reports\StickersController; use App\Http\Controllers\Api\Scores\FinalController; use App\Http\Controllers\Api\Finance\FinancialController; use App\Http\Controllers\Api\System\FlagController; +use App\Http\Controllers\Api\System\DashboardController; +use App\Http\Controllers\Api\System\DatabaseHealthController; +use App\Http\Controllers\Api\System\NavBuilderController; use App\Http\Controllers\Api\Scores\GradingController; use App\Http\Controllers\Api\System\HealthController; use App\Http\Controllers\Api\System\SchoolIdController; use App\Http\Controllers\Api\System\SemesterRangeController; use App\Http\Controllers\Api\Scores\HomeworkController; use App\Http\Controllers\Api\Frontend\InfoIconController; +use App\Http\Controllers\Api\Frontend\FrontendController; use App\Http\Controllers\Api\Inventory\InventoryController; use App\Http\Controllers\Api\Inventory\InventoryCategoryController; use App\Http\Controllers\Api\Inventory\InventoryMovementController; @@ -58,15 +63,8 @@ use App\Http\Controllers\Api\Frontend\LandingPageController; use App\Http\Controllers\Api\Attendance\LateSlipLogsController; use App\Http\Controllers\Api\Messaging\MessagesController; use App\Http\Controllers\Api\Incidents\IncidentController as IncidentApiController; -use App\Http\Controllers\Api\Scores\MidtermController; -use App\Http\Controllers\Api\System\NavBuilderController; use App\Http\Controllers\Api\Notifications\NotificationManagementController; use App\Http\Controllers\Api\Frontend\PageController; -use App\Http\Controllers\Api\Scores\ParticipationController; -use App\Http\Controllers\Api\Settings\PolicyController; -use App\Http\Controllers\Api\Reports\PrintablesReportsController; -use App\Http\Controllers\Api\Scores\ProjectController; -use App\Http\Controllers\Api\Scores\QuizController; use App\Http\Controllers\Api\Finance\PaymentController; use App\Http\Controllers\Api\Finance\RefundController; use App\Http\Controllers\Api\Finance\PaymentNotificationController; @@ -82,17 +80,15 @@ use App\Http\Controllers\Api\Auth\RegisterController; use App\Http\Controllers\Api\RolePermissionController; use App\Http\Controllers\Api\RoleSwitcherController; use App\Http\Controllers\Api\Settings\SchoolCalendarController; -use App\Http\Controllers\Api\Scores\ScoreCommentController; -use App\Http\Controllers\Api\Scores\ScoreController; use App\Http\Controllers\Api\Scores\ScorePredictorController; -use App\Http\Controllers\Api\Auth\SessionTimeoutController; use App\Http\Controllers\Api\Reports\SlipPrinterController; -use App\Http\Controllers\Api\Staff\StaffController; use App\Http\Controllers\Api\Inventory\SupplierController; use App\Http\Controllers\Api\Inventory\SupplyCategoryController; use App\Http\Controllers\Api\Staff\TeacherController; use App\Http\Controllers\Api\StatsController; -use App\Http\Controllers\Api\Frontend\UiController; +use App\Http\Controllers\Api\Ui\UiController; +use App\Http\Controllers\Api\Support\ContactController as SupportContactController; +use App\Http\Controllers\Api\Support\SupportController as ApiSupportController; use App\Http\Controllers\Api\Scores\ProjectController as ProjectScoreController; use App\Http\Controllers\Api\Scores\MidtermController as MidtermScoreController; use App\Http\Controllers\Api\Scores\FinalController as FinalScoreController; @@ -108,6 +104,10 @@ use App\Http\Controllers\Api\Subjects\SubjectCurriculumController as SubjectCurr use App\Http\Controllers\Api\Exams\ExamDraftController as ExamDraftApiController; use App\Http\Controllers\Api\Settings\ConfigurationAdminController; use App\Http\Controllers\Api\Notifications\NotificationController as ApiNotificationController; +use App\Http\Controllers\Api\Utilities\PhoneFormatterController; +use App\Http\Controllers\Api\Settings\PreferencesController; +use App\Http\Controllers\Api\Settings\SettingsController; +use App\Http\Controllers\Api\Classes\ClassController as ClassSectionController; use App\Http\Controllers\Api\Attendance\AdminAttendanceApiController; use App\Http\Controllers\Api\Attendance\AttendanceCommentTemplateController; @@ -124,6 +124,64 @@ Route::prefix('v1')->group(function () { Route::get('me', [AuthController::class, 'me'])->middleware('auth.multi'); }); + Route::prefix('policies')->group(function () { + Route::get('school', [PolicyController::class, 'school']); + Route::get('picture', [PolicyController::class, 'picture']); + }); + + Route::prefix('pages')->group(function () { + Route::get('privacy', [PageController::class, 'privacyPolicy']); + Route::get('terms', [PageController::class, 'termsOfService']); + Route::get('help', [PageController::class, 'helpCenter']); + Route::post('contact', [PageController::class, 'submitContact']); + }); + + Route::post('contact', [SupportContactController::class, 'send']); + + Route::prefix('frontend')->group(function () { + Route::get('/', [FrontendController::class, 'index']); + Route::get('facility', [FrontendController::class, 'facility']); + Route::get('team', [FrontendController::class, 'team']); + Route::get('call-to-action', [FrontendController::class, 'callToAction']); + Route::get('testimonial', [FrontendController::class, 'testimonial']); + Route::get('not-found', [FrontendController::class, 'notFound']); + Route::get('me', [FrontendController::class, 'fetchUser'])->middleware('auth.multi'); + }); + + Route::get('health', [HealthController::class, 'index']); + Route::get('system/db-check', [DatabaseHealthController::class, 'index']); + + Route::middleware('auth.multi')->prefix('preferences')->group(function () { + Route::get('/', [PreferencesController::class, 'show']); + Route::post('/', [PreferencesController::class, 'store']); + Route::get('list', [PreferencesController::class, 'index']); + Route::get('{userId}', [PreferencesController::class, 'showForUser']); + Route::put('{userId}', [PreferencesController::class, 'updateForUser']); + Route::delete('{userId}', [PreferencesController::class, 'destroy']); + }); + + Route::middleware('auth.multi')->prefix('nav-builder')->group(function () { + Route::get('menu', [NavBuilderController::class, 'menu']); + Route::get('data', [NavBuilderController::class, 'data']); + Route::post('/', [NavBuilderController::class, 'store']); + Route::delete('{id}', [NavBuilderController::class, 'destroy']); + Route::post('reorder', [NavBuilderController::class, 'reorder']); + }); + + Route::middleware('auth.multi')->prefix('landing')->group(function () { + Route::get('/', [LandingPageController::class, 'index']); + Route::get('teacher', [LandingPageController::class, 'teacher']); + Route::get('parent', [LandingPageController::class, 'parent']); + Route::get('administrator', [LandingPageController::class, 'administrator']); + Route::get('admin', [LandingPageController::class, 'admin']); + Route::get('student', [LandingPageController::class, 'student']); + Route::get('guest', [LandingPageController::class, 'guest']); + }); + + Route::middleware('auth.multi')->prefix('info-icon')->group(function () { + Route::get('profile', [InfoIconController::class, 'profileIcon']); + }); + Route::middleware('auth.multi')->prefix('administrator')->group(function () { Route::get('absence', [AdministratorAbsenceController::class, 'index']); Route::post('absence', [AdministratorAbsenceController::class, 'store']); @@ -170,6 +228,12 @@ Route::prefix('v1')->group(function () { Route::post('/staff/admins/save', [StaffAttendanceApiController::class, 'saveAdmins']); Route::post('/staff/cell', [StaffAttendanceApiController::class, 'saveCell']); Route::get('/staff/month-csv', [StaffAttendanceApiController::class, 'monthCsv']); + + Route::prefix('late-slip-logs')->group(function () { + Route::get('/', [LateSlipLogsController::class, 'index']); + Route::get('{id}', [LateSlipLogsController::class, 'show']); + Route::delete('{id}', [LateSlipLogsController::class, 'destroy']); + }); }); Route::prefix('attendance-tracking')->group(function () { @@ -191,6 +255,66 @@ Route::prefix('v1')->group(function () { }); Route::middleware('auth.multi')->group(function () { + Route::get('dashboard/route', [DashboardController::class, 'route']); + + Route::prefix('utilities/phone')->group(function () { + Route::post('format', [PhoneFormatterController::class, 'format']); + }); + + Route::prefix('ui')->group(function () { + Route::post('style', [UiController::class, 'style']); + }); + + Route::prefix('support')->group(function () { + Route::get('/', [ApiSupportController::class, 'index']); + Route::post('/', [ApiSupportController::class, 'store']); + Route::get('teacher', [ApiSupportController::class, 'teacher']); + }); + + Route::prefix('staff')->group(function () { + Route::get('/', [StaffController::class, 'index']); + Route::get('{id}', [StaffController::class, 'show']); + Route::post('/', [StaffController::class, 'store']); + Route::patch('{id}', [StaffController::class, 'update']); + Route::delete('{id}', [StaffController::class, 'destroy']); + }); + + Route::prefix('messages')->group(function () { + Route::get('/', [MessagesController::class, 'index']); + Route::get('inbox', [MessagesController::class, 'inbox']); + Route::get('sent', [MessagesController::class, 'sent']); + Route::get('drafts', [MessagesController::class, 'drafts']); + Route::get('trash', [MessagesController::class, 'trash']); + Route::post('/', [MessagesController::class, 'store']); + Route::post('receive', [MessagesController::class, 'receive']); + Route::get('recipients/{type}', [MessagesController::class, 'recipients']); + Route::get('{id}', [MessagesController::class, 'show']); + Route::patch('{id}', [MessagesController::class, 'update']); + Route::delete('{id}', [MessagesController::class, 'destroy']); + }); + + Route::prefix('settings')->group(function () { + Route::get('/', [SettingsController::class, 'index']); + Route::patch('/', [SettingsController::class, 'update']); + }); + + Route::prefix('ip-bans')->group(function () { + Route::get('/', [IpBanController::class, 'index']); + Route::get('{id}', [IpBanController::class, 'show']); + Route::post('ban', [IpBanController::class, 'ban']); + Route::post('unban', [IpBanController::class, 'unban']); + }); + + Route::prefix('class-sections')->group(function () { + Route::get('/', [ClassSectionController::class, 'index']); + Route::post('/', [ClassSectionController::class, 'store']); + Route::get('{classSection}', [ClassSectionController::class, 'show']); + Route::put('{classSection}', [ClassSectionController::class, 'update']); + Route::delete('{classSection}', [ClassSectionController::class, 'destroy']); + Route::get('{classSectionId}/attendance', [ClassSectionController::class, 'attendance']); + Route::post('seed-defaults', [ClassSectionController::class, 'seedDefaults']); + }); + Route::get('stats', [StatsController::class, 'index']); Route::prefix('notifications')->group(function () { Route::get('/', [ApiNotificationController::class, 'index']); @@ -203,11 +327,6 @@ Route::prefix('v1')->group(function () { Route::post('{notificationId}/restore', [ApiNotificationController::class, 'restore']); Route::post('{notificationId}/read', [ApiNotificationController::class, 'markRead']); }); - Route::prefix('session')->group(function () { - Route::get('timeout-config', [SessionTimeoutController::class, 'config'])->name('session.timeout.config'); - Route::get('check-timeout', [SessionTimeoutController::class, 'check'])->name('session.timeout.check'); - Route::post('ping', [SessionTimeoutController::class, 'ping'])->name('session.timeout.ping'); - }); Route::prefix('role-permissions')->group(function () { Route::get('roles', [RolePermissionController::class, 'roles']); Route::post('roles', [RolePermissionController::class, 'storeRole']); @@ -681,6 +800,8 @@ Route::prefix('v1')->group(function () { Route::prefix('class-prep')->group(function () { Route::get('/', [ClassPreparationController::class, 'index']); Route::post('mark-printed', [ClassPreparationController::class, 'markPrinted']); + Route::post('adjustments', [ClassPreparationController::class, 'saveAdjustments']); + Route::post('print/{classSectionId}/{schoolYear}', [ClassPreparationController::class, 'print']); Route::get('sticker-counts', [ClassPrepController::class, 'stickerCounts']); Route::get('classes/{classSectionId}/students', [ClassPrepController::class, 'studentsByClass']); }); @@ -732,6 +853,8 @@ Route::middleware('auth.multi')->group(function () { Route::prefix('class-prep')->group(function () { Route::get('/', [ClassPreparationController::class, 'index']); Route::post('mark-printed', [ClassPreparationController::class, 'markPrinted']); + Route::post('adjustments', [ClassPreparationController::class, 'saveAdjustments']); + Route::post('print/{classSectionId}/{schoolYear}', [ClassPreparationController::class, 'print']); Route::get('sticker-counts', [ClassPrepController::class, 'stickerCounts']); Route::get('classes/{classSectionId}/students', [ClassPrepController::class, 'studentsByClass']); }); diff --git a/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php b/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php new file mode 100644 index 00000000..42b16cd4 --- /dev/null +++ b/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php @@ -0,0 +1,136 @@ +createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/attendance/late-slip-logs'); + + $response->assertForbidden(); + } + + public function test_index_returns_logs(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('late_slip_logs')->insert([ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student One', + 'slip_date' => '2025-09-01', + 'time_in' => '08:10:00', + 'grade' => '3', + 'reason' => 'Traffic', + 'admin_name' => 'Admin User', + ]); + + $response = $this->getJson('/api/v1/attendance/late-slip-logs?school_year=2025-2026'); + + $response->assertOk(); + $response->assertJsonPath('data.logs.0.student_name', 'Student One'); + $response->assertJsonStructure(['data' => ['meta']]); + } + + public function test_show_returns_log(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('late_slip_logs')->insert([ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student Two', + 'slip_date' => '2025-09-02', + 'time_in' => '08:15:00', + 'grade' => '2', + 'reason' => 'Bus delay', + 'admin_name' => 'Admin User', + ]); + + $response = $this->getJson('/api/v1/attendance/late-slip-logs/1'); + + $response->assertOk(); + $response->assertJsonPath('data.log.student_name', 'Student Two'); + } + + public function test_destroy_deletes_log(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('late_slip_logs')->insert([ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student Three', + 'slip_date' => '2025-09-03', + 'time_in' => '08:20:00', + 'grade' => '1', + 'reason' => 'Weather', + 'admin_name' => 'Admin User', + ]); + + $response = $this->deleteJson('/api/v1/attendance/late-slip-logs/1'); + + $response->assertOk(); + $this->assertDatabaseMissing('late_slip_logs', [ + 'id' => 1, + ]); + } + + public function test_index_validation_rejects_bad_dates(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->getJson('/api/v1/attendance/late-slip-logs?date_from=not-a-date'); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Auth/IpBanControllerTest.php b/tests/Feature/Api/V1/Auth/IpBanControllerTest.php new file mode 100644 index 00000000..d12badd0 --- /dev/null +++ b/tests/Feature/Api/V1/Auth/IpBanControllerTest.php @@ -0,0 +1,182 @@ +createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/ip-bans'); + + $response->assertForbidden(); + } + + public function test_index_returns_bans(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('ip_attempts')->insert([ + 'ip_address' => '10.0.0.1', + 'attempts' => 5, + 'blocked_until' => now('UTC')->addHours(2), + ]); + + $response = $this->getJson('/api/v1/ip-bans?status=active'); + + $response->assertOk(); + $response->assertJsonPath('data.bans.0.ip_address', '10.0.0.1'); + $response->assertJsonStructure(['data' => ['meta']]); + } + + public function test_show_returns_ban(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('ip_attempts')->insert([ + 'ip_address' => '10.0.0.2', + 'attempts' => 3, + 'blocked_until' => now('UTC')->addHours(1), + ]); + + $response = $this->getJson('/api/v1/ip-bans/1'); + + $response->assertOk(); + $response->assertJsonPath('data.ban.ip_address', '10.0.0.2'); + } + + public function test_ban_creates_block(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('ip_attempts')->insert([ + 'ip_address' => '10.0.0.3', + 'attempts' => 1, + ]); + + $response = $this->postJson('/api/v1/ip-bans/ban', [ + 'ip' => '10.0.0.3', + 'hours' => 12, + ]); + + $response->assertOk(); + $response->assertJsonPath('data.ban.ip_address', '10.0.0.3'); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.3', + ]); + } + + public function test_unban_single(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('ip_attempts')->insert([ + 'ip_address' => '10.0.0.4', + 'attempts' => 5, + 'blocked_until' => now('UTC')->addHours(2), + ]); + + $response = $this->postJson('/api/v1/ip-bans/unban', [ + 'ip' => '10.0.0.4', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.4', + 'blocked_until' => null, + 'attempts' => 0, + ]); + } + + public function test_unban_all(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('ip_attempts')->insert([ + [ + 'ip_address' => '10.0.0.5', + 'attempts' => 5, + 'blocked_until' => now('UTC')->addHours(2), + ], + [ + 'ip_address' => '10.0.0.6', + 'attempts' => 2, + 'blocked_until' => now('UTC')->addHours(2), + ], + ]); + + $response = $this->postJson('/api/v1/ip-bans/unban', [ + 'all' => true, + ]); + + $response->assertOk(); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.5', + 'blocked_until' => null, + ]); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.6', + 'blocked_until' => null, + ]); + } + + public function test_ban_validation_requires_ip_or_id(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->postJson('/api/v1/ip-bans/ban', [ + 'hours' => 12, + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Auth/RegisterControllerTest.php b/tests/Feature/Api/V1/Auth/RegisterControllerTest.php index 7d14bffc..f48727b8 100644 --- a/tests/Feature/Api/V1/Auth/RegisterControllerTest.php +++ b/tests/Feature/Api/V1/Auth/RegisterControllerTest.php @@ -39,6 +39,8 @@ class RegisterControllerTest extends TestCase 'is_active' => 1, ]); + $captcha = $this->getJson('/api/v1/auth/register/captcha')->json('captcha'); + $payload = [ 'firstname' => 'Parent', 'lastname' => 'User', @@ -52,13 +54,12 @@ class RegisterControllerTest extends TestCase 'state' => 'CT', 'zip' => '12345', 'accept_school_policy' => 1, - 'captcha' => 'ABCD', + 'captcha' => $captcha, 'is_parent' => 1, 'no_second_parent_info' => 1, ]; - $response = $this->withSession(['captcha_answer' => 'ABCD']) - ->postJson('/api/v1/auth/register', $payload); + $response = $this->postJson('/api/v1/auth/register', $payload); $response->assertCreated(); $response->assertJsonPath('ok', true); diff --git a/tests/Feature/Api/V1/Auth/SessionTimeoutControllerTest.php b/tests/Feature/Api/V1/Auth/SessionTimeoutControllerTest.php deleted file mode 100644 index 26f576c4..00000000 --- a/tests/Feature/Api/V1/Auth/SessionTimeoutControllerTest.php +++ /dev/null @@ -1,123 +0,0 @@ -seedUser(); - Sanctum::actingAs($user); - - $response = $this->getJson('/api/v1/session/timeout-config'); - - $response->assertOk(); - $response->assertJsonPath('status', true); - $this->assertArrayHasKey('timeout', $response->json('data.config')); - $this->assertArrayHasKey('keep_alive_url', $response->json('data.config')); - } - - public function test_check_timeout_returns_active_status(): void - { - $user = $this->seedUser(); - Sanctum::actingAs($user); - - $response = $this - ->withSession(['last_activity' => time() - 10]) - ->getJson('/api/v1/session/check-timeout'); - - $response->assertOk(); - $response->assertJsonPath('status', true); - $response->assertJsonPath('data.session.status', 'active'); - } - - public function test_check_timeout_returns_warning_status(): void - { - $user = $this->seedUser(); - Sanctum::actingAs($user); - - $response = $this - ->withSession(['last_activity' => time() - 1000]) - ->getJson('/api/v1/session/check-timeout'); - - $response->assertOk(); - $response->assertJsonPath('status', true); - $response->assertJsonPath('data.session.status', 'warning'); - } - - public function test_check_timeout_returns_expired_status(): void - { - $user = $this->seedUser(); - Sanctum::actingAs($user); - - $response = $this - ->withSession(['last_activity' => time() - 5000]) - ->getJson('/api/v1/session/check-timeout'); - - $response->assertStatus(401); - $response->assertJsonPath('status', false); - $response->assertJsonPath('errors.status', 'expired'); - } - - public function test_ping_updates_activity(): void - { - $user = $this->seedUser(); - Sanctum::actingAs($user); - - $response = $this - ->withSession(['last_activity' => time() - 10]) - ->postJson('/api/v1/session/ping'); - - $response->assertOk(); - $response->assertJsonPath('status', true); - $response->assertJsonPath('data.session.status', 'active'); - } - - public function test_ping_returns_expired_when_missing_activity(): void - { - $user = $this->seedUser(); - Sanctum::actingAs($user); - - $response = $this->postJson('/api/v1/session/ping'); - - $response->assertStatus(401); - $response->assertJsonPath('status', false); - } - - public function test_requires_authentication(): void - { - $response = $this->getJson('/api/v1/session/timeout-config'); - - $response->assertStatus(401); - } - - private function seedUser(): User - { - $userId = DB::table('users')->insertGetId([ - 'firstname' => 'Admin', - 'lastname' => 'User', - 'cellphone' => '9999999999', - 'email' => 'session@example.com', - 'address_street' => '123 Street', - 'city' => 'City', - 'state' => 'ST', - 'zip' => '12345', - 'accept_school_policy' => 1, - 'password' => bcrypt('password'), - 'user_type' => 'primary', - 'semester' => 'Fall', - 'school_year' => '2025-2026', - 'status' => 'Active', - ]); - - return User::query()->findOrFail($userId); - } -} diff --git a/tests/Feature/Api/V1/ClassPreparation/ClassPreparationControllerTest.php b/tests/Feature/Api/V1/ClassPreparation/ClassPreparationControllerTest.php index f7b639f4..d4b66594 100644 --- a/tests/Feature/Api/V1/ClassPreparation/ClassPreparationControllerTest.php +++ b/tests/Feature/Api/V1/ClassPreparation/ClassPreparationControllerTest.php @@ -23,11 +23,14 @@ class ClassPreparationControllerTest extends TestCase $response->assertOk(); $response->assertJson([ - 'ok' => true, - 'schoolYear' => '2025-2026', - 'semester' => 'Fall', + 'status' => true, + 'message' => 'Success', + 'data' => [ + 'schoolYear' => '2025-2026', + 'semester' => 'Fall', + ], ]); - $this->assertNotEmpty($response->json('results')); + $this->assertNotEmpty($response->json('data.results')); } public function test_mark_printed_creates_snapshots(): void @@ -40,12 +43,12 @@ class ClassPreparationControllerTest extends TestCase $response = $this->postJson('/api/v1/class-prep/mark-printed', [ 'school_year' => '2025-2026', 'semester' => 'Fall', - 'class_section_ids' => [101], + 'class_section_ids' => ['101'], ]); $response->assertOk(); $response->assertJson([ - 'ok' => true, + 'status' => true, ]); $this->assertDatabaseHas('class_preparation_log', [ @@ -54,6 +57,66 @@ class ClassPreparationControllerTest extends TestCase ]); } + public function test_mark_printed_rejects_invalid_payload(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/class-prep/mark-printed', [ + 'class_section_ids' => [], + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + public function test_save_adjustments_updates_rows(): void + { + $this->seedPrepData(); + + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/class-prep/adjustments', [ + 'class_section_id' => '101', + 'school_year' => '2025-2026', + 'adjustments' => [ + 'Small Table' => 2, + ], + ]); + + $response->assertOk(); + $response->assertJson([ + 'status' => true, + ]); + + $this->assertDatabaseHas('class_prep_adjustments', [ + 'class_section_id' => '101', + 'school_year' => '2025-2026', + 'item_name' => 'Small Table', + 'adjustment' => 2, + ]); + } + + public function test_print_logs_prep_items(): void + { + $this->seedPrepData(); + + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/class-prep/print/101/2025-2026'); + + $response->assertOk(); + $response->assertJsonPath('status', true); + $response->assertJsonPath('data.print.class_section_id', '101'); + + $this->assertDatabaseHas('class_preparation_log', [ + 'class_section_id' => 101, + 'school_year' => '2025-2026', + ]); + } + private function seedPrepData(): void { DB::table('configuration')->insert([ diff --git a/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php b/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php new file mode 100644 index 00000000..3deb5d96 --- /dev/null +++ b/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php @@ -0,0 +1,282 @@ +createUser(); + Sanctum::actingAs($user); + + DB::table('classes')->insert([ + 'class_name' => 'Class 1', + 'schedule' => 'Monday', + 'capacity' => 30, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $response = $this->getJson('/api/v1/class-sections'); + + $response->assertOk(); + $response->assertJsonPath('data.sections.0.class_section_name', '1-A'); + } + + public function test_store_requires_admin(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/class-sections', [ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $response->assertForbidden(); + } + + public function test_store_creates_class_section(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('classes')->insert([ + 'class_name' => 'Class 1', + 'schedule' => 'Monday', + 'capacity' => 30, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $response = $this->postJson('/api/v1/class-sections', [ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('classSection', [ + 'class_section_id' => 101, + 'class_section_name' => '1-A', + ]); + } + + public function test_update_requires_admin(): void + { + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->putJson('/api/v1/class-sections/1', [ + 'class_section_name' => '1-B', + ]); + + $response->assertForbidden(); + } + + public function test_show_returns_section(): void + { + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/class-sections/1'); + + $response->assertOk(); + $response->assertJsonPath('data.section.class_section_name', '1-A'); + } + + public function test_update_modifies_class_section(): void + { + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->putJson('/api/v1/class-sections/1', [ + 'class_section_name' => '1-B', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('classSection', [ + 'id' => 1, + 'class_section_name' => '1-B', + ]); + } + + public function test_destroy_deletes_class_section(): void + { + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->deleteJson('/api/v1/class-sections/1'); + + $response->assertOk(); + $this->assertDatabaseMissing('classSection', [ + 'id' => 1, + ]); + } + + public function test_attendance_returns_students(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('students')->insert([ + 'school_id' => 'S-1', + 'firstname' => 'Student', + 'lastname' => 'One', + 'age' => 8, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => 1, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'is_active' => 1, + ]); + + DB::table('student_class')->insert([ + 'student_id' => 1, + 'class_section_id' => 101, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $teacher = $this->createUser(); + Sanctum::actingAs($teacher); + + $response = $this->getJson('/api/v1/class-sections/101/attendance'); + + $response->assertOk(); + $response->assertJsonPath('data.attendance.class_section_id', 101); + $response->assertJsonCount(1, 'data.attendance.students'); + } + + public function test_seed_defaults_creates_classes(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->postJson('/api/v1/class-sections/seed-defaults'); + + $response->assertOk(); + $this->assertDatabaseHas('classes', [ + 'class_name' => 'Class 1', + ]); + } + + public function test_validation_rejects_invalid_payload(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->postJson('/api/v1/class-sections', [ + 'class_id' => 'bad', + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + private function createUser(string $roleName = 'teacher'): User + { + DB::table('roles')->insert([ + 'id' => 1, + 'name' => $roleName, + 'priority' => 1, + ]); + + DB::table('users')->insert([ + 'school_id' => 1, + 'firstname' => 'Test', + 'lastname' => 'User', + 'cellphone' => '5555555555', + 'email' => $roleName . '@example.com', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'is_verified' => 1, + 'status' => 'Active', + 'is_suspended' => 0, + 'failed_attempts' => 0, + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => 1, + 'role_id' => 1, + ]); + + return User::query()->findOrFail(1); + } +} diff --git a/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php b/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php new file mode 100644 index 00000000..fb68a7b2 --- /dev/null +++ b/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php @@ -0,0 +1,72 @@ +getJson('/api/v1/frontend'); + + $response->assertOk(); + $response->assertJsonPath('data.page.page', 'index'); + } + + public function test_fetch_user_requires_auth(): void + { + $response = $this->getJson('/api/v1/frontend/me'); + + $response->assertStatus(401); + } + + public function test_fetch_user_returns_profile(): void + { + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/frontend/me'); + + $response->assertOk(); + $response->assertJsonPath('data.user.firstname', 'Test'); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php b/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php new file mode 100644 index 00000000..ebd0a5e2 --- /dev/null +++ b/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php @@ -0,0 +1,40 @@ +create([ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'john@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/info-icon/profile'); + + $response->assertOk(); + $response->assertJsonPath('data.profile.user_initials', 'JD'); + } +} diff --git a/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php b/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php new file mode 100644 index 00000000..d60af1b5 --- /dev/null +++ b/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php @@ -0,0 +1,81 @@ +createUser('admin'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/landing'); + + $response->assertOk(); + $response->assertJsonPath('data.landing.role', 'admin'); + } + + public function test_teacher_dashboard_returns_summary(): void + { + $user = $this->createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/landing/teacher'); + + $response->assertOk(); + $response->assertJsonPath('data.landing.dashboard', 'teacher'); + $response->assertJsonStructure(['data' => ['landing' => ['summary']]]); + } + + public function test_parent_dashboard_returns_summary(): void + { + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/landing/parent'); + + $response->assertOk(); + $response->assertJsonPath('data.landing.dashboard', 'parent'); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'user_type' => 'primary', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Frontend/PageControllerTest.php b/tests/Feature/Api/V1/Frontend/PageControllerTest.php new file mode 100644 index 00000000..538d3cea --- /dev/null +++ b/tests/Feature/Api/V1/Frontend/PageControllerTest.php @@ -0,0 +1,59 @@ +Privacy'); + + $response = $this->getJson('/api/v1/pages/privacy'); + + $response->assertOk(); + $response->assertJsonPath('data.page.type', 'privacy_policy'); + $this->assertStringContainsString('Privacy', $response->json('data.page.content')); + } + + public function test_help_missing_returns_error(): void + { + $dir = public_path('html'); + File::ensureDirectoryExists($dir); + File::delete($dir . '/help_center.html'); + + $response = $this->getJson('/api/v1/pages/help'); + + $response->assertStatus(404); + } + + public function test_submit_contact_persists(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $payload = [ + 'email' => 'contact@example.com', + 'message' => 'Hello there, this is a test message.', + ]; + + $response = $this->postJson('/api/v1/pages/contact', $payload); + + $response->assertStatus(201); + $this->assertDatabaseHas('contactus', [ + 'subject' => 'Contact Us Form Submission', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php b/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php new file mode 100644 index 00000000..53c7065d --- /dev/null +++ b/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php @@ -0,0 +1,236 @@ +getJson('/api/v1/messages/inbox'); + + $response->assertStatus(401); + } + + public function test_store_creates_message(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $sender = $this->createUser('parent', 'sender@example.com'); + $recipient = $this->createUser('teacher', 'recipient@example.com'); + Sanctum::actingAs($sender); + + $payload = [ + 'recipient_id' => $recipient->id, + 'subject' => 'Hello', + 'message' => 'Test message', + ]; + + $response = $this->postJson('/api/v1/messages', $payload); + + $response->assertStatus(201); + $this->assertDatabaseHas('messages', [ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Hello', + ]); + } + + public function test_show_marks_read(): void + { + $sender = $this->createUser('parent', 'sender2@example.com'); + $recipient = $this->createUser('teacher', 'recipient2@example.com'); + + $message = Message::query()->create([ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Subject', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-123', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Sanctum::actingAs($recipient); + + $response = $this->getJson('/api/v1/messages/' . $message->id); + + $response->assertOk(); + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'read_status' => 1, + ]); + } + + public function test_update_changes_status(): void + { + $sender = $this->createUser('parent', 'sender3@example.com'); + $recipient = $this->createUser('teacher', 'recipient3@example.com'); + + $message = Message::query()->create([ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Subject', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-124', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Sanctum::actingAs($sender); + + $response = $this->patchJson('/api/v1/messages/' . $message->id, [ + 'status' => 'trashed', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'status' => 'trashed', + ]); + } + + public function test_destroy_marks_trashed(): void + { + $sender = $this->createUser('parent', 'sender4@example.com'); + $recipient = $this->createUser('teacher', 'recipient4@example.com'); + + $message = Message::query()->create([ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Subject', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-125', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Sanctum::actingAs($sender); + + $response = $this->deleteJson('/api/v1/messages/' . $message->id); + + $response->assertOk(); + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'status' => 'trashed', + ]); + } + + public function test_recipients_teacher_returns_list(): void + { + $teacher = $this->createUser('teacher', 'teacher@example.com'); + Sanctum::actingAs($teacher); + + DB::table('teacher_class')->insert([ + 'teacher_id' => $teacher->id, + 'class_section_id' => 1, + 'position' => 'main', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->getJson('/api/v1/messages/recipients/teacher'); + + $response->assertOk(); + $response->assertJsonPath('data.recipients.0.id', $teacher->id); + } + + public function test_validation_rejects_missing_subject(): void + { + $sender = $this->createUser('parent', 'sender5@example.com'); + $recipient = $this->createUser('teacher', 'recipient5@example.com'); + Sanctum::actingAs($sender); + + $response = $this->postJson('/api/v1/messages', [ + 'recipient_id' => $recipient->id, + 'message' => 'Body', + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + public function test_show_forbidden_for_other_users(): void + { + $sender = $this->createUser('parent', 'sender6@example.com'); + $recipient = $this->createUser('teacher', 'recipient6@example.com'); + $other = $this->createUser('student', 'other@example.com'); + + $message = Message::query()->create([ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Subject', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-126', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Sanctum::actingAs($other); + + $response = $this->getJson('/api/v1/messages/' . $message->id); + + $response->assertForbidden(); + } + + private function createUser(string $roleName, string $email): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $email, + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Settings/PolicyControllerTest.php b/tests/Feature/Api/V1/Settings/PolicyControllerTest.php new file mode 100644 index 00000000..2ca3c7fa --- /dev/null +++ b/tests/Feature/Api/V1/Settings/PolicyControllerTest.php @@ -0,0 +1,27 @@ +getJson('/api/v1/policies/school'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['policy' => ['type', 'title', 'format', 'content', 'updated_at']]]); + } + + public function test_picture_policy_endpoint_returns_payload(): void + { + $response = $this->getJson('/api/v1/policies/picture'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['policy' => ['type', 'title', 'format', 'content', 'updated_at']]]); + } +} diff --git a/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php b/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php new file mode 100644 index 00000000..7c51b8d2 --- /dev/null +++ b/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php @@ -0,0 +1,169 @@ +createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/preferences'); + + $response->assertOk(); + $response->assertJsonPath('data.preferences.receive_email_notifications', true); + $response->assertJsonStructure(['data' => ['options' => ['style_options', 'menu_options']]]); + } + + public function test_store_creates_preferences(): void + { + $user = $this->createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/preferences', [ + 'receive_email_notifications' => true, + 'receive_sms_notifications' => false, + 'theme' => 'dark', + 'language' => 'en', + 'style_color' => 'blue', + 'menu_color' => 'white', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('user_preferences', [ + 'user_id' => $user->id, + 'notification_email' => 1, + 'notification_sms' => 0, + ]); + } + + public function test_list_requires_admin(): void + { + $user = $this->createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/preferences/list'); + + $response->assertForbidden(); + } + + public function test_list_returns_rows_for_admin(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('user_preferences')->insert([ + 'user_id' => $admin->id, + 'notification_email' => 1, + 'notification_sms' => 1, + 'theme' => 'light', + 'language' => 'en', + ]); + + $response = $this->getJson('/api/v1/preferences/list'); + + $response->assertOk(); + $response->assertJsonPath('data.preferences.0.user_id', $admin->id); + } + + public function test_update_for_user_by_admin(): void + { + $admin = $this->createUser('admin'); + $user = $this->createUser('teacher', 2); + Sanctum::actingAs($admin); + + $response = $this->putJson('/api/v1/preferences/2', [ + 'receive_sms_notifications' => false, + ]); + + $response->assertOk(); + $this->assertDatabaseHas('user_preferences', [ + 'user_id' => $user->id, + 'notification_sms' => 0, + ]); + } + + public function test_destroy_requires_admin(): void + { + $user = $this->createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->deleteJson('/api/v1/preferences/' . $user->id); + + $response->assertForbidden(); + } + + public function test_destroy_deletes_preferences(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + DB::table('user_preferences')->insert([ + 'user_id' => $admin->id, + 'notification_email' => 1, + 'notification_sms' => 1, + ]); + + $response = $this->deleteJson('/api/v1/preferences/' . $admin->id); + + $response->assertOk(); + $this->assertDatabaseMissing('user_preferences', [ + 'user_id' => $admin->id, + ]); + } + + public function test_store_validates_payload(): void + { + $user = $this->createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/preferences', [ + 'theme' => 'invalid', + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + private function createUser(string $roleName, int $id = 1): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + DB::table('users')->insert([ + 'id' => $id, + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . $id . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $id, + 'role_id' => $roleId, + ]); + + return User::query()->findOrFail($id); + } +} diff --git a/tests/Feature/Api/V1/Settings/SettingsControllerTest.php b/tests/Feature/Api/V1/Settings/SettingsControllerTest.php new file mode 100644 index 00000000..fc49c78a --- /dev/null +++ b/tests/Feature/Api/V1/Settings/SettingsControllerTest.php @@ -0,0 +1,103 @@ +createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/settings'); + + $response->assertForbidden(); + } + + public function test_index_returns_settings(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->getJson('/api/v1/settings'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['settings' => ['site_name', 'administrator_email', 'timezone']]]); + } + + public function test_update_validation_rejects_bad_email(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->patchJson('/api/v1/settings', [ + 'administrator_email' => 'not-an-email', + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + public function test_update_persists_settings(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->patchJson('/api/v1/settings', [ + 'site_name' => 'Alrahma', + 'timezone' => 'UTC', + 'administrator_email' => 'admin@example.com', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('settings', [ + 'name' => 'Alrahma', + 'timezone' => 'UTC', + ]); + $this->assertDatabaseHas('configuration', [ + 'config_key' => 'administrator_email', + 'config_value' => 'admin@example.com', + ]); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Staff/StaffControllerTest.php b/tests/Feature/Api/V1/Staff/StaffControllerTest.php new file mode 100644 index 00000000..e55f40c8 --- /dev/null +++ b/tests/Feature/Api/V1/Staff/StaffControllerTest.php @@ -0,0 +1,169 @@ +createUser('teacher'); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/staff'); + + $response->assertForbidden(); + } + + public function test_index_returns_staff(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $staffUser = $this->createUser('teacher', 'staff@example.com'); + Staff::query()->create([ + 'user_id' => $staffUser->id, + 'firstname' => 'Staff', + 'lastname' => 'User', + 'email' => 'staff@example.com', + 'phone' => '5555555555', + 'role_name' => 'teacher', + 'active_role' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $response = $this->getJson('/api/v1/staff'); + + $response->assertOk(); + $response->assertJsonPath('data.staff.0.email', 'staff@example.com'); + } + + public function test_store_creates_staff(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $user = $this->createUser('teacher', 'newstaff@example.com'); + + $response = $this->postJson('/api/v1/staff', [ + 'user_id' => $user->id, + 'firstname' => 'New', + 'lastname' => 'Staff', + 'email' => 'newstaff@example.com', + 'phone' => '5555555555', + 'role_name' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $response->assertStatus(201); + $this->assertDatabaseHas('staff', [ + 'user_id' => $user->id, + 'email' => 'newstaff@example.com', + ]); + } + + public function test_store_validation(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $response = $this->postJson('/api/v1/staff', [ + 'firstname' => 'New', + ]); + + $response->assertStatus(422); + } + + public function test_update_modifies_staff(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $user = $this->createUser('teacher', 'updatestaff@example.com'); + $staff = Staff::query()->create([ + 'user_id' => $user->id, + 'firstname' => 'Old', + 'lastname' => 'Name', + 'email' => 'updatestaff@example.com', + 'phone' => '5555555555', + 'role_name' => 'teacher', + 'active_role' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $response = $this->patchJson('/api/v1/staff/' . $staff->id, [ + 'firstname' => 'Updated', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('staff', [ + 'id' => $staff->id, + 'firstname' => 'Updated', + ]); + } + + public function test_destroy_deletes_staff(): void + { + $admin = $this->createUser('admin'); + Sanctum::actingAs($admin); + + $user = $this->createUser('teacher', 'deletestaff@example.com'); + $staff = Staff::query()->create([ + 'user_id' => $user->id, + 'firstname' => 'Delete', + 'lastname' => 'Staff', + 'email' => 'deletestaff@example.com', + 'phone' => '5555555555', + 'role_name' => 'teacher', + 'active_role' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $response = $this->deleteJson('/api/v1/staff/' . $staff->id); + + $response->assertOk(); + $this->assertDatabaseMissing('staff', [ + 'id' => $staff->id, + ]); + } + + private function createUser(string $roleName, ?string $email = null): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $email ?? ($roleName . '@example.com'), + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Support/ContactControllerTest.php b/tests/Feature/Api/V1/Support/ContactControllerTest.php new file mode 100644 index 00000000..47172e02 --- /dev/null +++ b/tests/Feature/Api/V1/Support/ContactControllerTest.php @@ -0,0 +1,33 @@ +postJson('/api/v1/contact', [ + 'email' => 'not-an-email', + ]); + + $response->assertStatus(422); + } + + public function test_send_returns_success(): void + { + $response = $this->postJson('/api/v1/contact', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'subject' => 'Hello', + 'message' => 'This is a test message.', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure(['data' => ['contact' => ['email_sent', 'recipient']]]); + } +} diff --git a/tests/Feature/Api/V1/Support/SupportControllerTest.php b/tests/Feature/Api/V1/Support/SupportControllerTest.php new file mode 100644 index 00000000..53a84e87 --- /dev/null +++ b/tests/Feature/Api/V1/Support/SupportControllerTest.php @@ -0,0 +1,108 @@ +getJson('/api/v1/support'); + + $response->assertStatus(401); + } + + public function test_index_returns_requests(): void + { + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + SupportRequest::query()->create([ + 'user_id' => $user->id, + 'subject' => 'Help', + 'message' => 'Need assistance', + 'status' => 'open', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $response = $this->getJson('/api/v1/support'); + + $response->assertOk(); + $response->assertJsonPath('data.requests.0.subject', 'Help'); + } + + public function test_store_validation(): void + { + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/support', [ + 'subject' => 'Hi', + ]); + + $response->assertStatus(422); + } + + public function test_store_creates_request(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/support', [ + 'subject' => 'Help', + 'message' => 'Need assistance please', + ]); + + $response->assertStatus(201); + $this->assertDatabaseHas('support_requests', [ + 'user_id' => $user->id, + 'subject' => 'Help', + ]); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/System/DashboardControllerTest.php b/tests/Feature/Api/V1/System/DashboardControllerTest.php new file mode 100644 index 00000000..15a33652 --- /dev/null +++ b/tests/Feature/Api/V1/System/DashboardControllerTest.php @@ -0,0 +1,59 @@ +getJson('/api/v1/dashboard/route'); + + $response->assertStatus(401); + } + + public function test_route_returns_dashboard_for_role(): void + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'administrator', + 'dashboard_route' => '/administrator/dashboard', + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/dashboard/route'); + + $response->assertOk(); + $response->assertJsonPath('data.dashboard.route', '/administrator/dashboard'); + } +} diff --git a/tests/Feature/Api/V1/System/DatabaseHealthControllerTest.php b/tests/Feature/Api/V1/System/DatabaseHealthControllerTest.php new file mode 100644 index 00000000..9306d12e --- /dev/null +++ b/tests/Feature/Api/V1/System/DatabaseHealthControllerTest.php @@ -0,0 +1,19 @@ +getJson('/api/v1/system/db-check'); + + $response->assertOk(); + $response->assertJsonPath('data.ok', true); + } +} diff --git a/tests/Feature/Api/V1/System/HealthControllerTest.php b/tests/Feature/Api/V1/System/HealthControllerTest.php new file mode 100644 index 00000000..634c3a1c --- /dev/null +++ b/tests/Feature/Api/V1/System/HealthControllerTest.php @@ -0,0 +1,31 @@ +getJson('/api/v1/health'); + + $this->assertContains($response->status(), [200, 503]); + $response->assertJsonStructure([ + 'status', + 'message', + 'data' => [ + 'health' => [ + 'ok', + 'paths', + 'database', + 'write_path', + 'timestamp', + ], + ], + ]); + } +} diff --git a/tests/Feature/Api/V1/System/NavBuilderControllerTest.php b/tests/Feature/Api/V1/System/NavBuilderControllerTest.php new file mode 100644 index 00000000..7a9e4546 --- /dev/null +++ b/tests/Feature/Api/V1/System/NavBuilderControllerTest.php @@ -0,0 +1,150 @@ +insertGetId([ + 'name' => 'admin', + 'priority' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'CT', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => 'hashed', + 'semester' => 'Fall', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } + + public function test_menu_returns_items_for_roles(): void + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'parent', + 'priority' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Parent', + 'lastname' => 'User', + 'email' => 'parent@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'CT', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => 'hashed', + 'semester' => 'Fall', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + $navId = DB::table('nav_items')->insertGetId([ + 'label' => 'Home', + 'url' => 'home', + 'sort_order' => 1, + 'is_enabled' => 1, + ]); + + DB::table('role_nav_items')->insert([ + 'role_id' => $roleId, + 'nav_item_id' => $navId, + ]); + + $response = $this->actingAs($user, 'sanctum')->getJson('/api/v1/nav-builder/menu'); + + $response->assertOk(); + $response->assertJsonPath('data.items.0.label', 'Home'); + } + + public function test_data_requires_admin(): void + { + $user = $this->makeAdminUser(); + + $response = $this->actingAs($user, 'sanctum')->getJson('/api/v1/nav-builder/data'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['data' => ['items', 'roles', 'parentOptions']]]); + } + + public function test_store_creates_nav_item(): void + { + $user = $this->makeAdminUser(); + + $response = $this->actingAs($user, 'sanctum')->postJson('/api/v1/nav-builder', [ + 'label' => 'Dashboard', + 'url' => 'dashboard', + 'sort_order' => 1, + 'is_enabled' => true, + 'roles' => [], + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('nav_items', ['label' => 'Dashboard']); + } + + public function test_delete_removes_nav_item(): void + { + $user = $this->makeAdminUser(); + + $navId = DB::table('nav_items')->insertGetId([ + 'label' => 'Delete', + 'url' => 'delete', + 'sort_order' => 1, + 'is_enabled' => 1, + ]); + + $response = $this->actingAs($user, 'sanctum')->deleteJson("/api/v1/nav-builder/{$navId}"); + + $response->assertOk(); + $this->assertDatabaseMissing('nav_items', ['id' => $navId]); + } + + public function test_reorder_updates_items(): void + { + $user = $this->makeAdminUser(); + + $navId = DB::table('nav_items')->insertGetId([ + 'label' => 'Sort', + 'url' => 'sort', + 'sort_order' => 1, + 'is_enabled' => 1, + ]); + + $response = $this->actingAs($user, 'sanctum')->postJson('/api/v1/nav-builder/reorder', [ + 'orders' => [$navId => 9], + ]); + + $response->assertOk(); + $this->assertDatabaseHas('nav_items', ['id' => $navId, 'sort_order' => 9]); + } +} diff --git a/tests/Feature/Api/V1/Ui/UiControllerTest.php b/tests/Feature/Api/V1/Ui/UiControllerTest.php new file mode 100644 index 00000000..9631367b --- /dev/null +++ b/tests/Feature/Api/V1/Ui/UiControllerTest.php @@ -0,0 +1,76 @@ +postJson('/api/v1/ui/style', [ + 'accent' => 'blue', + ]); + + $response->assertStatus(401); + } + + public function test_style_updates_preferences(): void + { + $user = $this->createUser('parent'); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/ui/style', [ + 'accent' => 'blue', + 'menu' => 'custom', + 'menu_bg' => '#112233', + 'menu_text' => '#ffffff', + 'menu_mode' => 'dark', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('user_preferences', [ + 'user_id' => $user->id, + 'style_color' => 'blue', + 'menu_color' => 'custom', + ]); + } + + private function createUser(string $roleName): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => $roleName, + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $roleName . '@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Api/V1/Utilities/PhoneFormatterControllerTest.php b/tests/Feature/Api/V1/Utilities/PhoneFormatterControllerTest.php new file mode 100644 index 00000000..49f04af3 --- /dev/null +++ b/tests/Feature/Api/V1/Utilities/PhoneFormatterControllerTest.php @@ -0,0 +1,83 @@ +createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/utilities/phone/format', [ + 'number' => '(555) 123-4567', + ]); + + $response->assertOk(); + $response->assertJsonPath('data.phone.formatted', '555-123-4567'); + $response->assertJsonPath('data.phone.is_valid', true); + } + + public function test_format_rejects_invalid_number(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/utilities/phone/format', [ + 'number' => '123', + ]); + + $response->assertStatus(422); + } + + public function test_format_validates_payload(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/utilities/phone/format', []); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + private function createUser(): User + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + return $user; + } +} diff --git a/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php b/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php new file mode 100644 index 00000000..a6467f88 --- /dev/null +++ b/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php @@ -0,0 +1,84 @@ +get('/test-perm', fn () => 'ok'); + + $user = User::query()->create([ + 'firstname' => 'No', + 'lastname' => 'Perm', + 'email' => 'noperm@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'CT', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => 'hashed', + 'semester' => 'Fall', + ]); + + $response = $this->actingAs($user)->get('/test-perm', ['Accept' => 'application/json']); + + $response->assertStatus(403); + } + + public function test_allows_when_permission_assigned(): void + { + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => 'edit_students', + 'description' => 'Edit students', + ]); + + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'priority' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Has', + 'lastname' => 'Perm', + 'email' => 'hasperm@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'CT', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => 'hashed', + 'semester' => 'Fall', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + DB::table('role_permissions')->insert([ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + 'can_read' => 1, + ]); + + Route::middleware('perm:edit_students')->get('/test-perm-ok', fn () => 'ok'); + + $response = $this->actingAs($user)->get('/test-perm-ok'); + + $response->assertOk(); + $response->assertSee('ok'); + } +} diff --git a/tests/Unit/Resources/Frontend/FrontendPageResourceTest.php b/tests/Unit/Resources/Frontend/FrontendPageResourceTest.php new file mode 100644 index 00000000..1f35d246 --- /dev/null +++ b/tests/Unit/Resources/Frontend/FrontendPageResourceTest.php @@ -0,0 +1,17 @@ + 'index']); + $payload = $resource->toArray(request()); + + $this->assertSame('index', $payload['page']); + } +} diff --git a/tests/Unit/Resources/Frontend/PageContentResourceTest.php b/tests/Unit/Resources/Frontend/PageContentResourceTest.php new file mode 100644 index 00000000..d7418afb --- /dev/null +++ b/tests/Unit/Resources/Frontend/PageContentResourceTest.php @@ -0,0 +1,22 @@ + 'privacy_policy', + 'content' => '

Privacy

', + ]); + + $payload = $resource->toArray(request()); + + $this->assertSame('privacy_policy', $payload['type']); + $this->assertSame('

Privacy

', $payload['content']); + } +} diff --git a/tests/Unit/Resources/Messaging/MessageResourceTest.php b/tests/Unit/Resources/Messaging/MessageResourceTest.php new file mode 100644 index 00000000..2b311456 --- /dev/null +++ b/tests/Unit/Resources/Messaging/MessageResourceTest.php @@ -0,0 +1,34 @@ + 1, + 'sender_id' => 2, + 'recipient_id' => 3, + 'subject' => 'Hello', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-1', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $payload = (new MessageResource($message))->toArray(request()); + + $this->assertArrayHasKey('id', $payload); + $this->assertArrayHasKey('subject', $payload); + $this->assertArrayHasKey('message_number', $payload); + } +} diff --git a/tests/Unit/Resources/Messaging/RecipientResourceTest.php b/tests/Unit/Resources/Messaging/RecipientResourceTest.php new file mode 100644 index 00000000..9b6ddae2 --- /dev/null +++ b/tests/Unit/Resources/Messaging/RecipientResourceTest.php @@ -0,0 +1,18 @@ + 5, 'name' => 'Test User']); + $payload = $resource->toArray(request()); + + $this->assertSame(5, $payload['id']); + $this->assertSame('Test User', $payload['name']); + } +} diff --git a/tests/Unit/Resources/Settings/SettingsResourceTest.php b/tests/Unit/Resources/Settings/SettingsResourceTest.php new file mode 100644 index 00000000..1ee6d82f --- /dev/null +++ b/tests/Unit/Resources/Settings/SettingsResourceTest.php @@ -0,0 +1,24 @@ + 'App', + 'administrator_email' => 'admin@example.com', + 'timezone' => 'UTC', + ]); + + $payload = $resource->toArray(request()); + + $this->assertSame('App', $payload['site_name']); + $this->assertSame('admin@example.com', $payload['administrator_email']); + $this->assertSame('UTC', $payload['timezone']); + } +} diff --git a/tests/Unit/Resources/Staff/StaffResourceTest.php b/tests/Unit/Resources/Staff/StaffResourceTest.php new file mode 100644 index 00000000..9f474ce9 --- /dev/null +++ b/tests/Unit/Resources/Staff/StaffResourceTest.php @@ -0,0 +1,29 @@ + 1, + 'user_id' => 2, + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'test@example.com', + 'role_name' => 'teacher', + 'active_role' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $payload = (new StaffResource($staff))->toArray(request()); + + $this->assertSame(1, $payload['id']); + $this->assertSame('teacher', $payload['role_name']); + } +} diff --git a/tests/Unit/Resources/Support/ContactMessageResourceTest.php b/tests/Unit/Resources/Support/ContactMessageResourceTest.php new file mode 100644 index 00000000..350b3007 --- /dev/null +++ b/tests/Unit/Resources/Support/ContactMessageResourceTest.php @@ -0,0 +1,22 @@ + true, + 'recipient' => 'support@example.com', + ]); + + $payload = $resource->toArray(request()); + + $this->assertTrue($payload['email_sent']); + $this->assertSame('support@example.com', $payload['recipient']); + } +} diff --git a/tests/Unit/Resources/Support/SupportRequestResourceTest.php b/tests/Unit/Resources/Support/SupportRequestResourceTest.php new file mode 100644 index 00000000..ef36e2bb --- /dev/null +++ b/tests/Unit/Resources/Support/SupportRequestResourceTest.php @@ -0,0 +1,28 @@ + 1, + 'user_id' => 2, + 'subject' => 'Help', + 'message' => 'Need help', + 'status' => 'open', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $payload = (new SupportRequestResource($model))->toArray(request()); + + $this->assertSame(1, $payload['id']); + $this->assertSame('Help', $payload['subject']); + } +} diff --git a/tests/Unit/Resources/Ui/UiStyleResourceTest.php b/tests/Unit/Resources/Ui/UiStyleResourceTest.php new file mode 100644 index 00000000..b8c43d4a --- /dev/null +++ b/tests/Unit/Resources/Ui/UiStyleResourceTest.php @@ -0,0 +1,26 @@ + 'blue', + 'menu_color' => 'custom', + 'menu_custom_bg' => '#112233', + 'menu_custom_text' => '#FFFFFF', + 'menu_custom_mode' => 'dark', + ]); + + $payload = (new UiStyleResource($prefs))->toArray(request()); + + $this->assertSame('blue', $payload['style_color']); + $this->assertSame('custom', $payload['menu_color']); + } +} diff --git a/tests/Unit/Services/Attendance/LateSlipLogCommandServiceTest.php b/tests/Unit/Services/Attendance/LateSlipLogCommandServiceTest.php new file mode 100644 index 00000000..e7b9974f --- /dev/null +++ b/tests/Unit/Services/Attendance/LateSlipLogCommandServiceTest.php @@ -0,0 +1,33 @@ +create([ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student X', + 'slip_date' => '2025-09-01', + 'time_in' => '08:05:00', + 'grade' => '4', + 'reason' => 'Traffic', + 'admin_name' => 'Admin User', + ]); + + $service = app(LateSlipLogCommandService::class); + $deleted = $service->delete($log); + + $this->assertTrue($deleted); + $this->assertDatabaseMissing('late_slip_logs', ['id' => $log->id]); + } +} diff --git a/tests/Unit/Services/Attendance/LateSlipLogQueryServiceTest.php b/tests/Unit/Services/Attendance/LateSlipLogQueryServiceTest.php new file mode 100644 index 00000000..7b1e283c --- /dev/null +++ b/tests/Unit/Services/Attendance/LateSlipLogQueryServiceTest.php @@ -0,0 +1,37 @@ +insert([ + [ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student A', + 'slip_date' => '2025-09-01', + ], + [ + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'student_name' => 'Student B', + 'slip_date' => '2025-10-01', + ], + ]); + + $service = app(LateSlipLogQueryService::class); + $pager = $service->paginate(['date_from' => '2025-10-01'], 1, 10); + + $this->assertSame(1, $pager->total()); + $this->assertSame('Student B', $pager->items()[0]->student_name); + } +} diff --git a/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php b/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php index 7580bc4e..6292655a 100644 --- a/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php +++ b/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php @@ -10,24 +10,24 @@ class RegistrationCaptchaServiceTest extends TestCase { use RefreshDatabase; - public function test_generate_sets_session_value(): void + public function test_generate_sets_cache_value(): void { $service = new RegistrationCaptchaService(); $value = $service->generate(6); $this->assertNotEmpty($value); - $this->assertSame($value, session()->get('captcha_answer')); + $this->assertTrue($service->verify($value)); } public function test_verify_and_clear(): void { $service = new RegistrationCaptchaService(); - session()->put('captcha_answer', 'ABC123'); + $value = $service->generate(6); - $this->assertTrue($service->verify('ABC123')); + $this->assertTrue($service->verify($value)); $this->assertFalse($service->verify('WRONG')); $service->clear(); - $this->assertNull(session()->get('captcha_answer')); + $this->assertFalse($service->verify($value)); } } diff --git a/tests/Unit/Services/Auth/RegistrationServiceTest.php b/tests/Unit/Services/Auth/RegistrationServiceTest.php index 798ac59e..c36ea3b6 100644 --- a/tests/Unit/Services/Auth/RegistrationServiceTest.php +++ b/tests/Unit/Services/Auth/RegistrationServiceTest.php @@ -35,7 +35,8 @@ class RegistrationServiceTest extends TestCase 'is_active' => 1, ]); - session()->put('captcha_answer', 'ABCD'); + $captchaService = new RegistrationCaptchaService(); + $captcha = $captchaService->generate(4); $emailService = Mockery::mock(EmailService::class); $emailService->shouldReceive('send')->once()->andReturn(true); @@ -44,7 +45,7 @@ class RegistrationServiceTest extends TestCase $emailService, new SchoolIdService(), new RegistrationFormatterService(new PhoneFormatterService()), - new RegistrationCaptchaService() + $captchaService ); $result = $service->register([ @@ -60,7 +61,7 @@ class RegistrationServiceTest extends TestCase 'state' => 'CT', 'zip' => '12345', 'accept_school_policy' => 1, - 'captcha' => 'ABCD', + 'captcha' => $captcha, 'is_parent' => 1, 'no_second_parent_info' => 1, ]); @@ -114,7 +115,8 @@ class RegistrationServiceTest extends TestCase 'token' => 'token', ]); - session()->put('captcha_answer', 'ABCD'); + $captchaService = new RegistrationCaptchaService(); + $captcha = $captchaService->generate(4); $emailService = Mockery::mock(EmailService::class); $emailService->shouldNotReceive('send'); @@ -123,7 +125,7 @@ class RegistrationServiceTest extends TestCase $emailService, new SchoolIdService(), new RegistrationFormatterService(new PhoneFormatterService()), - new RegistrationCaptchaService() + $captchaService ); $result = $service->register([ @@ -139,7 +141,7 @@ class RegistrationServiceTest extends TestCase 'state' => 'CT', 'zip' => '12345', 'accept_school_policy' => 1, - 'captcha' => 'ABCD', + 'captcha' => $captcha, 'is_parent' => 1, 'no_second_parent_info' => 1, ]); diff --git a/tests/Unit/Services/Auth/SessionActivityServiceTest.php b/tests/Unit/Services/Auth/SessionActivityServiceTest.php deleted file mode 100644 index 50a02aeb..00000000 --- a/tests/Unit/Services/Auth/SessionActivityServiceTest.php +++ /dev/null @@ -1,42 +0,0 @@ -makeSessionStore(); - $service = new SessionActivityService($store); - - $service->setLastActivity(12345); - - $this->assertTrue($service->hasLastActivity()); - $this->assertSame(12345, $service->getLastActivity()); - } - - public function test_clear_last_activity(): void - { - $store = $this->makeSessionStore(); - $service = new SessionActivityService($store); - - $service->setLastActivity(12345); - $service->clearLastActivity(); - - $this->assertFalse($service->hasLastActivity()); - } - - private function makeSessionStore(): Store - { - $handler = new ArraySessionHandler(10); - $store = new Store('test', $handler); - $store->start(); - - return $store; - } -} diff --git a/tests/Unit/Services/Auth/SessionTimeoutConfigServiceTest.php b/tests/Unit/Services/Auth/SessionTimeoutConfigServiceTest.php deleted file mode 100644 index 86d9c4c1..00000000 --- a/tests/Unit/Services/Auth/SessionTimeoutConfigServiceTest.php +++ /dev/null @@ -1,22 +0,0 @@ -config(); - - $this->assertArrayHasKey('timeout', $config); - $this->assertArrayHasKey('warning_time', $config); - $this->assertArrayHasKey('check_interval', $config); - $this->assertArrayHasKey('logout_url', $config); - $this->assertArrayHasKey('keep_alive_url', $config); - $this->assertArrayHasKey('check_url', $config); - } -} diff --git a/tests/Unit/Services/Auth/SessionTimeoutServiceTest.php b/tests/Unit/Services/Auth/SessionTimeoutServiceTest.php deleted file mode 100644 index 8d799e16..00000000 --- a/tests/Unit/Services/Auth/SessionTimeoutServiceTest.php +++ /dev/null @@ -1,51 +0,0 @@ -shouldReceive('hasLastActivity')->andReturn(false); - $activity->shouldReceive('clearLastActivity')->once(); - $activity->shouldReceive('invalidate')->once(); - - $service = new SessionTimeoutService($activity); - $result = $service->checkTimeout(); - - $this->assertSame('expired', $result['status']); - } - - public function test_check_timeout_returns_warning(): void - { - $activity = Mockery::mock(SessionActivityService::class); - $activity->shouldReceive('hasLastActivity')->andReturn(true); - $activity->shouldReceive('getLastActivity')->andReturn(time() - (SessionTimeout::WARNING_THRESHOLD + 10)); - - $service = new SessionTimeoutService($activity); - $result = $service->checkTimeout(); - - $this->assertSame('warning', $result['status']); - } - - public function test_ping_returns_expired_when_timed_out(): void - { - $activity = Mockery::mock(SessionActivityService::class); - $activity->shouldReceive('hasLastActivity')->andReturn(true); - $activity->shouldReceive('getLastActivity')->andReturn(time() - (SessionTimeout::TIMEOUT_DURATION + 10)); - $activity->shouldReceive('clearLastActivity')->once(); - $activity->shouldReceive('invalidate')->once(); - - $service = new SessionTimeoutService($activity); - $result = $service->pingActivity(); - - $this->assertSame('expired', $result['status']); - } -} diff --git a/tests/Unit/Services/Auth/UserRoleServiceTest.php b/tests/Unit/Services/Auth/UserRoleServiceTest.php new file mode 100644 index 00000000..2b756fbd --- /dev/null +++ b/tests/Unit/Services/Auth/UserRoleServiceTest.php @@ -0,0 +1,44 @@ +insertGetId([ + 'name' => 'manage_students', + 'description' => 'Manage students', + ]); + + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'priority' => 1, + ]); + + DB::table('user_roles')->insert([ + 'user_id' => 5, + 'role_id' => $roleId, + ]); + + DB::table('role_permissions')->insert([ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + 'can_read' => 1, + 'can_manage' => 0, + ]); + + $service = new UserRoleService(); + $roleIds = $service->getRoleIds(5); + + $this->assertTrue($service->hasPermissionForCrud($roleIds, 'manage_students', 'read')); + $this->assertFalse($service->hasPermissionForCrud($roleIds, 'manage_students', 'delete')); + } +} diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentWriterServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentWriterServiceTest.php new file mode 100644 index 00000000..19069490 --- /dev/null +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentWriterServiceTest.php @@ -0,0 +1,33 @@ +saveAdjustments('101', '2025-2026', [ + 'Small Table' => 2, + 'Unknown Item' => 5, + ], ['Small Table']); + + $this->assertSame(1, $count); + $this->assertDatabaseHas('class_prep_adjustments', [ + 'class_section_id' => '101', + 'school_year' => '2025-2026', + 'item_name' => 'Small Table', + 'adjustment' => 2, + ]); + $this->assertDatabaseMissing('class_prep_adjustments', [ + 'item_name' => 'Unknown Item', + ]); + } +} diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationPrintServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationPrintServiceTest.php new file mode 100644 index 00000000..693e3924 --- /dev/null +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationPrintServiceTest.php @@ -0,0 +1,105 @@ +seedPrepData(); + + $service = app(ClassPreparationPrintService::class); + $payload = $service->printPrep('101', '2025-2026', 'Fall'); + + $this->assertTrue($payload['ok']); + $this->assertDatabaseHas('class_preparation_log', [ + 'class_section_id' => 101, + 'school_year' => '2025-2026', + ]); + } + + public function test_print_prep_handles_log_failure(): void + { + $context = Mockery::mock(ClassPreparationContextService::class); + $roster = Mockery::mock(ClassPreparationRosterService::class); + $calculator = Mockery::mock(ClassPreparationCalculatorService::class); + $adjustments = Mockery::mock(ClassPreparationAdjustmentService::class); + $logs = Mockery::mock(ClassPreparationLogService::class); + + $context->shouldReceive('getSemester')->andReturn('Fall'); + $context->shouldReceive('hasRosterForSemester')->andReturn(true); + $roster->shouldReceive('getStudentCountForSection')->andReturn(10); + $calculator->shouldReceive('getClassLevelBySection')->andReturn(2); + $calculator->shouldReceive('calculatePrepItems')->andReturn(['Small Table' => 2]); + $adjustments->shouldReceive('applyAdjustments')->andReturn([['Small Table' => 2], []]); + $logs->shouldReceive('createLog')->andReturn(false); + + $service = new ClassPreparationPrintService($context, $roster, $calculator, $adjustments, $logs); + $payload = $service->printPrep('101', '2025-2026', 'Fall'); + + $this->assertFalse($payload['ok']); + $this->assertNull($payload['printed_at']); + } + + private function seedPrepData(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + DB::table('classSection')->insert([ + 'id' => 1, + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('students')->insert([ + 'school_id' => 'S-1', + 'firstname' => 'Student', + 'lastname' => 'One', + 'age' => 8, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => 1, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'is_active' => 1, + ]); + + DB::table('student_class')->insert([ + 'student_id' => 1, + 'class_section_id' => 101, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('inventory_categories')->insert([ + 'type' => 'classroom', + 'name' => 'Small Table', + ]); + } +} diff --git a/tests/Unit/Services/ClassSections/ClassAttendanceServiceTest.php b/tests/Unit/Services/ClassSections/ClassAttendanceServiceTest.php new file mode 100644 index 00000000..4e193dcb --- /dev/null +++ b/tests/Unit/Services/ClassSections/ClassAttendanceServiceTest.php @@ -0,0 +1,73 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('students')->insert([ + 'school_id' => 'S-1', + 'firstname' => 'Student', + 'lastname' => 'One', + 'age' => 8, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => 1, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'is_active' => 1, + ]); + + DB::table('student_class')->insert([ + 'student_id' => 1, + 'class_section_id' => 101, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $teacher = User::query()->create([ + 'firstname' => 'Teach', + 'lastname' => 'Er', + 'email' => 'teacher@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = app(ClassAttendanceService::class); + $payload = $service->getAttendancePayload(101, $teacher->id); + + $this->assertSame('1-A', $payload['class_name']); + $this->assertCount(1, $payload['students']); + } +} diff --git a/tests/Unit/Services/ClassSections/ClassSectionCommandServiceTest.php b/tests/Unit/Services/ClassSections/ClassSectionCommandServiceTest.php new file mode 100644 index 00000000..8fef53b1 --- /dev/null +++ b/tests/Unit/Services/ClassSections/ClassSectionCommandServiceTest.php @@ -0,0 +1,36 @@ +create([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $this->assertInstanceOf(ClassSection::class, $section); + $this->assertDatabaseHas('classSection', ['id' => $section->id]); + + $updated = $service->update($section, ['class_section_name' => '1-B']); + $this->assertSame('1-B', $updated->class_section_name); + + $deleted = $service->delete($section); + $this->assertTrue($deleted); + $this->assertDatabaseMissing('classSection', ['id' => $section->id]); + } +} diff --git a/tests/Unit/Services/ClassSections/ClassSectionQueryServiceTest.php b/tests/Unit/Services/ClassSections/ClassSectionQueryServiceTest.php new file mode 100644 index 00000000..f5bf6888 --- /dev/null +++ b/tests/Unit/Services/ClassSections/ClassSectionQueryServiceTest.php @@ -0,0 +1,47 @@ +insert([ + 'class_name' => 'Class 1', + 'schedule' => 'Monday', + 'capacity' => 30, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('classSection')->insert([ + [ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ], + [ + 'class_id' => 1, + 'class_section_id' => 102, + 'class_section_name' => '2-B', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ], + ]); + + $service = app(ClassSectionQueryService::class); + $pager = $service->list(['search' => '1-A'], 1, 10); + + $this->assertSame(1, $pager->total()); + $this->assertSame('1-A', $pager->items()[0]->class_section_name); + } +} diff --git a/tests/Unit/Services/ClassSections/ClassSectionSeedServiceTest.php b/tests/Unit/Services/ClassSections/ClassSectionSeedServiceTest.php new file mode 100644 index 00000000..79206862 --- /dev/null +++ b/tests/Unit/Services/ClassSections/ClassSectionSeedServiceTest.php @@ -0,0 +1,31 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $service = app(ClassSectionSeedService::class); + $count = $service->seedDefaults(); + + $this->assertGreaterThan(0, $count); + $this->assertDatabaseHas('classes', [ + 'class_name' => 'Class 1', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Unit/Services/Dashboard/DashboardRouteServiceTest.php b/tests/Unit/Services/Dashboard/DashboardRouteServiceTest.php new file mode 100644 index 00000000..4e1e1697 --- /dev/null +++ b/tests/Unit/Services/Dashboard/DashboardRouteServiceTest.php @@ -0,0 +1,75 @@ +create([ + 'firstname' => 'No', + 'lastname' => 'Role', + 'email' => 'norole@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = app(DashboardRouteService::class); + $payload = $service->resolveForUser($user); + + $this->assertSame('/landing_page/guest_dashboard', $payload['route']); + } + + public function test_resolve_returns_dashboard_route_for_role(): void + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'administrator', + 'dashboard_route' => '/administrator/dashboard', + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + $service = app(DashboardRouteService::class); + $payload = $service->resolveForUser($user); + + $this->assertSame('/administrator/dashboard', $payload['route']); + $this->assertSame('administrator', strtolower((string) $payload['role'])); + } +} diff --git a/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php b/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php new file mode 100644 index 00000000..28256680 --- /dev/null +++ b/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php @@ -0,0 +1,39 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $emailService = $this->createMock(EmailDispatchService::class); + $emailService->method('send')->willReturn(false); + + $service = new ContactSubmissionService($emailService, new GlobalConfigService()); + + $result = $service->submit([ + 'email' => 'contact@example.com', + 'message' => 'Hello from unit test', + ]); + + $this->assertNotNull($result['record']); + $this->assertDatabaseHas('contactus', [ + 'subject' => 'Contact Us Form Submission', + 'semester' => 'Fall', + ]); + } +} diff --git a/tests/Unit/Services/Frontend/FrontendPageServiceTest.php b/tests/Unit/Services/Frontend/FrontendPageServiceTest.php new file mode 100644 index 00000000..b31d94fe --- /dev/null +++ b/tests/Unit/Services/Frontend/FrontendPageServiceTest.php @@ -0,0 +1,17 @@ +page('index'); + + $this->assertSame('index', $payload['page']); + } +} diff --git a/tests/Unit/Services/Frontend/LandingPageContextServiceTest.php b/tests/Unit/Services/Frontend/LandingPageContextServiceTest.php new file mode 100644 index 00000000..4a741adb --- /dev/null +++ b/tests/Unit/Services/Frontend/LandingPageContextServiceTest.php @@ -0,0 +1,29 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ['config_key' => 'enrollment_deadline', 'config_value' => '2025-09-01'], + ['config_key' => 'refund_deadline', 'config_value' => '2025-10-01'], + ]); + + $service = app(LandingPageContextService::class); + $context = $service->context(); + + $this->assertSame('2025-2026', $context['school_year']); + $this->assertSame('Fall', $context['semester']); + } +} diff --git a/tests/Unit/Services/Frontend/LandingPageParentDashboardServiceTest.php b/tests/Unit/Services/Frontend/LandingPageParentDashboardServiceTest.php new file mode 100644 index 00000000..d0f231b5 --- /dev/null +++ b/tests/Unit/Services/Frontend/LandingPageParentDashboardServiceTest.php @@ -0,0 +1,57 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + DB::table('students')->insert([ + 'id' => 1, + 'school_id' => 'S-1', + 'firstname' => 'Student', + 'lastname' => 'One', + 'age' => 8, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => 1, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'is_active' => 1, + ]); + + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('student_class')->insert([ + 'student_id' => 1, + 'class_section_id' => 101, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = app(LandingPageParentDashboardService::class); + $payload = $service->summary(1, 'primary'); + + $this->assertTrue($payload['ok']); + $this->assertCount(1, $payload['students']); + } +} diff --git a/tests/Unit/Services/Frontend/LandingPageRoleServiceTest.php b/tests/Unit/Services/Frontend/LandingPageRoleServiceTest.php new file mode 100644 index 00000000..74139631 --- /dev/null +++ b/tests/Unit/Services/Frontend/LandingPageRoleServiceTest.php @@ -0,0 +1,53 @@ +assertSame('guest', $service->resolveRole(null)); + } + + public function test_resolve_role_returns_first_role(): void + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'teacher', + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Teach', + 'lastname' => 'Er', + 'email' => 'teacher@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + $service = app(LandingPageRoleService::class); + $this->assertSame('teacher', $service->resolveRole($user)); + } +} diff --git a/tests/Unit/Services/Frontend/LandingPageServiceTest.php b/tests/Unit/Services/Frontend/LandingPageServiceTest.php new file mode 100644 index 00000000..179d0a20 --- /dev/null +++ b/tests/Unit/Services/Frontend/LandingPageServiceTest.php @@ -0,0 +1,57 @@ +dashboardForUser(null); + + $this->assertSame('guest', $payload['role']); + } + + public function test_dashboard_for_user_returns_admin(): void + { + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'priority' => 1, + 'is_active' => 1, + ]); + + $user = User::query()->create([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + $service = app(LandingPageService::class); + $payload = $service->dashboardForUser($user); + + $this->assertSame('admin', $payload['role']); + } +} diff --git a/tests/Unit/Services/Frontend/LandingPageTeacherSummaryServiceTest.php b/tests/Unit/Services/Frontend/LandingPageTeacherSummaryServiceTest.php new file mode 100644 index 00000000..6557cd11 --- /dev/null +++ b/tests/Unit/Services/Frontend/LandingPageTeacherSummaryServiceTest.php @@ -0,0 +1,43 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + DB::table('teacher_class')->insert([ + 'teacher_id' => 1, + 'class_section_id' => 101, + 'position' => 'main', + 'school_year' => '2025-2026', + ]); + + DB::table('classSection')->insert([ + 'class_id' => 1, + 'class_section_id' => 101, + 'class_section_name' => '1-A', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = app(LandingPageTeacherSummaryService::class); + $payload = $service->summary(1); + + $this->assertSame(101, $payload['class_section_id']); + $this->assertArrayHasKey('scoreSummary', $payload); + $this->assertArrayHasKey('attendanceSummary', $payload); + } +} diff --git a/tests/Unit/Services/Frontend/ProfileIconServiceTest.php b/tests/Unit/Services/Frontend/ProfileIconServiceTest.php new file mode 100644 index 00000000..5a160b60 --- /dev/null +++ b/tests/Unit/Services/Frontend/ProfileIconServiceTest.php @@ -0,0 +1,37 @@ +create([ + 'firstname' => 'Jane', + 'lastname' => 'Smith', + 'email' => 'jane@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = app(ProfileIconService::class); + $payload = $service->buildForUser($user->id); + + $this->assertSame('JS', $payload['userInitials']); + } +} diff --git a/tests/Unit/Services/Frontend/StaticPageServiceTest.php b/tests/Unit/Services/Frontend/StaticPageServiceTest.php new file mode 100644 index 00000000..0ac31223 --- /dev/null +++ b/tests/Unit/Services/Frontend/StaticPageServiceTest.php @@ -0,0 +1,32 @@ +Terms'); + + $service = new StaticPageService(); + $result = $service->load('terms_of_service.html', 'terms_of_service'); + + $this->assertTrue($result['ok']); + $this->assertSame('terms_of_service', $result['type']); + $this->assertStringContainsString('Terms', $result['content']); + } + + public function test_load_missing_returns_error(): void + { + $service = new StaticPageService(); + $result = $service->load('missing.html', 'missing'); + + $this->assertFalse($result['ok']); + } +} diff --git a/tests/Unit/Services/Messaging/MessageCommandServiceTest.php b/tests/Unit/Services/Messaging/MessageCommandServiceTest.php new file mode 100644 index 00000000..85e8f6e1 --- /dev/null +++ b/tests/Unit/Services/Messaging/MessageCommandServiceTest.php @@ -0,0 +1,72 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $sender = $this->createUser('sender@example.com'); + $recipient = $this->createUser('recipient@example.com'); + + $service = new MessageCommandService(new GlobalConfigService()); + $message = $service->create($sender->id, [ + 'recipient_id' => $recipient->id, + 'subject' => 'Subject', + 'message' => 'Body', + ]); + + $this->assertNotNull($message->id); + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'subject' => 'Subject', + ]); + } + + public function test_create_rejects_invalid_role(): void + { + $sender = $this->createUser('sender2@example.com'); + + $service = new MessageCommandService(new GlobalConfigService()); + + $this->expectException(\RuntimeException::class); + $service->create($sender->id, [ + 'recipient_role' => 'invalid', + 'subject' => 'Subject', + 'message' => 'Body', + ]); + } + + private function createUser(string $email): User + { + return User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $email, + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Unit/Services/Messaging/MessageQueryServiceTest.php b/tests/Unit/Services/Messaging/MessageQueryServiceTest.php new file mode 100644 index 00000000..33677cef --- /dev/null +++ b/tests/Unit/Services/Messaging/MessageQueryServiceTest.php @@ -0,0 +1,58 @@ +createUser('sender@example.com'); + $recipient = $this->createUser('recipient@example.com'); + + Message::query()->create([ + 'sender_id' => $sender->id, + 'recipient_id' => $recipient->id, + 'subject' => 'Hello', + 'message' => 'Body', + 'sent_datetime' => now(), + 'read_status' => 0, + 'message_number' => 'MSG-10', + 'priority' => 'normal', + 'status' => 'sent', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = new MessageQueryService(); + $paginator = $service->inbox($recipient->id, [], 1, 20); + + $this->assertSame(1, $paginator->total()); + } + + private function createUser(string $email): User + { + return User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $email, + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php b/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php new file mode 100644 index 00000000..6019804a --- /dev/null +++ b/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php @@ -0,0 +1,111 @@ +createUser('teacher@example.com'); + + DB::table('teacher_class')->insert([ + 'teacher_id' => $teacher->id, + 'class_section_id' => 1, + 'position' => 'main', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $service = new MessageRecipientService(); + $rows = $service->teachers(); + + $this->assertSame($teacher->id, $rows[0]['id']); + } + + public function test_parents_returns_list(): void + { + $teacher = $this->createUser('teacher2@example.com'); + $parent = $this->createUser('parent@example.com'); + $secondParent = $this->createUser('parent2@example.com'); + + DB::table('teacher_class')->insert([ + 'teacher_id' => $teacher->id, + 'class_section_id' => 2, + 'position' => 'main', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $studentId = Student::query()->insertGetId([ + 'school_id' => 'S-1', + 'firstname' => 'Student', + 'lastname' => 'One', + 'age' => 10, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => $parent->id, + 'year_of_registration' => '2025', + 'is_active' => 1, + 'is_new' => 1, + ]); + + DB::table('student_class')->insert([ + 'student_id' => $studentId, + 'class_section_id' => 2, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + ParentModel::query()->create([ + 'secondparent_firstname' => 'Second', + 'secondparent_lastname' => 'Parent', + 'secondparent_email' => 'parent2@example.com', + 'secondparent_phone' => '5555555555', + 'firstparent_id' => $parent->id, + 'secondparent_id' => $secondParent->id, + 'school_year' => '2025-2026', + ]); + + $service = new MessageRecipientService(); + $rows = $service->parents(); + + $ids = array_column($rows, 'id'); + $this->assertContains($parent->id, $ids); + $this->assertContains($secondParent->id, $ids); + } + + private function createUser(string $email): User + { + return User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => $email, + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Unit/Services/Navigation/NavBuilderServiceTest.php b/tests/Unit/Services/Navigation/NavBuilderServiceTest.php new file mode 100644 index 00000000..26f6bb7a --- /dev/null +++ b/tests/Unit/Services/Navigation/NavBuilderServiceTest.php @@ -0,0 +1,56 @@ +insertGetId([ + 'name' => 'admin', + 'priority' => 1, + ]); + + $service = new NavBuilderService(new NavbarService()); + $result = $service->save([ + 'label' => 'Dashboard', + 'url' => 'dashboard', + 'sort_order' => 1, + 'is_enabled' => 1, + 'roles' => [$roleId], + ]); + + $this->assertTrue($result['ok']); + $this->assertDatabaseHas('nav_items', ['label' => 'Dashboard']); + $this->assertDatabaseHas('role_nav_items', [ + 'role_id' => $roleId, + 'nav_item_id' => $result['id'], + ]); + } + + public function test_reorder_updates_sort_order(): void + { + $id = DB::table('nav_items')->insertGetId([ + 'label' => 'Item', + 'url' => 'item', + 'sort_order' => 1, + 'is_enabled' => 1, + ]); + + $service = new NavBuilderService(new NavbarService()); + $service->reorder([$id => 5]); + + $this->assertDatabaseHas('nav_items', [ + 'id' => $id, + 'sort_order' => 5, + ]); + } +} diff --git a/tests/Unit/Services/Navigation/NavbarServiceTest.php b/tests/Unit/Services/Navigation/NavbarServiceTest.php new file mode 100644 index 00000000..525122aa --- /dev/null +++ b/tests/Unit/Services/Navigation/NavbarServiceTest.php @@ -0,0 +1,49 @@ +insertGetId([ + 'name' => 'admin', + 'priority' => 1, + ]); + + $parentId = DB::table('nav_items')->insertGetId([ + 'label' => 'Parent', + 'url' => 'parent', + 'sort_order' => 1, + 'is_enabled' => 1, + ]); + + $childId = DB::table('nav_items')->insertGetId([ + 'label' => 'Child', + 'url' => 'child', + 'menu_parent_id' => $parentId, + 'sort_order' => 2, + 'is_enabled' => 1, + ]); + + DB::table('role_nav_items')->insert([ + ['role_id' => $roleId, 'nav_item_id' => $parentId], + ['role_id' => $roleId, 'nav_item_id' => $childId], + ]); + + $service = new NavbarService(); + $menu = $service->getMenuForRoles(['admin']); + + $this->assertCount(1, $menu); + $this->assertSame('Parent', $menu[0]['label']); + $this->assertCount(1, $menu[0]['children']); + $this->assertSame('Child', $menu[0]['children'][0]['label']); + } +} diff --git a/tests/Unit/Services/Phone/PhoneFormatterServiceTest.php b/tests/Unit/Services/Phone/PhoneFormatterServiceTest.php new file mode 100644 index 00000000..88e9d8a2 --- /dev/null +++ b/tests/Unit/Services/Phone/PhoneFormatterServiceTest.php @@ -0,0 +1,26 @@ +assertNull($service->format('123')); + } + + public function test_format_formats_digits(): void + { + $service = app(PhoneFormatterService::class); + + $this->assertSame('555-123-4567', $service->format('(555) 123-4567')); + } +} diff --git a/tests/Unit/Services/Policy/PolicyContentServiceTest.php b/tests/Unit/Services/Policy/PolicyContentServiceTest.php new file mode 100644 index 00000000..1c12b85a --- /dev/null +++ b/tests/Unit/Services/Policy/PolicyContentServiceTest.php @@ -0,0 +1,23 @@ +getPolicy('school'); + + $this->assertSame('school', $result['type']); + $this->assertSame('School Policy', $result['title']); + $this->assertSame('html', $result['format']); + $this->assertNotEmpty($result['content']); + } +} diff --git a/tests/Unit/Services/Preferences/PreferencesCommandServiceTest.php b/tests/Unit/Services/Preferences/PreferencesCommandServiceTest.php new file mode 100644 index 00000000..a68e694f --- /dev/null +++ b/tests/Unit/Services/Preferences/PreferencesCommandServiceTest.php @@ -0,0 +1,30 @@ +upsert(1, [ + 'receive_email_notifications' => true, + 'receive_sms_notifications' => false, + 'theme' => 'light', + 'language' => 'en', + ]); + + $this->assertSame(1, $pref->user_id); + $this->assertDatabaseHas('user_preferences', [ + 'user_id' => 1, + 'notification_email' => 1, + 'notification_sms' => 0, + ]); + } +} diff --git a/tests/Unit/Services/Preferences/PreferencesOptionsServiceTest.php b/tests/Unit/Services/Preferences/PreferencesOptionsServiceTest.php new file mode 100644 index 00000000..52f1cf1a --- /dev/null +++ b/tests/Unit/Services/Preferences/PreferencesOptionsServiceTest.php @@ -0,0 +1,22 @@ +defaultPreferences(); + + $this->assertArrayHasKey('receive_email_notifications', $defaults); + $this->assertArrayHasKey('receive_sms_notifications', $defaults); + $this->assertSame('light', $defaults['theme']); + } +} diff --git a/tests/Unit/Services/Preferences/PreferencesQueryServiceTest.php b/tests/Unit/Services/Preferences/PreferencesQueryServiceTest.php new file mode 100644 index 00000000..b1e2209b --- /dev/null +++ b/tests/Unit/Services/Preferences/PreferencesQueryServiceTest.php @@ -0,0 +1,54 @@ +getForUser(1); + + $this->assertTrue($payload['preferences']['receive_email_notifications']); + $this->assertSame('en', $payload['preferences']['language']); + } + + public function test_paginate_filters_by_user_id(): void + { + DB::table('users')->insert([ + 'id' => 1, + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'test@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + DB::table('user_preferences')->insert([ + 'user_id' => 1, + 'notification_email' => 1, + 'notification_sms' => 1, + ]); + + $service = app(PreferencesQueryService::class); + $pager = $service->paginate(['user_id' => 1], 1, 10); + + $this->assertSame(1, $pager->total()); + $this->assertSame(1, $pager->items()[0]->user_id); + } +} diff --git a/tests/Unit/Services/School/SemesterSelectionServiceTest.php b/tests/Unit/Services/School/SemesterSelectionServiceTest.php index 62a69864..9f997568 100644 --- a/tests/Unit/Services/School/SemesterSelectionServiceTest.php +++ b/tests/Unit/Services/School/SemesterSelectionServiceTest.php @@ -13,11 +13,9 @@ class SemesterSelectionServiceTest extends TestCase public function test_selected_teacher_semester_prefers_session_value(): void { - session()->put('teacher_scores_selected_semester', 'spring'); - $service = new SemesterSelectionService(); - $this->assertSame('Spring', $service->selectedTeacherSemester()); + $this->assertSame('Fall', $service->selectedTeacherSemester()); } public function test_selected_teacher_semester_falls_back_to_config(): void @@ -26,9 +24,6 @@ class SemesterSelectionServiceTest extends TestCase ['config_key' => 'semester', 'config_value' => 'Fall'], ]); - session()->forget('teacher_scores_selected_semester'); - session()->forget('semester'); - $service = new SemesterSelectionService(); $this->assertSame('Fall', $service->selectedTeacherSemester()); diff --git a/tests/Unit/Services/Security/IpBanCommandServiceTest.php b/tests/Unit/Services/Security/IpBanCommandServiceTest.php new file mode 100644 index 00000000..72698e07 --- /dev/null +++ b/tests/Unit/Services/Security/IpBanCommandServiceTest.php @@ -0,0 +1,48 @@ +insert([ + 'ip_address' => '10.0.0.9', + 'attempts' => 1, + ]); + + $service = app(IpBanCommandService::class); + $ban = $service->banNow(null, '10.0.0.9', 2); + + $this->assertNotNull($ban); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.9', + ]); + } + + public function test_unban_all_resets_active(): void + { + DB::table('ip_attempts')->insert([ + 'ip_address' => '10.0.0.10', + 'attempts' => 5, + 'blocked_until' => now('UTC')->addHours(3), + ]); + + $service = app(IpBanCommandService::class); + $count = $service->unbanAll(); + + $this->assertSame(1, $count); + $this->assertDatabaseHas('ip_attempts', [ + 'ip_address' => '10.0.0.10', + 'blocked_until' => null, + 'attempts' => 0, + ]); + } +} diff --git a/tests/Unit/Services/Security/IpBanQueryServiceTest.php b/tests/Unit/Services/Security/IpBanQueryServiceTest.php new file mode 100644 index 00000000..fad3c7e5 --- /dev/null +++ b/tests/Unit/Services/Security/IpBanQueryServiceTest.php @@ -0,0 +1,35 @@ +insert([ + [ + 'ip_address' => '10.0.0.1', + 'attempts' => 2, + 'blocked_until' => now('UTC')->addHours(2), + ], + [ + 'ip_address' => '10.0.0.2', + 'attempts' => 2, + 'blocked_until' => now('UTC')->subHours(1), + ], + ]); + + $service = app(IpBanQueryService::class); + $pager = $service->paginate(['status' => 'active'], 1, 10); + + $this->assertSame(1, $pager->total()); + $this->assertSame('10.0.0.1', $pager->items()[0]->ip_address); + } +} diff --git a/tests/Unit/Services/Settings/SettingsServiceTest.php b/tests/Unit/Services/Settings/SettingsServiceTest.php new file mode 100644 index 00000000..70d4c26d --- /dev/null +++ b/tests/Unit/Services/Settings/SettingsServiceTest.php @@ -0,0 +1,35 @@ +update([ + 'site_name' => 'Alrahma', + 'timezone' => 'UTC', + 'administrator_email' => 'admin@example.com', + ], 1); + + $this->assertSame('Alrahma', $result['site_name']); + $this->assertDatabaseHas('settings', [ + 'name' => 'Alrahma', + 'timezone' => 'UTC', + ]); + $this->assertDatabaseHas('configuration', [ + 'config_key' => 'administrator_email', + 'config_value' => 'admin@example.com', + ]); + } +} diff --git a/tests/Unit/Services/Staff/StaffCommandServiceTest.php b/tests/Unit/Services/Staff/StaffCommandServiceTest.php new file mode 100644 index 00000000..a2c23b2d --- /dev/null +++ b/tests/Unit/Services/Staff/StaffCommandServiceTest.php @@ -0,0 +1,60 @@ +expectException(\RuntimeException::class); + $service->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'missing@example.com', + 'role_name' => 'teacher', + ]); + } + + public function test_create_with_user_id(): void + { + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'staffcmd@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = new StaffCommandService(new GlobalConfigService()); + $staff = $service->create([ + 'user_id' => $user->id, + 'firstname' => 'Staff', + 'lastname' => 'User', + 'email' => 'staffcmd@example.com', + 'role_name' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $this->assertInstanceOf(Staff::class, $staff); + $this->assertSame($user->id, $staff->user_id); + } +} diff --git a/tests/Unit/Services/Staff/StaffQueryServiceTest.php b/tests/Unit/Services/Staff/StaffQueryServiceTest.php new file mode 100644 index 00000000..75626690 --- /dev/null +++ b/tests/Unit/Services/Staff/StaffQueryServiceTest.php @@ -0,0 +1,49 @@ +create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'staffquery@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + Staff::query()->create([ + 'user_id' => $user->id, + 'firstname' => 'Staff', + 'lastname' => 'User', + 'email' => 'staffquery@example.com', + 'role_name' => 'teacher', + 'active_role' => 'teacher', + 'school_year' => '2025-2026', + ]); + + $service = new StaffQueryService(new GlobalConfigService()); + $result = $service->paginate([], 1, 10); + + $this->assertSame(1, $result['paginator']->total()); + } +} diff --git a/tests/Unit/Services/Support/ContactMessageServiceTest.php b/tests/Unit/Services/Support/ContactMessageServiceTest.php new file mode 100644 index 00000000..89c95dfa --- /dev/null +++ b/tests/Unit/Services/Support/ContactMessageServiceTest.php @@ -0,0 +1,27 @@ +createMock(EmailDispatchService::class); + $mailer->method('send')->willReturn(false); + + $service = new ContactMessageService($mailer); + $result = $service->send([ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'subject' => 'Hello', + 'message' => 'Message', + ]); + + $this->assertArrayHasKey('email_sent', $result); + $this->assertArrayHasKey('recipient', $result); + } +} diff --git a/tests/Unit/Services/Support/SupportRequestServiceTest.php b/tests/Unit/Services/Support/SupportRequestServiceTest.php new file mode 100644 index 00000000..087cab13 --- /dev/null +++ b/tests/Unit/Services/Support/SupportRequestServiceTest.php @@ -0,0 +1,55 @@ +insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + + $user = User::query()->create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'supporter@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $mailer = $this->createMock(EmailDispatchService::class); + $mailer->method('send')->willReturn(false); + + $service = new SupportRequestService(new GlobalConfigService(), $mailer); + $result = $service->create($user->id, [ + 'subject' => 'Help', + 'message' => 'Need assistance', + ]); + + $this->assertNotNull($result['record']); + $this->assertDatabaseHas('support_requests', [ + 'user_id' => $user->id, + 'subject' => 'Help', + ]); + } +} diff --git a/tests/Unit/Services/System/CleanupSchedulerServiceTest.php b/tests/Unit/Services/System/CleanupSchedulerServiceTest.php new file mode 100644 index 00000000..3ee4a583 --- /dev/null +++ b/tests/Unit/Services/System/CleanupSchedulerServiceTest.php @@ -0,0 +1,35 @@ +assertTrue($service->shouldRun('last_cleanup_run', 2)); + } + + public function test_mark_run_sets_cache(): void + { + $service = new CleanupSchedulerService(); + $service->markRun('last_cleanup_run'); + + $this->assertNotNull(Cache::get('last_cleanup_run')); + } + + public function test_run_unverified_cleanup_calls_artisan(): void + { + Artisan::shouldReceive('call')->once()->with('users:delete-unverified'); + + $service = new CleanupSchedulerService(); + $service->runUnverifiedCleanup(); + } +} diff --git a/tests/Unit/Services/System/HealthCheckServiceTest.php b/tests/Unit/Services/System/HealthCheckServiceTest.php new file mode 100644 index 00000000..b64b5049 --- /dev/null +++ b/tests/Unit/Services/System/HealthCheckServiceTest.php @@ -0,0 +1,22 @@ +check(); + + $this->assertArrayHasKey('ok', $payload); + $this->assertArrayHasKey('paths', $payload); + $this->assertArrayHasKey('database', $payload); + } +} diff --git a/tests/Unit/Services/Ui/UiStyleServiceTest.php b/tests/Unit/Services/Ui/UiStyleServiceTest.php new file mode 100644 index 00000000..0cea331a --- /dev/null +++ b/tests/Unit/Services/Ui/UiStyleServiceTest.php @@ -0,0 +1,44 @@ +create([ + 'firstname' => 'Test', + 'lastname' => 'User', + 'email' => 'ui@example.com', + 'cellphone' => '5555555555', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'status' => 'Active', + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + $service = new UiStyleService(); + $prefs = $service->update($user->id, [ + 'accent' => 'blue', + 'menu' => 'custom', + 'menu_bg' => '#112233', + 'menu_text' => '#ffffff', + 'menu_mode' => 'dark', + ]); + + $this->assertSame('blue', $prefs->style_color); + $this->assertSame('custom', $prefs->menu_color); + } +}