├── public ├── favicon.ico ├── robots.txt ├── images │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ └── site.webmanifest │ ├── logos │ │ └── vatsca-email.png │ └── control-tower.svg └── .htaccess ├── docs ├── .markdownlintrc ├── _assets │ ├── favicon.ico │ └── logo.png ├── _theme │ └── README.md ├── _includes │ └── exec-in-container.md ├── integrations │ ├── vatsim-connect.md │ ├── handover.md │ ├── vateud.md │ └── vatsim.md ├── why.md ├── concepts │ ├── buddies.md │ └── mentors.md ├── setup │ ├── logo.md │ └── custom.md ├── pyproject.toml ├── background-jobs.md └── stylesheets │ └── extra.css ├── bootstrap └── cache │ └── .gitignore ├── storage ├── logs │ └── .gitignore ├── debugbar │ └── .gitignore ├── app │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── database ├── .gitignore ├── migrations │ ├── 2022_04_23_143945_delete_group_for_examiners.php │ ├── 2022_02_27_105736_delete_solo_table.php │ ├── 2024_07_19_123109_rename_masc_to_facility.php │ ├── 2025_10_10_202944_add_buddy_group.php │ ├── 2024_02_18_164045_add_division_api_setting.php │ ├── 2021_02_25_195620_add_new_groups.php │ ├── 2022_05_08_511291_add_examsheet_setting.php │ ├── 2024_02_13_221808_rename_settings.php │ ├── 2021_07_27_194313_add_new_acitivty_contact_row.php │ ├── 2022_07_30_141538_add_new_settings.php │ ├── 2024_02_03_115826_add_totalhours_setting.php │ ├── 2022_02_10_220022_add_vatsim_booking_column.php │ ├── 2024_03_26_175154_extend_booking_callsign_characters.php │ ├── 2024_07_19_103139_add_area_waiting_time_string.php │ ├── 2024_12_27_131934_add_area_readme_url.php │ ├── 2024_02_06_203837_add_area_feedback_url.php │ ├── 2021_03_20_140940_add_traininginterval_setting.php │ ├── 2024_07_04_124209_add_pretraining_completed_check.php │ ├── 2020_12_01_050819_add_exam_column_to_vatbooks.php │ ├── 2022_05_27_110006_add_user_activity_column.php │ ├── 2022_01_01_211040_vatbook_sources.php │ ├── 2021_04_12_184638_create_positions_freq_column.php │ ├── 2021_08_28_193923_add_solo_req_setting.php │ ├── 2022_07_30_180006_add_user_warning_column.php │ ├── 2022_12_10_122309_add_s1_template_to_areas.php │ ├── 2020_10_10_192309_add_training_interests_expire_columns.php │ ├── 2023_10_05_182438_add_notify_user_setting.php │ ├── 2021_02_21_214807_rename_votes_member_column.php │ ├── 2025_08_17_062853_add_last_atc_inactivity_reminder.php │ ├── 2020_11_19_203252_add_votes_table_vatsca_role_column.php │ ├── 2020_11_21_103028_add_pretraining_column_countries.php │ ├── 2021_05_13_195341_add_acticity_category_enums.php │ ├── 2021_05_24_114034_add_grp_bundle_boolean_to_ratings.php │ ├── 2025_08_17_065024_add_atc_reminder_setting.php │ ├── 2022_05_14_081018_delete_passport_tables.php │ ├── 2023_10_01_200047_add_cronjob_datetime.php │ ├── 2020_05_21_140316_create_notifications_table.php │ ├── 2020_05_30_175009_create_failed_jobs_table.php │ ├── 2023_02_12_114913_add_telemetry_setting.php │ ├── 2020_03_08_200040_create_training_object_attachments_table.php │ ├── 2020_06_10_190641_create_vote_options_table.php │ ├── 2022_05_14_000001_create_api_tokens_table.php │ ├── 2020_06_10_190421_create_votes_table.php │ ├── 2021_03_20_130343_add_new_estimate_columns.php │ ├── 2024_02_13_172836_rename_position_area_to_id_col.php │ ├── 2022_11_18_210255_add_rating_req_hours.php │ ├── 2023_10_01_194017_add_allow_inactive_controlling_setting.php │ ├── 2020_03_08_200050_create_rating_user.php │ ├── 2020_06_10_190844_create_user_vote_table.php │ ├── 2024_02_22_175840_add_subject_training_rating_id_column.php │ ├── 2020_05_21_142025_create_jobs_table.php │ ├── 2021_02_25_200220_create_permissions_table.php │ ├── 2021_05_13_154011_add_workmail_to_users.php │ ├── 2023_10_02_132846_add_createdby_training_column.php │ ├── 2020_03_08_200080_create_rating_training_table.php │ ├── 2020_09_11_193706_create_one_time_links_table.php │ ├── 2022_05_15_095715_add_endorsement_log_type.php │ ├── 2024_02_04_113019_delete_template_s1_positions.php │ ├── 2020_10_03_151333_create_training_interests.php │ ├── 2022_11_19_142147_remove_vatbook_add_booking.php │ ├── 2024_07_17_162745_add_required_endorsement_id_to_positions.php │ ├── 2020_04_15_193948_create_files_table.php │ ├── 2020_03_08_200060_create_training_mentor_table.php │ ├── 2020_03_09_204817_create_solo_endorsements_table.php │ ├── 2023_08_26_184643_fix_utc_defaults.php │ ├── 2024_07_08_194628_add_trainingactivity_pretraining_type.php │ ├── 2021_02_25_075635_create_atc_activity_table.php │ ├── 2020_08_24_191843_create_activity_logs.php │ ├── 2020_03_08_000010_create_groups_table.php │ ├── 2024_02_04_111709_remove_s1_endorsements.php │ ├── 2021_01_29_001536_add_mae_column_to_positions.php │ └── 2021_03_02_202644_change_country_to_areas.php └── factories │ ├── AreaFactory.php │ ├── EndorsementFactory.php │ ├── TrainingInterestFactory.php │ ├── RatingFactory.php │ ├── TrainingExaminationFactory.php │ ├── BookingFactory.php │ └── TrainingReportFactory.php ├── .release-please-manifest.json ├── resources ├── views │ ├── vendor │ │ └── mail │ │ │ ├── text │ │ │ ├── footer.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── promotion.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── table.blade.php │ │ │ ├── button.blade.php │ │ │ ├── header.blade.php │ │ │ ├── promotion │ │ │ │ └── button.blade.php │ │ │ ├── layout.blade.php │ │ │ └── message.blade.php │ │ │ └── html │ │ │ ├── table.blade.php │ │ │ ├── header.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── promotion.blade.php │ │ │ ├── footer.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── promotion │ │ │ └── button.blade.php │ │ │ ├── button.blade.php │ │ │ └── message.blade.php │ ├── errors │ │ ├── 404.blade.php │ │ ├── 401.blade.php │ │ ├── 419.blade.php │ │ ├── 500.blade.php │ │ ├── 429.blade.php │ │ ├── 503.blade.php │ │ ├── 403.blade.php │ │ └── maintenance.blade.php │ ├── mail │ │ ├── endorsement.blade.php │ │ ├── staffnotice.blade.php │ │ ├── warning.blade.php │ │ ├── tasks.blade.php │ │ ├── mentornotice.blade.php │ │ └── training.blade.php │ └── scripts │ │ ├── tooltips.blade.php │ │ └── zulutime.blade.php ├── sass │ ├── easymde.scss │ ├── flatpickr.scss │ ├── _navs.scss │ ├── _utilities.scss │ ├── _dropdowns.scss │ ├── bootstrap-table.scss │ ├── _buttons.scss │ ├── utilities │ │ ├── _border.scss │ │ ├── _background.scss │ │ ├── _animation.scss │ │ └── _text.scss │ ├── navs │ │ └── _global.scss │ └── app.scss ├── js │ ├── flatpickr.js │ ├── easymde.js │ ├── vue.js │ ├── chart.js │ ├── bootstrap-table.js │ └── app.js └── images │ └── front.jpg ├── .phpactor.json ├── .gitattributes ├── app ├── Models │ ├── ManagementReport.php │ ├── VoteOption.php │ ├── Vote.php │ ├── ActivityLog.php │ ├── Sweatbook.php │ ├── TrainingObject.php │ ├── ApiKey.php │ ├── TrainingInterest.php │ ├── TrainingReport.php │ ├── TrainingExamination.php │ ├── AtcActivity.php │ ├── TrainingObjectAttachment.php │ ├── Position.php │ ├── Booking.php │ ├── Group.php │ ├── Feedback.php │ ├── TrainingActivity.php │ ├── Rating.php │ ├── Endorsement.php │ ├── File.php │ ├── Area.php │ └── Task.php ├── Helpers │ ├── TaskStatus.php │ ├── TrainingStatus.php │ ├── VatsimRating.php │ └── Vatsim.php ├── Facades │ └── DivisionApi.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── TrimStrings.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── Authenticate.php │ │ ├── VerifyCsrfToken.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── UserActive.php │ │ ├── SuspendedUser.php │ │ └── TrustProxies.php │ └── Controllers │ │ ├── Controller.php │ │ ├── API │ │ └── PositionController.php │ │ ├── FrontPageController.php │ │ ├── MentorController.php │ │ └── RosterController.php ├── Policies │ ├── ActivityLogPolicy.php │ ├── SettingPolicy.php │ ├── NotificationPolicy.php │ ├── TaskPolicy.php │ ├── SweatbookPolicy.php │ ├── TrainingActivityPolicy.php │ └── TrainingObjectAttachmentPolicy.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── CarbonServiceProvider.php │ └── DivisionApiServiceProvider.php ├── Exceptions │ ├── VatsimAPIException.php │ ├── InvalidTrainingActivityType.php │ ├── PolicyMissingException.php │ ├── PolicyMethodMissingException.php │ └── Handler.php ├── Console │ └── Commands │ │ ├── KeyGet.php │ │ ├── CleanSweatbooks.php │ │ ├── UpdateBookings.php │ │ └── CleanVotes.php ├── Rules │ ├── InactivityReminderHours.php │ └── ValidTaskType.php ├── Mail │ ├── StaffNoticeMail.php │ ├── MentorNoticeMail.php │ └── TaskMail.php └── Tasks │ └── Types │ ├── Custom.php │ └── SoloEndorsement.php ├── docker-compose.dev.yaml ├── .editorconfig ├── phpstan.neon ├── .dockerignore ├── pint.json ├── release-please-config.json ├── tests ├── CreatesApplication.php ├── TestCase.php └── Feature │ ├── FrontpageTest.php │ └── ReportControllerTest.php ├── .github ├── renovate.json ├── workflows │ ├── linting.yaml │ └── release-please.yaml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── actions │ └── setup-dependencies │ └── action.yml ├── routes ├── channels.php └── console.php ├── container ├── migrate.sh ├── install-npm.sh ├── example-prod.env ├── theme │ └── build.sh ├── entrypoint.sh └── configs │ └── 000-default.conf ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── server.php ├── docker-compose.dev.full.yaml ├── .gitignore ├── package.json ├── docker-compose.yaml ├── .env.ci ├── config └── view.php ├── phpunit.xml └── .pre-commit-config.yaml /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.markdownlintrc: -------------------------------------------------------------------------------- 1 | eee 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite-journal 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "6.4.2" 3 | } 4 | -------------------------------------------------------------------------------- /docs/_assets/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../public/images/favicon/favicon.ico -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/promotion.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /docs/_theme/README.md: -------------------------------------------------------------------------------- 1 | # Theme Overrides 2 | 3 | Theme overrides go here. 4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/sass/easymde.scss: -------------------------------------------------------------------------------- 1 | 2 | // Imports 3 | @import "easymde/dist/easymde.min.css"; 4 | -------------------------------------------------------------------------------- /resources/sass/flatpickr.scss: -------------------------------------------------------------------------------- 1 | 2 | // Imports 3 | @import "flatpickr/dist/flatpickr.css"; 4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/promotion/button.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/js/flatpickr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import flatpickr from "flatpickr"; -------------------------------------------------------------------------------- /docs/_assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/docs/_assets/logo.png -------------------------------------------------------------------------------- /resources/images/front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/resources/images/front.jpg -------------------------------------------------------------------------------- /resources/sass/_navs.scss: -------------------------------------------------------------------------------- 1 | @import "navs/global.scss"; 2 | @import "navs/topbar.scss"; 3 | @import "navs/sidebar.scss"; 4 | -------------------------------------------------------------------------------- /.phpactor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "/usr/lib/phpactor/phpactor.schema.json", 3 | "language_server_phpstan.enabled": true 4 | } -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /public/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/favicon.ico -------------------------------------------------------------------------------- /public/images/logos/vatsca-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/logos/vatsca-email.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /public/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /public/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $slot }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vatsim-Scandinavia/controlcenter/HEAD/public/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | maintenance.php -------------------------------------------------------------------------------- /resources/sass/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "utilities/animation.scss"; 2 | @import "utilities/background.scss"; 3 | @import "utilities/text.scss"; 4 | @import "utilities/border.scss"; 5 | -------------------------------------------------------------------------------- /resources/views/errors/404.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Not Found')) 4 | @section('code', '404') 5 | @section('message', __('Not Found')) 6 | -------------------------------------------------------------------------------- /resources/js/easymde.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import easymde from "easymde"; 6 | 7 | /** 8 | * Insert global variables 9 | */ 10 | 11 | window.EasyMDE= easymde; -------------------------------------------------------------------------------- /resources/js/vue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import { createApp } from 'vue'; 6 | 7 | /** 8 | * Insert global variables 9 | */ 10 | 11 | window.createApp = createApp; -------------------------------------------------------------------------------- /resources/sass/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | 2 | // Utility class to hide arrow from dropdown 3 | .dropdown.no-arrow { 4 | .dropdown-toggle::after { 5 | display: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/views/errors/401.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Unauthorized')) 4 | @section('code', '401') 5 | @section('message', __('Unauthorized')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/419.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Page Expired')) 4 | @section('code', '419') 5 | @section('message', __('Page Expired')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Server Error')) 4 | @section('code', '500') 5 | @section('message', __('Server Error')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/429.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Too Many Requests')) 4 | @section('code', '429') 5 | @section('message', __('Too Many Requests')) 6 | -------------------------------------------------------------------------------- /app/Models/ManagementReport.php: -------------------------------------------------------------------------------- 1 | getMessage() ?: 'Forbidden')) 6 | -------------------------------------------------------------------------------- /resources/sass/bootstrap-table.scss: -------------------------------------------------------------------------------- 1 | 2 | // Bootstrap Table 3 | @import "bootstrap-table/dist/bootstrap-table.css"; 4 | @import "bootstrap-table/dist/extensions/filter-control/bootstrap-table-filter-control.css"; 5 | -------------------------------------------------------------------------------- /resources/views/errors/maintenance.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Service Unavailable')) 4 | @section('code', '503') 5 | @section('message', __('We will be back shortly. Updating.')) 6 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header) !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer) !!} 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/Helpers/TaskStatus.php: -------------------------------------------------------------------------------- 1 | .btn { 2 | padding: 0.15rem 0.45rem; 3 | font-size: 0.775rem; 4 | line-height: 1rem; 5 | border-radius: $border-radius; 6 | } 7 | 8 | .accordion button{ 9 | text-align: left; 10 | } -------------------------------------------------------------------------------- /docker-compose.dev.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | container_name: controlcenter 4 | build: . 5 | ports: 6 | - 5010:80 7 | - 5011:443 8 | extra_hosts: 9 | - "vatsca.local:host-gateway" 10 | volumes: 11 | - ./:/app 12 | -------------------------------------------------------------------------------- /resources/sass/utilities/_border.scss: -------------------------------------------------------------------------------- 1 | @each $color, $value in $theme-colors { 2 | @each $position in [ "left", "bottom" ] { 3 | .border-#{$position}-#{$color} { 4 | border-#{$position}: 0.25rem solid $value !important; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/promotion.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | - vendor/nesbot/carbon/extension.neon 4 | 5 | parameters: 6 | # Level 9 is the highest level 7 | level: 3 8 | # Where are our files? They're right in app/ 9 | paths: 10 | - app/ 11 | 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker itself and relevant files 2 | Dockerfile 3 | .env 4 | # No, no node modules. 5 | node_modules 6 | # No tests either, you're damn straight. 7 | tests 8 | # Don't care about the devcontainer 9 | .devcontainer 10 | # Don't care about docs either, they're online. 11 | docs 12 | -------------------------------------------------------------------------------- /app/Facades/DivisionApi.php: -------------------------------------------------------------------------------- 1 | belongsTo(Vote::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/Models/Vote.php: -------------------------------------------------------------------------------- 1 | hasMany(VoteOption::class); 12 | } 13 | 14 | public function user() 15 | { 16 | return $this->belongsToMany(User::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/mail/endorsement.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{-- Greeting --}} 4 | # Hello {{ $firstName }}, 5 | 6 | {{-- Intro Lines --}} 7 | @foreach ($textLines as $line) 8 | {{ $line }} 9 | 10 | @endforeach 11 | 12 | {{-- Subcopy --}} 13 | @slot('subcopy') 14 | For questions regarding your endorsement, contact your mentor. 15 | @endslot 16 | 17 | @endcomponent -------------------------------------------------------------------------------- /resources/js/chart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import Chart from 'chart.js/auto'; 6 | import * as helpers from 'chart.js/helpers'; 7 | import 'chartjs-adapter-moment'; 8 | import autocolors from 'chartjs-plugin-autocolors'; 9 | 10 | 11 | /** 12 | * Insert global variables 13 | */ 14 | 15 | Chart.helpers = helpers; 16 | Chart.register(autocolors); 17 | window.Chart = Chart; 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 'datetime', 13 | ]; 14 | 15 | public function user() 16 | { 17 | return $this->belongsTo(User::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Sweatbook.php: -------------------------------------------------------------------------------- 1 | hasOne(Position::class, 'id', 'position_id'); 12 | } 13 | 14 | public function user() 15 | { 16 | return $this->belongsTo(User::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/mail/staffnotice.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{-- Greeting --}} 4 | # Hello, 5 | 6 | {{-- Intro Lines --}} 7 | @foreach ($textLines as $line) 8 | {{ $line }} 9 | 10 | @endforeach 11 | 12 | {{-- Subcopy --}} 13 | @slot('subcopy') 14 | This is an automatically generated notice. If you think you received this by error, contact the staff. 15 | @endslot 16 | 17 | @endcomponent -------------------------------------------------------------------------------- /resources/views/scripts/tooltips.blade.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "concat_space": { 5 | "spacing": "one" 6 | }, 7 | "no_useless_concat_operator": true, 8 | "simple_to_complex_string_variable": true, 9 | "nullable_type_declaration_for_default_null_value": true, 10 | "new_with_parentheses": false, 11 | "single_line_comment_spacing": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/views/mail/warning.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{-- Greeting --}} 4 | # Hello {{ $firstName }}, 5 | 6 | {{-- Intro Lines --}} 7 | @foreach ($textLines as $line) 8 | {{ $line }} 9 | 10 | @endforeach 11 | 12 | {{-- Subcopy --}} 13 | @slot('subcopy') 14 | This is an automatically generated notice. If you think you received this by error, contact the staff. 15 | @endslot 16 | 17 | @endcomponent -------------------------------------------------------------------------------- /resources/js/bootstrap-table.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import bootstrapTable from 'bootstrap-table'; 6 | 7 | import bootstrapTableEnUS from 'bootstrap-table/dist/locale/bootstrap-table-en-US.js'; 8 | import bootstrapTableFilterControl from 'bootstrap-table/dist/extensions/filter-control/bootstrap-table-filter-control.js'; 9 | import bootstrapTableCookie from 'bootstrap-table/dist/extensions/cookie/bootstrap-table-cookie.js'; -------------------------------------------------------------------------------- /app/Models/TrainingObject.php: -------------------------------------------------------------------------------- 1 | morphMany(TrainingObjectAttachment::class, 'object'); 12 | } 13 | 14 | public function training() 15 | { 16 | return $this->belongsTo(Training::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Helpers/TrainingStatus.php: -------------------------------------------------------------------------------- 1 | isAdmin(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "release-type": "php", 3 | "changelog-path": "CHANGELOG.md", 4 | "draft": false, 5 | "extra-files": [ 6 | "config/app.php" 7 | ], 8 | "packages": { 9 | ".": { 10 | "bump-minor-pre-major": false, 11 | "bump-patch-for-minor-pre-major": false, 12 | "prerelease": false 13 | } 14 | }, 15 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 16 | } 17 | -------------------------------------------------------------------------------- /docs/_includes/exec-in-container.md: -------------------------------------------------------------------------------- 1 | !!! tip "Executing commands in containers" 2 | If you're using a container you need to use execute the command *inside* of the container. 3 | For example, if you're using Docker you must prefix the command with `docker exec` and specify the container name. Here's an example where we assume that the container is `control-center`: 4 | 5 | ```sh 6 | docker exec -it --user www-data control-center COMMAND [ARGUMENTS...] 7 | ``` 8 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | $actionUrl, 'color' => 'primary']) 13 | See tasks 14 | @endcomponent 15 | 16 | {{-- Subcopy --}} 17 | @slot('subcopy') 18 | This is an automatically generated notice. 19 | @endslot 20 | 21 | @endcomponent -------------------------------------------------------------------------------- /app/Http/Controllers/API/PositionController.php: -------------------------------------------------------------------------------- 1 | get(); 16 | 17 | return response()->json(['data' => $positions], 200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/integrations/vatsim-connect.md: -------------------------------------------------------------------------------- 1 | # VATSIM Connect 2 | 3 | VATSIM Connect is an OAuth2-based identity provider that allows for VATSIM-related sites to authenticate and fetch information about VATSIM members. 4 | 5 | !!! tip "Use VATSIM Connect with Control Center" 6 | By default, we recommend using [VATSIM Connect][vatsim-connect] to authenticate users. 7 | See [the configuration manual](../configuration/index.md) to get started. 8 | 9 | [vatsim-connect]: https://vatsim.dev/api/connect-api/ 10 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | Handling training requests and keeping a VATSIM Area Control Centre (or multiple!) active takes a lot of work. 2 | Control Center is built on the idea of reducing the amount of work, while also making the experience faster and enjoyable. 3 | 4 | - Handle training flow of virtual Air Traffic Controllers (ATC) 5 | - Set and maintain custom activity targets for ATCs, and mark ATCs that fall under the target as inactive 6 | - Clean up members, trainings and exams in the event of account closure 7 | - GDPR compliant 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "configMigration": true, 4 | "extends": [ 5 | "config:recommended", 6 | ":pinDevDependencies", 7 | ":semanticCommitTypeAll(chore)" 8 | ], 9 | "packageRules": [ 10 | { 11 | "matchDepTypes": [ 12 | "dependencies" 13 | ], 14 | "major": { 15 | "semanticCommitType": "fix" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/TrainingInterest.php: -------------------------------------------------------------------------------- 1 | 'datetime', 16 | 'confirmed_at' => 'datetime', 17 | ]; 18 | 19 | public function training() 20 | { 21 | return $this->belongsTo(Training::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/concepts/buddies.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/account-group 3 | --- 4 | 5 | # Buddies 6 | 7 | *Buddies* are active ATC members in the vACC who acts like a study buddy. Like mentors they support the student during their pre-training by answering questions they may have. 8 | 9 | ## Limited access 10 | 11 | The *Buddy* role provides the buddy limited access to write training reports for a given student using a one-time link provided by a mentor or moderator. The buddy have no access to view other training reports but the ones written by themselves. -------------------------------------------------------------------------------- /.github/workflows/linting.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | lint-formatting: 10 | name: Lint formatting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Control Center 14 | uses: actions/checkout@v5 15 | with: 16 | path: . 17 | 18 | - name: Setup dependenices 19 | uses: ./.github/actions/setup-dependencies 20 | with: 21 | path: . 22 | 23 | - name: Check for incorrect formatting 24 | run: ./vendor/bin/pint --test 25 | -------------------------------------------------------------------------------- /app/Exceptions/VatsimAPIException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 |
7 | {{ $slot }} 8 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/Exceptions/InvalidTrainingActivityType.php: -------------------------------------------------------------------------------- 1 | where('id', 4)->delete(); 15 | } 16 | 17 | /** 18 | * Reverse the migrations. 19 | * 20 | * @return void 21 | */ 22 | public function down() 23 | { 24 | // Breaking change 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /app/Exceptions/PolicyMissingException.php: -------------------------------------------------------------------------------- 1 | code = $code ?: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/sass/utilities/_background.scss: -------------------------------------------------------------------------------- 1 | // Background Gradient Utilities 2 | 3 | @each $color, $value in $theme-colors { 4 | .bg-gradient-#{$color} { 5 | background-color: $value; 6 | background-image: linear-gradient(180deg, $value 10%, darken($value, 15%) 100%); 7 | background-size: cover; 8 | } 9 | } 10 | 11 | // Grayscale Background Utilities 12 | 13 | @each $level, $value in $grays { 14 | .bg-gray-#{$level} { 15 | background-color: $value !important; 16 | } 17 | } 18 | 19 | .bg-lightorange { 20 | background-color: #fff6e9; 21 | } 22 | -------------------------------------------------------------------------------- /public/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Control Center", 3 | "short_name": "Control Center", 4 | "icons": [ 5 | { 6 | "src": "/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/mail/mentornotice.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{-- Greeting --}} 4 | # Hello, 5 | 6 | {{-- Intro Lines --}} 7 | @foreach ($textLines as $line) 8 | {{ $line }} 9 | 10 | @endforeach 11 | 12 | {{-- Action Button --}} 13 | @if($actionUrl !== null) 14 | @component('mail::button', ['url' => $actionUrl, 'color' => 'primary']) 15 | {{ $actionText }} 16 | @endcomponent 17 | @endif 18 | 19 | {{-- Subcopy --}} 20 | @slot('subcopy') 21 | This is an automatically generated notice. If you think you received this by error, contact the staff. 22 | @endslot 23 | 24 | @endcomponent -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | code = $code ?: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/sass/navs/_global.scss: -------------------------------------------------------------------------------- 1 | // Login front landing page 2 | a.version-front{ 3 | display: block; 4 | 5 | text-align: center; 6 | text-decoration: none; 7 | font-size: 0.625rem; 8 | color: #8da0ab; 9 | 10 | &:hover{ 11 | text-decoration: underline; 12 | } 13 | 14 | } 15 | 16 | // Navigation triggered fixed body when responsive menu is open 17 | .fixed-body{ 18 | position: initial; 19 | @include media-breakpoint-down(lg) { 20 | position: fixed; 21 | } 22 | } 23 | 24 | #newreqrow, #newmentorrow, #pretrainingrow{ 25 | display: none; 26 | } -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /container/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a manual script for entering maintenance mode and 3 | # migrating the database during an upgrade of Control Center. 4 | # 5 | # WARNING: 6 | # This script does not perform any validation steps or 7 | # otherwise ensures that it is doing the right thing at the 8 | # right time. 9 | 10 | # Turn maintenance mode on, unless it's the initial run 11 | php artisan down --render="errors.maintenance" 12 | 13 | # Artisan magic 14 | php artisan migrate 15 | 16 | # Clear all of the cache 17 | php artisan optimize:clear 18 | 19 | # Turn maintenance mode off 20 | php artisan up 21 | -------------------------------------------------------------------------------- /database/migrations/2022_02_27_105736_delete_solo_table.php: -------------------------------------------------------------------------------- 1 | where('type', 'MASC')->update(['type' => 'FACILITY']); 13 | } 14 | 15 | /** 16 | * Reverse the migrations. 17 | */ 18 | public function down(): void 19 | { 20 | DB::table('endorsements')->where('type', 'FACILITY')->update(['type' => 'MASC']); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $uri = urldecode( 9 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 10 | ); 11 | 12 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 13 | // built-in PHP web server. This provides a convenient way to test a Laravel 14 | // application without having installed a "real" web server software here. 15 | if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) { 16 | return false; 17 | } 18 | 19 | require_once __DIR__ . '/public/index.php'; 20 | -------------------------------------------------------------------------------- /container/install-npm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting theme building process..." 4 | 5 | # Install 6 | apt update 7 | apt install -y ca-certificates curl gnupg 8 | mkdir -p /etc/apt/keyrings 9 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 10 | 11 | NODE_MAJOR=24 12 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list 13 | 14 | apt update 15 | apt install nodejs -y 16 | 17 | # Build config 18 | npm config set cache /tmp --global 19 | 20 | echo "Done!" -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /app/Models/TrainingReport.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | 'report_date' => 'datetime', 16 | ]; 17 | 18 | public function path() 19 | { 20 | return route('training.report.edit', ['report' => $this->id]); 21 | } 22 | 23 | public function author() 24 | { 25 | return $this->belongsTo(User::class, 'written_by_id'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/views/mail/training.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{-- Greeting --}} 4 | # Hello {{ $firstName }}, 5 | 6 | {{-- Intro Lines --}} 7 | @foreach ($textLines as $line) 8 | {{ $line }} 9 | 10 | @endforeach 11 | 12 | {{-- Action Button --}} 13 | @isset($actionUrl) 14 | @component('mail::button', ['url' => $actionUrl, 'color' => $actionColor]) 15 | {{ $actionText }} 16 | @endcomponent 17 | @endisset 18 | 19 | {{-- Subcopy --}} 20 | @isset($contactMail) 21 | @slot('subcopy') 22 | For questions regarding your training, contact [{{ $contactMail }}](mailto:{{ $contactMail }}) 23 | @endslot 24 | @endisset 25 | 26 | @endcomponent -------------------------------------------------------------------------------- /app/Http/Controllers/FrontPageController.php: -------------------------------------------------------------------------------- 1 | intended(route('dashboard')); 21 | } 22 | 23 | return view('front'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/concepts/mentors.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/human-male-board 3 | --- 4 | 5 | # Mentors 6 | 7 | *Mentors* are active ATC members in the vACC who help teach division members and visiting controllers the necessary syllabus, theoretical and practical, relevant to the vACC and rating [they have applied for][training]. 8 | 9 | ## Mentees 10 | 11 | There are no limitations to the number of active trainings a mentor can have. There's however a hard limit of having a mentor assigned 12 months before they will automatically be removed. This is to avoid mentors having access to personal data over long time, and most trainings are assumed to be finished within this. -------------------------------------------------------------------------------- /app/Models/TrainingExamination.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | 'examination_date' => 'datetime', 16 | ]; 17 | 18 | public function position() 19 | { 20 | return $this->hasOne(Position::class, 'id', 'position_id'); 21 | } 22 | 23 | public function examiner() 24 | { 25 | return $this->hasOne(User::class, 'id', 'examiner_id'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2025_10_10_202944_add_buddy_group.php: -------------------------------------------------------------------------------- 1 | insert([ 13 | ['id' => 4, 'name' => 'Buddy', 'description' => 'Access meant for buddies, to give them buddy-related functionality.'], 14 | ]); 15 | } 16 | 17 | /** 18 | * Reverse the migrations. 19 | */ 20 | public function down(): void 21 | { 22 | DB::table('groups')->where('id', 4)->delete(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /docker-compose.dev.full.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | container_name: controlcenter 4 | build: . 5 | ports: 6 | - 8080:80 7 | - 8443:443 8 | extra_hosts: 9 | - "vatsca.local:host-gateway" 10 | volumes: 11 | - ./:/app 12 | db: 13 | image: docker.io/library/mariadb:12 14 | ports: 15 | - 3306:3306 16 | environment: 17 | MARIADB_DATABASE: controlcenter 18 | MARIADB_ROOT_PASSWORD: root 19 | redis: 20 | image: docker.io/library/redis:8.2-alpine 21 | restart: always 22 | ports: 23 | - 6379:6379 24 | volumes: 25 | - cache:/data 26 | volumes: 27 | cache: 28 | driver: local -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .htaccess 4 | .idea/ 5 | .vscode/ 6 | /_ide_helper.php 7 | /.phpstorm.meta.php 8 | /node_modules 9 | /public/build 10 | /public/hot 11 | /public/storage 12 | /storage/*.key 13 | /vendor 14 | apache/* 15 | Homestead.json 16 | Homestead.yaml 17 | npm-debug.log 18 | public/css/* 19 | public/fonts/vendor/* 20 | public/images/logos/* 21 | public/js/* 22 | resources/sass/_env.scss 23 | yarn-error.log 24 | 25 | # phpunit and results 26 | *.phpunit.cache 27 | *.phpunit.result.cache 28 | coverage/* 29 | 30 | # composer and mise helpers 31 | .composer-installed 32 | 33 | # mkdocs 34 | docs_site/ 35 | 36 | # php type generation 37 | _ide_helper_models.php 38 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow is used to release control-center. It runs an action that 3 | # determines whether it needs to create a release or create a PR which 4 | # contains a list of changes for the next release. 5 | name: Release (release-please) 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | - ci/release-please 12 | 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | jobs: 20 | release-please: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: googleapis/release-please-action@v4 24 | with: 25 | token: ${{ secrets.VATSCA_BOT_TOKEN }} 26 | -------------------------------------------------------------------------------- /database/factories/AreaFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->country(), 26 | 'contact' => $this->faker->email(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2024_02_18_164045_add_division_api_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 13 | ['key' => 'divisionApiEnabled', 'value' => 0], 14 | ]); 15 | } 16 | 17 | /** 18 | * Reverse the migrations. 19 | * 20 | * @return void 21 | */ 22 | public function down() 23 | { 24 | DB::raw('DELETE FROM ' . Config::get('settings.table') . ' WHERE key = `divisionApiEnabled`'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /database/migrations/2021_02_25_195620_add_new_groups.php: -------------------------------------------------------------------------------- 1 | insert([ 15 | ['id' => 4, 'name' => 'Examinator', 'description' => 'Access to examine other students.'], 16 | ]); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | * 22 | * @return void 23 | */ 24 | public function down() 25 | { 26 | DB::table('groups')->where('id', 4)->delete(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/AtcActivity.php: -------------------------------------------------------------------------------- 1 | 'datetime', 13 | 'updated_at' => 'datetime', 14 | 'atc_active' => 'boolean', 15 | 'start_of_grace_period' => 'datetime', 16 | ]; 17 | 18 | public function user() 19 | { 20 | return $this->belongsTo(User::class); 21 | } 22 | 23 | public function area() 24 | { 25 | return $this->belongsTo(Area::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Models/TrainingObjectAttachment.php: -------------------------------------------------------------------------------- 1 | 'boolean', 16 | ]; 17 | 18 | protected $keyType = 'string'; 19 | 20 | public $incrementing = false; 21 | 22 | public function object() 23 | { 24 | return $this->morphTo(); 25 | } 26 | 27 | public function file() 28 | { 29 | return $this->hasOne(File::class, 'id', 'file_id'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2022_05_08_511291_add_examsheet_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 16 | ['key' => 'trainingExamTemplate', 'value' => ''], 17 | ]); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | * 23 | * @return void 24 | */ 25 | public function down() 26 | { 27 | // Breaking change 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2024_02_13_221808_rename_settings.php: -------------------------------------------------------------------------------- 1 | where('key', 'atcActivityAllowTotalHours')->update(['key' => 'atcActivityBasedOnTotalHours']); 14 | } 15 | 16 | /** 17 | * Reverse the migrations. 18 | */ 19 | public function down(): void 20 | { 21 | DB::table('settings')->where('key', 'atcActivityBasedOnTotalHours')->update(['key' => 'atcActivityAllowTotalHours']); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 20 | return redirect()->route('dashboard'); 21 | } 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Models/Position.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Booking::class, 'id', 'position_id'); 14 | } 15 | 16 | public function area() 17 | { 18 | return $this->belongsTo(Area::class); 19 | } 20 | 21 | public function endorsements() 22 | { 23 | return $this->belongsToMany(Endorsement::class); 24 | } 25 | 26 | public function requiredRating() 27 | { 28 | return $this->belongsTo(Rating::class, 'required_facility_rating_id'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2021_07_27_194313_add_new_acitivty_contact_row.php: -------------------------------------------------------------------------------- 1 | insert([ 15 | ['key' => 'atcActivityContact', 'value' => 'local training staff'], 16 | ]); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | * 22 | * @return void 23 | */ 24 | public function down() 25 | { 26 | DB::table('settings')->where('key', 'atcActivityContact')->delete(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /database/migrations/2022_07_30_141538_add_new_settings.php: -------------------------------------------------------------------------------- 1 | insert([ 15 | ['key' => 'atcActivityNotifyInactive', 'value' => 1], 16 | ]); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | * 22 | * @return void 23 | */ 24 | public function down() 25 | { 26 | DB::raw('DELETE FROM ' . Config::get('settings.table') . ' WHERE key = `atcActivityNotifyInactive`'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /app/Models/Booking.php: -------------------------------------------------------------------------------- 1 | hasOne(Position::class, 'id', 'position_id'); 22 | } 23 | 24 | public function user() 25 | { 26 | return $this->belongsTo(User::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Group.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class, 'permissions')->withPivot('area_id')->withTimestamps(); 14 | } 15 | 16 | public static function admins() 17 | { 18 | return static::where('id', 1)->first()->users; 19 | } 20 | 21 | public static function moderators() 22 | { 23 | return static::where('id', 2)->first()->users; 24 | } 25 | 26 | public static function mentors() 27 | { 28 | return static::where('id', 3)->first()->users; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/setup/logo.md: -------------------------------------------------------------------------------- 1 | # Changing the logo 2 | 3 | If you'd like to change the division logo to your division's logo, you must put the logo inside of Control Center. You can do this with binding or by creating your own variant of Control Center. 4 | 5 | === "Binding" 6 | 7 | Bind your logo image files to `public/images/logos`. 8 | 9 | === "Custom container" 10 | 11 | [Create a variant of Control Center](custom.md) and add your logos to `public/images/logos`. 12 | 13 | | Variable | Default value | Explanation | 14 | | ------- | --- | --- | 15 | | APP_LOGO | vatsca.svg | The logo of your division, located in `public/images/logos` | 16 | | APP_LOGO_MAIL | vatsca-email.png | The logo of your division, located in `public/images/logos` | 17 | -------------------------------------------------------------------------------- /database/migrations/2024_02_03_115826_add_totalhours_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 15 | ['key' => 'atcActivityBasedOnTotalHours', 'value' => 1], 16 | ]); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | * 22 | * @return void 23 | */ 24 | public function down() 25 | { 26 | DB::raw('DELETE FROM ' . Config::get('settings.table') . ' WHERE key = `atcActivityBasedOnTotalHours`'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /docs/integrations/handover.md: -------------------------------------------------------------------------------- 1 | # Handover 2 | 3 | *Handover* is another project maintained by VATSIM Scandinavia. 4 | It is an alternative to [VATSIM Connect](./vatsim-connect.md) which provides you with the ability to 5 | 6 | - [x] **GDPR Complicance**: Centralise user authentication in one location to easily scrub systems of user data. 7 | - [x] **Authorization**: Use Handover to authorize or reject user requests to other websites. 8 | - [x] **OAuth2 compatible**: Integrate Handover instead of VATSIM Connect directly with other services for your division. 9 | 10 | !!! tip "See :material-github: GitHub for more" 11 | Find out more [about VATSCA's Handover project](https://github.com/vatsim-scandinavia/handover) by visiting its repository. 12 | -------------------------------------------------------------------------------- /docs/integrations/vateud.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | We provide a integration for the VATSIM Europe Division (VATEUD). This calls the VATEUD Core API providing sync for the following functions: 4 | 5 | - Mentors 6 | - Examiners 7 | - Tier 1/2 endorsements 8 | - Solo endorsements 9 | - Rating upgrades (via Tasks) 10 | - Theory test requests (via Tasks) 11 | 12 | !!! info 13 | Control Center works as the source of truth, so it's important that existing data in VATEUD Core is in sync prior to enabling this integration. Sync all your members, endorsements and such within the VATEUD Core portal manually. 14 | 15 | !!! tip "Activate in settings" 16 | Remember turning on the Division API setting in Administration > Settings -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $policies = [ 15 | 'anlutro\LaravelSettings\Facade' => 'App\Policies\SettingPolicy', 16 | 'Illuminate\Notifications\Notification' => 'App\Policies\NotificationPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | */ 22 | public function boot(): void 23 | { 24 | // 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2022_02_10_220022_add_vatsim_booking_column.php: -------------------------------------------------------------------------------- 1 | integer('vatsim_booking')->nullable()->after('eu_id'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withoutVite(); 18 | } 19 | 20 | /** 21 | * Configures the default list of transactioned connections 22 | **/ 23 | protected function connectionsToTransact() 24 | { 25 | return [ 26 | config('database.default'), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/UserActive.php: -------------------------------------------------------------------------------- 1 | last_activity = Carbon::now(); 22 | $user->save(); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Models/Feedback.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class, 'submitter_user_id'); 19 | } 20 | 21 | public function referenceUser() 22 | { 23 | return $this->belongsTo(User::class, 'reference_user_id'); 24 | } 25 | 26 | public function referencePosition() 27 | { 28 | return $this->belongsTo(Position::class, 'reference_position_id'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Policies/SettingPolicy.php: -------------------------------------------------------------------------------- 1 | isAdmin(); 21 | } 22 | 23 | /** 24 | * Determine whether the user can update global settings. 25 | * 26 | * @return bool 27 | */ 28 | public function edit(User $user, Setting $setting) 29 | { 30 | return $user->isAdmin(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2024_03_26_175154_extend_booking_callsign_characters.php: -------------------------------------------------------------------------------- 1 | string('callsign', 30)->change(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('bookings', function (Blueprint $table) { 25 | $table->string('callsign', 11)->change(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_07_19_103139_add_area_waiting_time_string.php: -------------------------------------------------------------------------------- 1 | string('waiting_time')->nullable()->after('contact'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('areas', function (Blueprint $table) { 25 | $table->dropColumn('waiting_time'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_12_27_131934_add_area_readme_url.php: -------------------------------------------------------------------------------- 1 | string('readme_url')->nullable()->after('template_pretraining'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('areas', function (Blueprint $table) { 25 | $table->dropColumn('readme_url'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /docs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "controlcenter-docs" 3 | version = "0.1.0" 4 | description = "Default template for PDM package" 5 | authors = [ 6 | {name = "Thor K. Høgås", email = "thor@roht.no"}, 7 | ] 8 | requires-python = ">=3.13" 9 | readme = "README.md" 10 | license = {text = "GPL3"} 11 | dependencies = [ 12 | "mkdocs>=1.5.3", 13 | "mkdocs-material>=9.5.2", 14 | "mike>=2.0.0", 15 | "mkdocs-exclude>=1.0.2", 16 | "mkdocs-git-revision-date-localized-plugin>=1.2.2", 17 | "mkdocs-git-committers-plugin-2>=2.2.2", 18 | ] 19 | 20 | [tool.pdm] 21 | distribution = false 22 | [tool.pdm.scripts] 23 | "docs:serve" = { cmd = "mike serve", help = "View local documentation" } 24 | "docs:build" = { cmd = "mike deploy --ignore-remote-status dev", help = "Build local documentation" } 25 | -------------------------------------------------------------------------------- /app/Models/TrainingActivity.php: -------------------------------------------------------------------------------- 1 | belongsTo(Training::class); 21 | } 22 | 23 | public function user() 24 | { 25 | return $this->belongsTo(User::class, 'triggered_by_id'); 26 | } 27 | 28 | public function endorsement() 29 | { 30 | return $this->belongsTo(Endorsement::class, 'new_data'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Policies/NotificationPolicy.php: -------------------------------------------------------------------------------- 1 | isModeratorOrAbove(); 21 | } 22 | 23 | /** 24 | * Determine if the user can modify a specific area's templates 25 | * 26 | * @return bool 27 | */ 28 | public function modifyAreaTemplate(User $user, Area $area) 29 | { 30 | return $user->isAdmin() || $user->isModerator($area); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | // Font Awesome 2 | 3 | @use '@fortawesome/fontawesome-free/scss/variables' with ( 4 | $font-path: "@fortawesome/fontawesome-free/webfonts", 5 | ); 6 | 7 | @use '@fortawesome/fontawesome-free'; 8 | @use '@fortawesome/fontawesome-free/scss/fa' as fa; 9 | @use '@fortawesome/fontawesome-free/scss/solid.scss' as fa-solid; 10 | @use '@fortawesome/fontawesome-free/scss/regular.scss' as fa-regular; 11 | @use '@fortawesome/fontawesome-free/scss/brands.scss' as fa-brands; 12 | 13 | // Variables 14 | @import "variables"; 15 | 16 | // Bootstrap 17 | @import "~bootstrap/scss/bootstrap"; 18 | 19 | // Import app scss 20 | @import "global.scss"; 21 | @import "utilities.scss"; 22 | 23 | // Custom Components 24 | @import "dropdowns.scss"; 25 | @import "navs.scss"; 26 | @import "buttons.scss"; 27 | 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /database/factories/EndorsementFactory.php: -------------------------------------------------------------------------------- 1 | User::inRandomOrder()->first()->id, 28 | 'type' => 'FACILITY', 29 | 'valid_from' => Carbon::now(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/TrainingInterestFactory.php: -------------------------------------------------------------------------------- 1 | Training::factory(), 26 | 'key' => Str::random(16), 27 | 'deadline' => now()->addDays(7), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2024_02_06_203837_add_area_feedback_url.php: -------------------------------------------------------------------------------- 1 | string('feedback_url')->nullable()->after('template_pretraining'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('areas', function (Blueprint $table) { 25 | $table->dropColumn('feedback_url'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.owner_name') }}
Control Center 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | 18 | 19 | @endcomponent 20 | @endslot 21 | @endisset 22 | 23 | {{-- Footer --}} 24 | @slot('footer') 25 | @component('mail::footer') 26 | [![Logo]({{ asset('images/logos/'.Config::get('app.logo_mail')) }})]({{ Setting::get("linkHome") }})\ 27 | [Change your e-mail settings here]({{ route('user.settings') }}) 28 | @endcomponent 29 | @endslot 30 | @endcomponent 31 | -------------------------------------------------------------------------------- /database/migrations/2021_03_20_140940_add_traininginterval_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 17 | ['key' => 'trainingInterval', 'value' => 14], 18 | ]); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('settings', function (Blueprint $table) { 29 | // 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/background-jobs.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/table-refresh 3 | --- 4 | 5 | To support all the functionality, Control Center relies on scheduled background jobs for certain tasks. 6 | These tasks must run regularly for Control Center to work as expected. 7 | 8 | ## Scheduled tasks 9 | 10 | A selection of important scheduled tasks include: 11 | 12 | - All trainings with status In Queue or Pre-Training are given a continued interest request each month, and a reminder after a week. 13 | - ATC Active is flag given based on ATC activity. Refreshes daily with data from VATSIM Data API. It counts the hours from today's date and backwards according to the length of qualification period. 14 | - Daily member cleanup, if a member leaves the division, their training will be automatically closed. Same for mentors. Does not apply to visitors. 15 | -------------------------------------------------------------------------------- /app/Console/Commands/KeyGet.php: -------------------------------------------------------------------------------- 1 | info('Current application key, copy the whole next line:'); 32 | $this->info(config('app.key')); 33 | 34 | return Command::SUCCESS; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/MentorController.php: -------------------------------------------------------------------------------- 1 | mentoringTrainings(); 21 | $statuses = TrainingController::$statuses; 22 | $types = TrainingController::$types; 23 | if ($user->isMentorOrAbove()) { 24 | return view('mentor.index', compact('trainings', 'user', 'statuses', 'types')); 25 | } 26 | 27 | abort(403); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2024_07_04_124209_add_pretraining_completed_check.php: -------------------------------------------------------------------------------- 1 | boolean('pre_training_completed')->default(false)->after('experience'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('trainings', function (Blueprint $table) { 25 | $table->dropColumn('pre_training_completed'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controlcenter", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build" 8 | }, 9 | "dependencies": { 10 | "@fortawesome/fontawesome-free": "^7.0.0", 11 | "@popperjs/core": "^2.11.8", 12 | "@vitejs/plugin-vue": "^6.0.0", 13 | "bootstrap": "^5.3.0", 14 | "bootstrap-table": "1.25.0", 15 | "chart.js": "^4.4.0", 16 | "chartjs-adapter-moment": "^1.0.1", 17 | "chartjs-plugin-autocolors": "^0.3.1", 18 | "core-js": "^3.37.1", 19 | "easymde": "^2.18.0", 20 | "flatpickr": "^4.6.13", 21 | "laravel-vite-plugin": "^2.0.0", 22 | "moment": "^2.29.1", 23 | "sass": "1.90.0", 24 | "vite": "^7.0.0", 25 | "vue": "^3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/sass/utilities/_animation.scss: -------------------------------------------------------------------------------- 1 | // Animation Utilities 2 | 3 | // Grow In Animation 4 | 5 | @keyframes growIn { 6 | 0% { 7 | transform: scale(0.9); 8 | opacity: 0; 9 | } 10 | 100% { 11 | transform: scale(1); 12 | opacity: 1; 13 | } 14 | } 15 | 16 | // Fade In Animation 17 | 18 | @keyframes fadeIn { 19 | 0% { 20 | opacity: 0; 21 | } 22 | 100% { 23 | opacity: 1; 24 | } 25 | } 26 | 27 | .animated--fade-in { 28 | animation-name: fadeIn; 29 | animation-duration: 200ms; 30 | animation-timing-function: opacity cubic-bezier(0, 1, 0.4, 1); 31 | } 32 | 33 | .animated--grow-in { 34 | animation-name: growIn; 35 | animation-duration: 200ms; 36 | animation-timing-function: transform cubic-bezier(0.18, 1.25, 0.4, 1), opacity cubic-bezier(0, 1, 0.4, 1); 37 | } -------------------------------------------------------------------------------- /database/factories/RatingFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class RatingFactory extends Factory 12 | { 13 | /** 14 | * Define the model the factory is a factory for. 15 | */ 16 | protected $model = Rating::class; 17 | 18 | /** 19 | * Define the model's default state. 20 | * 21 | * @return array 22 | */ 23 | public function definition(): array 24 | { 25 | return [ 26 | 'name' => $this->faker->word(), 27 | 'description' => $this->faker->sentence(), 28 | 'vatsim_rating' => $this->faker->numberBetween(2, 5), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Middleware/SuspendedUser.php: -------------------------------------------------------------------------------- 1 | rating == 0) { 21 | \Auth::logout(); 22 | 23 | return redirect('/')->with('error', 'Your account has been suspended.'); 24 | } 25 | } 26 | 27 | return $next($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2020_12_01_050819_add_exam_column_to_vatbooks.php: -------------------------------------------------------------------------------- 1 | boolean('exam')->default(false)->after('event'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('vatbooks', function (Blueprint $table) { 29 | $table->dropColumn('exam'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_05_27_110006_add_user_activity_column.php: -------------------------------------------------------------------------------- 1 | timestamp('last_activity')->nullable()->after('last_login'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('last_activity'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_01_01_211040_vatbook_sources.php: -------------------------------------------------------------------------------- 1 | enum('source', ['CC', 'VATBOOK', 'DISCORD'])->default('CC')->after('id'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('vatbooks', function (Blueprint $table) { 29 | $table->dropColumn('source'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_04_12_184638_create_positions_freq_column.php: -------------------------------------------------------------------------------- 1 | string('frequency', 7)->nullable()->after('name'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('positions', function (Blueprint $table) { 29 | $table->dropColumn('frequency'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_08_28_193923_add_solo_req_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 17 | ['key' => 'trainingSoloRequirement', 'value' => 'The student has passed the requirements to gain a solo endorsement.'], 18 | ]); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('settings', function (Blueprint $table) { 29 | // 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_07_30_180006_add_user_warning_column.php: -------------------------------------------------------------------------------- 1 | timestamp('last_inactivity_warning')->nullable()->after('last_activity'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('last_inactivity_warning'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_12_10_122309_add_s1_template_to_areas.php: -------------------------------------------------------------------------------- 1 | text('template_s1_positions')->nullable()->after('template_pretraining'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('areas', function (Blueprint $table) { 29 | $table->dropColumn('template_s1_positions'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Rules/InactivityReminderHours.php: -------------------------------------------------------------------------------- 1 | boolean('expired')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('training_interests', function (Blueprint $table) { 29 | $table->dropColumn('expired'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2023_10_05_182438_add_notify_user_setting.php: -------------------------------------------------------------------------------- 1 | boolean('setting_notify_tasks')->default(true)->after('setting_notify_newexamreport'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('setting_notify_tasks'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2021_02_21_214807_rename_votes_member_column.php: -------------------------------------------------------------------------------- 1 | renameColumn('require_vatsca_member', 'require_member'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('votes', function (Blueprint $table) { 29 | $table->renameColumn('require_member', 'require_vatsca_member'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Models/Rating.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Training::class); 17 | } 18 | 19 | public function endorsements() 20 | { 21 | return $this->belongsToMany(Endorsement::class); 22 | } 23 | 24 | public function areas() 25 | { 26 | return $this->belongsToMany(Area::class)->withPivot('required_vatsim_rating', 'allow_bundling', 'hour_requirement', 'queue_length_low', 'queue_length_high'); 27 | } 28 | 29 | public function requiredByPositions() 30 | { 31 | return $this->hasMany(Position::class, 'required_facility_rating_id'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Models/Endorsement.php: -------------------------------------------------------------------------------- 1 | 'datetime', 14 | 'valid_to' => 'datetime', 15 | 'created_at' => 'datetime', 16 | 'updated_at' => 'datetime', 17 | ]; 18 | 19 | public function ratings() 20 | { 21 | return $this->belongsToMany(Rating::class); 22 | } 23 | 24 | public function positions() 25 | { 26 | return $this->belongsToMany(Position::class); 27 | } 28 | 29 | public function areas() 30 | { 31 | return $this->belongsToMany(Area::class); 32 | } 33 | 34 | public function user() 35 | { 36 | return $this->belongsTo(User::class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2025_08_17_062853_add_last_atc_inactivity_reminder.php: -------------------------------------------------------------------------------- 1 | timestamp('last_inactivity_warning')->nullable()->after('atc_active'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('atc_activities', function (Blueprint $table) { 29 | $table->dropColumn('last_inactivity_warning'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2020_11_19_203252_add_votes_table_vatsca_role_column.php: -------------------------------------------------------------------------------- 1 | boolean('require_vatsca_member')->default(false)->after('require_active'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('votes', function (Blueprint $table) { 29 | $table->dropColumn('require_vatsca_member'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_11_21_103028_add_pretraining_column_countries.php: -------------------------------------------------------------------------------- 1 | text('template_pretraining')->nullable()->after('template_newmentor'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('countries', function (Blueprint $table) { 29 | $table->dropColumn('template_pretraining'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Feature/FrontpageTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | $response->assertStatus(200); 17 | } 18 | 19 | #[Test] 20 | public function user_gets_redirect_if_logged_in() 21 | { 22 | $user = User::factory()->make(); 23 | Auth::login($user); 24 | 25 | $response = $this->get('/'); 26 | $response->assertRedirect('/dashboard'); 27 | } 28 | 29 | #[Test] 30 | public function user_cant_logout_if_not_logged_in() 31 | { 32 | $response = $this->get('/logout'); 33 | $response->assertRedirect('/login'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2021_05_13_195341_add_acticity_category_enums.php: -------------------------------------------------------------------------------- 1 | enum('category', ['ACCESS', 'TRAINING', 'BOOKING', 'OTHER'])->nullable()->after('type'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('activity_logs', function (Blueprint $table) { 29 | $table->dropColumn('category'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_05_24_114034_add_grp_bundle_boolean_to_ratings.php: -------------------------------------------------------------------------------- 1 | boolean('allow_mae_bundling')->nullable()->after('required_vatsim_rating'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('area_rating', function (Blueprint $table) { 29 | $table->dropColumn('allow_grp_bundling'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Environment** 11 | CC Version: 12 | Host type: 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **How to reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Device and browser (if applicable):** 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /app/Rules/ValidTaskType.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least eight characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset.', 18 | 'sent' => 'We have emailed your password reset link.', 19 | 'throttled' => 'Please wait before retrying.', 20 | 'token' => 'This password reset token is invalid.', 21 | 'user' => "We can't find a user with that email address.", 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /app/Mail/StaffNoticeMail.php: -------------------------------------------------------------------------------- 1 | mailSubject = $mailSubject; 25 | $this->textLines = $textLines; 26 | } 27 | 28 | /** 29 | * Build the message. 30 | * 31 | * @return $this 32 | */ 33 | public function build() 34 | { 35 | return $this->subject($this->mailSubject)->markdown('mail.staffnotice', [ 36 | 'textLines' => $this->textLines, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2025_08_17_065024_add_atc_reminder_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 16 | ['key' => 'atcActivityInactivityReminder', 'value' => 0], 17 | ]); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::table('settings', function (Blueprint $table) { 27 | DB::table(Config::get('settings.table'))->where('key', 'atcActivityInactivityReminder')->delete(); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /resources/sass/utilities/_text.scss: -------------------------------------------------------------------------------- 1 | // Grayscale Text Utilities 2 | 3 | .fs-sm { 4 | font-size: 0.7rem; 5 | } 6 | 7 | .fs-lg { 8 | font-size: 1.2rem; 9 | } 10 | 11 | .text-gray-100 { 12 | color: $gray-100 !important; 13 | } 14 | 15 | .text-gray-200 { 16 | color: $gray-200 !important; 17 | } 18 | 19 | .text-gray-300 { 20 | color: $gray-300 !important; 21 | } 22 | 23 | .text-gray-400 { 24 | color: $gray-400 !important; 25 | } 26 | 27 | .text-gray-500 { 28 | color: $gray-500 !important; 29 | } 30 | 31 | .text-gray-600 { 32 | color: $gray-600 !important; 33 | } 34 | 35 | .text-gray-700 { 36 | color: $gray-700 !important; 37 | } 38 | 39 | .text-gray-800 { 40 | color: $gray-800 !important; 41 | } 42 | 43 | .text-gray-900 { 44 | color: $gray-900 !important; 45 | } 46 | 47 | blockquote{ 48 | padding-left: 0.625rem; 49 | border-left: 3px solid grey; 50 | 51 | color: $gray-900; 52 | font-style: italic; 53 | } -------------------------------------------------------------------------------- /container/example-prod.env: -------------------------------------------------------------------------------- 1 | APP_NAME='Control Center' 2 | APP_OWNER='Subdivision Name' 3 | APP_OWNER_SHORT='SCA' 4 | 5 | APP_ENV=production 6 | APP_KEY= 7 | APP_DEBUG=false 8 | APP_URL=http://localhost 9 | 10 | APP_LOGO="vatsca.svg" 11 | APP_LOGO_MAIL="vatsca-email.png" 12 | 13 | DEBUGBAR_ENABLED=false 14 | LOG_CHANNEL=stack 15 | 16 | DB_CONNECTION=mysql 17 | DB_HOST=127.0.0.1 18 | DB_PORT=3306 19 | DB_DATABASE=test 20 | DB_USERNAME=root 21 | DB_PASSWORD= 22 | 23 | OAUTH_ID= 24 | OAUTH_SECRET= 25 | OAUTH_URL="https://auth.vatsim.net" 26 | 27 | SENTRY_LARAVEL_DSN=null 28 | SENTRY_TRACES_SAMPLE_RATE=0.1 29 | 30 | MAIL_MAILER=smtp 31 | MAIL_HOST=smtp.mailtrap.io 32 | MAIL_PORT=2525 33 | MAIL_USERNAME=null 34 | MAIL_PASSWORD=null 35 | MAIL_ENCRYPTION=null 36 | MAIL_FROM_NAME="Control Center" 37 | MAIL_FROM_ADDRESS="noreply@yourvacc.com" 38 | 39 | BROADCAST_DRIVER=log 40 | CACHE_DRIVER=file 41 | QUEUE_CONNECTION=sync 42 | SESSION_DRIVER=file 43 | SESSION_LIFETIME=10080 44 | -------------------------------------------------------------------------------- /database/migrations/2022_05_14_081018_delete_passport_tables.php: -------------------------------------------------------------------------------- 1 | divisionCallsignPrefixes 14 | */ 15 | public static function isDivisionCallsign(string $callsign, Collection $divisionCallsignPrefixes) 16 | { 17 | $validAtcSuffixes = ['DEL' => true, 'GND' => true, 'TWR' => true, 'APP' => true, 'DEP' => true, 'CTR' => true, 'FSS' => true]; 18 | // Filter away invalid ATC suffixes 19 | $suffix = substr($callsign, -3); 20 | if (! array_key_exists($suffix, $validAtcSuffixes)) { 21 | return false; 22 | } 23 | 24 | // PREFIX 25 | if ($divisionCallsignPrefixes->contains(substr($callsign, 0, 4))) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2023_10_01_200047_add_cronjob_datetime.php: -------------------------------------------------------------------------------- 1 | insert([ 18 | ['key' => '_lastCronRun', 'value' => now()], 19 | ]); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('settings', function (Blueprint $table) { 31 | DB::table(Config::get('settings.table'))->where('key', '_lastCronRun')->delete(); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #1a475f; 3 | } 4 | 5 | [data-md-color-scheme="default"]{ 6 | --md-typeset-a-color: #1a475f; 7 | --md-accent-fg-color: #43c6e7; 8 | } 9 | 10 | [data-md-color-scheme="slate"]{ 11 | --md-typeset-a-color: #43c6e7; 12 | --md-accent-fg-color: #43c6e7; 13 | } 14 | 15 | .md-content{ 16 | a{ 17 | text-decoration: underline; 18 | } 19 | } 20 | 21 | .md-nav__link--active{ 22 | text-decoration: underline; 23 | } 24 | 25 | 26 | h1{ 27 | font-weight: 600 !important; 28 | } 29 | 30 | h2{ 31 | font-weight: 500 !important; 32 | } 33 | 34 | h1, h2, h3, h4, h5, h6 { 35 | color: var(--md-typeset-a-color) !important; 36 | } 37 | 38 | /* Tweak in-page content tabs as we don't want double underline */ 39 | .md-typeset .tabbed-labels--linked > label > a { 40 | text-decoration: none; 41 | } 42 | .js .md-typeset .tabbed-labels:before { 43 | background: var(--md-accent-fg-color); 44 | } 45 | -------------------------------------------------------------------------------- /app/Policies/TaskPolicy.php: -------------------------------------------------------------------------------- 1 | isMentorOrAbove() || $user->isExaminer(); 20 | } 21 | 22 | /** 23 | * Determine whether the user can update the task. 24 | * 25 | * @return bool 26 | */ 27 | public function update(User $user) 28 | { 29 | return $user->isMentorOrAbove() || $user->isExaminer(); 30 | } 31 | 32 | /** 33 | * Determine if user is able to receive a task 34 | * 35 | * @return bool 36 | */ 37 | public function receive(User $user) 38 | { 39 | return $user->isMentorOrAbove() || $user->isExaminer(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 18 | SendEmailVerificationNotification::class, 19 | ], 20 | ]; 21 | 22 | /** 23 | * Register any events for your application. 24 | */ 25 | public function boot(): void 26 | { 27 | // 28 | } 29 | 30 | /** 31 | * Determine if events and listeners should be automatically discovered. 32 | */ 33 | public function shouldDiscoverEvents(): bool 34 | { 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_05_21_140316_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('type'); 19 | $table->morphs('notifiable'); 20 | $table->text('data'); 21 | $table->timestamp('read_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('notifications'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import * as bootstrap from 'bootstrap' 6 | import moment from 'moment'; 7 | 8 | /** 9 | * Insert global variables 10 | */ 11 | 12 | window.moment = moment; 13 | window.bootstrap = bootstrap; 14 | 15 | /** 16 | * Sidebar logic 17 | */ 18 | 19 | window.addEventListener('load', function(event) { 20 | var sidebar = document.getElementById('sidebar'); 21 | var sidebarButton = document.getElementById('sidebar-button'); 22 | var sidebarCloseButton = document.getElementById('sidebar-button-close'); 23 | 24 | sidebarButton.onclick = function(event){ 25 | event.preventDefault(); 26 | sidebar.classList.toggle('sidebar-show'); 27 | document.body.classList.toggle('fixed-body'); 28 | } 29 | 30 | sidebarCloseButton.onclick = function(event){ 31 | event.preventDefault(); 32 | sidebar.classList.toggle('sidebar-show'); 33 | document.body.classList.toggle('fixed-body'); 34 | } 35 | }); -------------------------------------------------------------------------------- /container/theme/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting theme building process..." 4 | 5 | # Install 6 | apt update 7 | apt install -y ca-certificates curl gnupg 8 | mkdir -p /etc/apt/keyrings 9 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 10 | 11 | NODE_MAJOR=23 12 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list 13 | 14 | apt update 15 | apt install nodejs -y 16 | 17 | # Build 18 | npm ci --omit dev 19 | npm config set cache /tmp --global 20 | su www-data -s /usr/bin/npm run build 21 | 22 | # Cleanup 23 | npm cache clean --force 24 | apt purge curl gnupg nodejs -y 25 | apt autoremove -y 26 | rm -r /etc/apt/sources.list.d/nodesource.list 27 | rm -r /etc/apt/keyrings/nodesource.gpg 28 | 29 | rm -rf /app/node_modules/ 30 | 31 | echo "Theme building process complete. Cleaned up all dependecies to save space." -------------------------------------------------------------------------------- /database/migrations/2020_05_30_175009_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2023_02_12_114913_add_telemetry_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 19 | ['key' => 'telemetryEnabled', 'value' => 1], 20 | ]); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::table('settings', function (Blueprint $table) { 32 | // No need to delete as it just won't have any effect 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2020_03_08_200040_create_training_object_attachments_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->morphs('object'); 19 | $table->string('file_id'); 20 | $table->boolean('hidden')->default(false); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('training_object_attachments'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_06_10_190641_create_vote_options_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('vote_id'); 19 | 20 | $table->string('option'); 21 | 22 | $table->unsignedInteger('voted'); 23 | 24 | $table->foreign('vote_id')->references('id')->on('votes'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('vote_options'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2022_05_14_000001_create_api_tokens_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->unique(); 18 | $table->string('name'); 19 | $table->boolean('read_only')->default(true); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('created_at')->default(\DB::raw('CURRENT_TIMESTAMP')); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('api_keys'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_06_10_190421_create_votes_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('question'); 19 | $table->boolean('require_active'); 20 | $table->boolean('closed')->default(false); 21 | $table->timestamps(); 22 | $table->timestamp('end_at')->default(\DB::raw('CURRENT_TIMESTAMP')); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('votes'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Policies/SweatbookPolicy.php: -------------------------------------------------------------------------------- 1 | isMentorOrAbove(); 21 | } 22 | 23 | /** 24 | * Determine whether the user can create bookings. 25 | * 26 | * @return bool 27 | */ 28 | public function create(User $user) 29 | { 30 | return $user->isMentorOrAbove(); 31 | } 32 | 33 | /** 34 | * Determine whether the user can update the booking. 35 | * 36 | * @return bool 37 | */ 38 | public function update(User $user, Sweatbook $booking) 39 | { 40 | return $booking->user_id == $user->id || $user->isModeratorOrAbove(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Policies/TrainingActivityPolicy.php: -------------------------------------------------------------------------------- 1 | mentors->contains($user) || 21 | $user->can('update', [Training::class, $training]); 22 | } 23 | 24 | /** 25 | * Determine whether the user can view training activity. 26 | * 27 | * @return bool 28 | */ 29 | public function view(User $user, Training $training, string $type) 30 | { 31 | if ($type == 'COMMENT') { 32 | return $training->mentors->contains($user) || $user->isModeratorOrAbove($training->area); 33 | } 34 | 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2021_03_20_130343_add_new_estimate_columns.php: -------------------------------------------------------------------------------- 1 | renameColumn('queue_length', 'queue_length_low'); 18 | $table->unsignedInteger('queue_length_high')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('area_rating', function (Blueprint $table) { 30 | $table->renameColumn('queue_length_low', 'queue_length'); 31 | $table->dropColumn('queue_length_high'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2024_02_13_172836_rename_position_area_to_id_col.php: -------------------------------------------------------------------------------- 1 | renameColumn('area', 'area_id'); 17 | }); 18 | } 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | if (Schema::hasColumn('positions', 'area_id')) { 27 | Schema::table('positions', function (Blueprint $table) { 28 | $table->renameColumn('area_id', 'area'); 29 | }); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | integer('hour_requirement')->nullable()->after('allow_mae_bundling'); 18 | $table->renameColumn('allow_mae_bundling', 'allow_bundling'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('area_rating', function (Blueprint $table) { 30 | $table->dropColumn('hour_requirement'); 31 | $table->renameColumn('allow_bundling', 'allow_mae_bundling'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_10_01_194017_add_allow_inactive_controlling_setting.php: -------------------------------------------------------------------------------- 1 | insert([ 18 | ['key' => 'atcActivityAllowInactiveControlling', 'value' => 0], 19 | ]); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('settings', function (Blueprint $table) { 31 | DB::table(Config::get('settings.table'))->where('key', 'atcActivityAllowInactiveControlling')->delete(); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /public/images/control-tower.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /database/migrations/2020_03_08_200050_create_rating_user.php: -------------------------------------------------------------------------------- 1 | primary(['rating_id', 'user_id']); 18 | 19 | $table->unsignedInteger('rating_id'); 20 | $table->unsignedBigInteger('user_id'); 21 | 22 | $table->foreign('rating_id')->references('id')->on('ratings'); 23 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('rating_user'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2020_06_10_190844_create_user_vote_table.php: -------------------------------------------------------------------------------- 1 | primary(['vote_id', 'user_id']); 18 | 19 | $table->unsignedBigInteger('vote_id'); 20 | $table->unsignedBigInteger('user_id'); 21 | 22 | $table->foreign('vote_id')->references('id')->on('votes'); 23 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('user_vote'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2024_02_22_175840_add_subject_training_rating_id_column.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('subject_training_rating_id')->nullable()->after('subject_training_id'); 16 | $table->foreign('subject_training_rating_id')->references('id')->on('ratings')->onDelete('cascade'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('tasks', function (Blueprint $table) { 26 | $table->dropForeign('tasks_subject_training_rating_id_foreign'); 27 | $table->dropColumn('subject_training_rating_id'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Providers/CarbonServiceProvider.php: -------------------------------------------------------------------------------- 1 | format('D. d/m/Y'); 30 | } 31 | 32 | return $this->format('d/m/Y'); 33 | }); 34 | 35 | Carbon::macro('toEuropeanDateTime', function () { 36 | return $this->format('d/m/Y H:i\z'); 37 | }); 38 | 39 | Carbon::macro('toEuropeanTime', function () { 40 | return $this->format('H:i\z'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2020_05_21_142025_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('queue')->index(); 19 | $table->longText('payload'); 20 | $table->unsignedTinyInteger('attempts'); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('jobs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2021_02_25_200220_create_permissions_table.php: -------------------------------------------------------------------------------- 1 | primary(['user_id', 'country_id', 'group_id']); 18 | 19 | $table->unsignedBigInteger('user_id'); 20 | $table->unsignedInteger('country_id'); 21 | $table->unsignedInteger('group_id'); 22 | 23 | $table->unsignedBigInteger('inserted_by')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('permissions'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2021_05_13_154011_add_workmail_to_users.php: -------------------------------------------------------------------------------- 1 | string('setting_workmail_address', 64)->nullable()->after('remember_token'); 18 | $table->timestamp('setting_workmail_expire')->nullable()->after('setting_workmail_address'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('users', function (Blueprint $table) { 30 | $table->dropColumn('setting_workmail_address'); 31 | $table->dropColumn('setting_workmail_expire'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2023_10_02_132846_add_createdby_training_column.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('created_by')->nullable()->after('closed_reason'); 18 | $table->foreign('created_by')->references('id')->on('users')->onUpdate('CASCADE')->onDelete('SET NULL'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('trainings', function (Blueprint $table) { 30 | $table->dropForeign('trainings_created_by_foreign'); 31 | $table->dropColumn('created_by'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/Console/Commands/CleanSweatbooks.php: -------------------------------------------------------------------------------- 1 | where('date', '<', date('Y-m-d'))->delete(); 42 | $this->info('All old sweatbooks have been cleaned.'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Console/Commands/UpdateBookings.php: -------------------------------------------------------------------------------- 1 | where('time_end', '<', date('Y-m-d H:i:s'))->delete(); 43 | 44 | $this->info('All bookings have been updated.'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /container/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CONTROL_CENTER_ROOT=/app 5 | SELF_SIGNED_KEY=/etc/ssl/private/apache-selfsigned.key 6 | SELF_SIGNED_CERT=/etc/ssl/certs/apache-selfsigned.crt 7 | 8 | if [ ! -f "$SELF_SIGNED_KEY" ] || [ ! -f "$SELF_SIGNED_CERT" ]; then 9 | # Generate a self-signed cert to support SSL connections 10 | openssl req -x509 -nodes -days 358000 -newkey rsa:2048 -keyout "$SELF_SIGNED_KEY" -out "$SELF_SIGNED_CERT" -subj "/O=Your vACC/CN=Control Center" 11 | fi 12 | 13 | if [ -z "$APP_KEY" ] && [ ! -f "$CONTROL_CENTER_ROOT/.env" ]; then 14 | echo "################################################################################" 15 | echo "WARNING: You need to follow the configuration guide for Control Center" 16 | echo "################################################################################" 17 | echo "WARNING: Copying over example .env file" 18 | cp container/example-prod.env .env 19 | echo "WARNING: Temporarily updating the application key" 20 | php artisan key:generate 21 | fi 22 | 23 | exec docker-php-entrypoint "$@" 24 | -------------------------------------------------------------------------------- /app/Console/Commands/CleanVotes.php: -------------------------------------------------------------------------------- 1 | where('end_at', '<=', date('Y-m-d H:i:s'))->update(['closed' => 1]); 42 | $this->info('All expired votes have been closed.'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_03_08_200080_create_rating_training_table.php: -------------------------------------------------------------------------------- 1 | primary(['rating_id', 'training_id']); 18 | 19 | $table->unsignedInteger('rating_id'); 20 | $table->unsignedBigInteger('training_id'); 21 | 22 | $table->foreign('rating_id')->references('id')->on('ratings'); 23 | $table->foreign('training_id')->references('id')->on('trainings')->onDelete('cascade'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('rating_training'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/TrainingExaminationFactory.php: -------------------------------------------------------------------------------- 1 | faker->dateTimeBetween($startDate = 'now', $endDate = '+ 1 years'); 25 | 26 | return [ 27 | 'examination_date' => $date, 28 | 'position_id' => \App\Models\Position::query()->inRandomOrder()->first()->id, 29 | 'result' => $this->faker->randomElement([ 30 | 'PASSED', 'FAILED', 'INCOMPLETE', 'POSTPONED', 31 | ]), 32 | 'created_at' => $date, 33 | 'updated_at' => $date, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2020_09_11_193706_create_one_time_links_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('training_id'); 19 | $table->string('training_object_type'); 20 | $table->string('key'); 21 | $table->timestamp('expires_at'); 22 | 23 | $table->foreign('training_id')->references('id')->on('trainings')->onUpdate('CASCADE')->onDelete('CASCADE'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('one_time_links'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2022_05_15_095715_add_endorsement_log_type.php: -------------------------------------------------------------------------------- 1 | dropColumn('category'); 19 | }); 20 | 21 | Schema::table('activity_logs', function (Blueprint $table) { 22 | $table->enum('category', ['ACCESS', 'TRAINING', 'BOOKING', 'ENDORSEMENT', 'OTHER'])->nullable()->after('type'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | // Breaking change 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/factories/BookingFactory.php: -------------------------------------------------------------------------------- 1 | create(); 29 | $position = Position::whereNotNull('name')->inRandomOrder()->first(); 30 | 31 | return [ 32 | 'callsign' => $position->name, 33 | 'position_id' => $position->id, 34 | 'name' => $user->name, 35 | 'time_start' => Carbon::now()->addHours(1), 36 | 'time_end' => Carbon::now()->addHours(2), 37 | 'user_id' => $user->id, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | container_name: control-center 4 | image: ghcr.io/vatsim-scandinavia/control-center:v6 5 | ports: 6 | - 8080:80 7 | - 8443:443 8 | volumes: 9 | - sessions:/app/storage/framework/sessions 10 | environment: 11 | # Please check the complete configure guide for all variables and their description 12 | # You need to adjust this file to your needs 13 | - APP_NAME=Control Center 14 | - APP_OWNER=Your VACC Name 15 | - APP_OWNER_SHORT=SCA 16 | - APP_URL=https://cc.yourvacc.com 17 | - APP_ENV=production 18 | - DB_HOST=localhost 19 | - DB_DATABASE=control-center 20 | - DB_USERNAME=control-center 21 | - DB_PASSWORD=yourPASShere 22 | - OAUTH_ID=yourIDhere 23 | - OAUTH_SECRET=yourSECREThere 24 | - VATSIM_BOOKING_API_TOKEN=yourTOKENhere 25 | - MAIL_HOST=smtp.mailgun.org 26 | - MAIL_PORT=587 27 | - MAIL_USERNAME=yourUSERNAMEhere 28 | - MAIL_PASSWORD=yourPASSWORDhere 29 | - MAIL_FROM_ADDRESS=noreply@yourvacc.com 30 | volumes: 31 | sessions: -------------------------------------------------------------------------------- /database/migrations/2024_02_04_113019_delete_template_s1_positions.php: -------------------------------------------------------------------------------- 1 | getDriverName() == 'sqlite') { 17 | Schema::disableForeignKeyConstraints(); 18 | } 19 | 20 | Schema::table('areas', function (Blueprint $table) { 21 | $table->dropColumn('template_s1_positions'); 22 | }); 23 | 24 | // Re-enable foreign key checks for SQLite 25 | if (Schema::getConnection()->getDriverName() == 'sqlite') { 26 | Schema::enableForeignKeyConstraints(); 27 | } 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | // Breaking change 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /app/Providers/DivisionApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(DivisionApiContract::class, function ($app) { 19 | $apiType = config('vatsim.division_api_driver'); // Setting from environment 20 | $enabled = Setting::get('divisionApiEnabled', false); // Setting from admin panel 21 | 22 | if (! $enabled) { 23 | return new NoOpAdapter(); 24 | } 25 | 26 | switch ($apiType) { 27 | case 'VATEUD': 28 | return new VATEUD(); 29 | default: 30 | return new NoOpAdapter(); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_10_03_151333_create_training_interests.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('training_id'); 19 | $table->string('key'); 20 | $table->timestamps(); 21 | $table->timestamp('deadline')->nullable(); 22 | $table->timestamp('confirmed_at')->nullable(); 23 | 24 | $table->foreign('training_id')->references('id')->on('trainings')->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('training_interests'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2022_11_19_142147_remove_vatbook_add_booking.php: -------------------------------------------------------------------------------- 1 | dropColumn(['eu_id', 'local_id', 'cid']); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::rename('bookings', 'vatbooks'); 30 | Schema::table('vatbooks', function (Blueprint $table) { 31 | $table->bigInteger('eu_id')->unsigned()->after('source'); 32 | $table->bigInteger('local_id')->unsigned()->nullable()->after('eu_id'); 33 | $table->bigInteger('cid')->unsigned()->after('local_id'); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/Tasks/Types/Custom.php: -------------------------------------------------------------------------------- 1 | message; 22 | } 23 | 24 | public function getLink(Task $model) 25 | { 26 | return false; 27 | } 28 | 29 | public function allowMessage() 30 | { 31 | return true; 32 | } 33 | 34 | public function create(Task $model) 35 | { 36 | parent::onCreated($model); 37 | } 38 | 39 | public function complete(Task $model) 40 | { 41 | parent::onCompleted($model); 42 | } 43 | 44 | public function decline(Task $model) 45 | { 46 | parent::onDeclined($model); 47 | } 48 | 49 | public function showConnectedRatings() 50 | { 51 | return false; 52 | } 53 | 54 | public function allowNonVatsimRatings() 55 | { 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.env.ci: -------------------------------------------------------------------------------- 1 | APP_NAME="Control Center" 2 | APP_OWNER_NAME="Subdivision Name" 3 | APP_OWNER_NAME_SHORT="SCA" 4 | APP_OWNER_CODE="SCA" 5 | 6 | APP_ENV=ci 7 | APP_KEY= 8 | APP_DEBUG=false 9 | APP_URL=http://localhost 10 | APP_TRACKING_SCRIPT= 11 | 12 | APP_LOGO="logo.png" 13 | APP_LOGO_MAIL="logo.png" 14 | 15 | LOG_CHANNEL=stack 16 | 17 | OAUTH_ID= 18 | OAUTH_SECRET= 19 | OAUTH_URL="https://handover.test.vatsca.org" 20 | 21 | VATSIM_BOOKING_API_URL= 22 | VATSIM_BOOKING_API_TOKEN= 23 | 24 | SENTRY_LARAVEL_DSN=null 25 | SENTRY_TRACES_SAMPLE_RATE=0.1 26 | 27 | MAIL_MAILER=smtp 28 | MAIL_HOST=smtp.mailtrap.io 29 | MAIL_PORT=2525 30 | MAIL_USERNAME=null 31 | MAIL_PASSWORD=null 32 | MAIL_ENCRYPTION=null 33 | MAIL_FROM_NAME="Control Center" 34 | MAIL_FROM_ADDRESS="noreply@yourvacc.com" 35 | 36 | VITE_THEME_PRIMARY="#1a475f" 37 | VITE_THEME_SECONDARY="#484b4c" 38 | VITE_THEME_TERTIARY="#011328" 39 | VITE_THEME_INFO="#17a2b8" 40 | VITE_THEME_SUCCESS="#41826e" 41 | VITE_THEME_WARNING="#ff9800" 42 | VITE_THEME_DANGER="#b63f3f" 43 | VITE_THEME_BORDER_RADIUS="0px" 44 | 45 | BROADCAST_DRIVER=log 46 | CACHE_DRIVER=file 47 | QUEUE_CONNECTION=sync 48 | SESSION_DRIVER=file 49 | SESSION_LIFETIME=10080 50 | -------------------------------------------------------------------------------- /database/migrations/2024_07_17_162745_add_required_endorsement_id_to_positions.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('required_facility_rating_id')->nullable()->after('mae'); 16 | $table->foreign('required_facility_rating_id')->references('id')->on('ratings')->onDelete('set null'); 17 | $table->dropColumn('mae'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::table('positions', function (Blueprint $table) { 27 | $table->dropForeign('positions_required_facility_rating_id_foreign'); 28 | $table->dropColumn('required_facility_rating_id'); 29 | $table->unsignedInteger('mae')->nullable()->after('rating'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | bound('sentry') && $this->shouldReport($exception)) { 28 | app('sentry')->captureException($exception); 29 | } 30 | 31 | parent::report($exception); 32 | } 33 | 34 | /** 35 | * Render an exception into an HTTP response. 36 | * 37 | * @param \Illuminate\Http\Request $request 38 | * @return \Illuminate\Http\Response 39 | */ 40 | public function render($request, Throwable $exception) 41 | { 42 | return parent::render($request, $exception); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_04_15_193948_create_files_table.php: -------------------------------------------------------------------------------- 1 | string('id'); 18 | $table->unsignedBigInteger('uploaded_by')->nullable(); 19 | $table->string('name'); 20 | $table->string('path'); 21 | $table->timestamps(); 22 | 23 | $table->primary('id'); 24 | 25 | // We want to keep all the files even though the user is deleted from the database 26 | $table->foreign('uploaded_by')->references('id')->on('users')->onDelete('SET NULL')->onUpdate('CASCADE'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('files'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2020_03_08_200060_create_training_mentor_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('user_id'); 18 | $table->unsignedBigInteger('training_id'); 19 | $table->timestamp('expire_at'); 20 | $table->timestamps(); 21 | 22 | $table->primary(['user_id', 'training_id']); 23 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 24 | $table->foreign('training_id')->references('id')->on('trainings')->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('training_user'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Models/File.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class, 'uploaded_by'); 23 | } 24 | 25 | /** 26 | * Get the training report attachment to which the file is used 27 | * 28 | * @return |null 29 | */ 30 | public function getTrainingReportAttachmentAttribute() 31 | { 32 | return count(TrainingObjectAttachment::where('file_id', $this->id)->get()) != 0 ? TrainingObjectAttachment::where('file_id', $this->id)->first() : null; 33 | } 34 | 35 | /** 36 | * Get the full server path. 37 | * Can simply be called as $file->full_path 38 | * 39 | * @return string 40 | */ 41 | public function getFullPathAttribute() 42 | { 43 | return 'public/files/' . $this->path; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /container/configs/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | ServerName localhost 4 | ServerAdmin ${MAIL_FROM_ADDRESS} 5 | 6 | DocumentRoot /app/public 7 | 8 | Options -Indexes +FollowSymLinks +MultiViews 9 | AllowOverride All 10 | Require all granted 11 | 12 | 13 | ErrorLog /app/storage/logs/apache-error.log 14 | CustomLog /app/storage/logs/apache-access.log combined 15 | 16 | 17 | 18 | 19 | 20 | ServerName localhost 21 | ServerAdmin ${MAIL_FROM_ADDRESS} 22 | 23 | DocumentRoot /app/public 24 | 25 | Options -Indexes +FollowSymLinks +MultiViews 26 | AllowOverride All 27 | Require all granted 28 | 29 | 30 | SSLEngine On 31 | SSLCertificateFile /etc/ssl/certs/apache-selfsigned.crt 32 | SSLCertificateKeyFile /etc/ssl/private/apache-selfsigned.key 33 | 34 | ErrorLog /app/storage/logs/apache-error.log 35 | CustomLog /app/storage/logs/apache-access.log combined 36 | 37 | 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Unit 6 | 7 | 8 | ./tests/Feature 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ./app 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /database/factories/TrainingReportFactory.php: -------------------------------------------------------------------------------- 1 | faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'); 27 | 28 | return [ 29 | 'written_by_id' => User::factory(), 30 | 'report_date' => $date->format('Y-M-d'), 31 | 'content' => $this->faker->paragraph(), 32 | 'contentimprove' => $this->faker->paragraph(), 33 | 'position' => Position::inRandomOrder()->first()->callsign, 34 | 'draft' => $this->faker->numberBetween(0, 1), 35 | 'created_at' => $date, 36 | 'updated_at' => $date, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | # Excluding mkdocs.yml due to the use of custom tags 11 | exclude: ^docs/mkdocs.yml$ 12 | - id: check-added-large-files 13 | - repo: https://github.com/igorshubovych/markdownlint-cli 14 | rev: v0.38.0 15 | hooks: 16 | - id: markdownlint 17 | args: [--disable, line-length, --] 18 | - repo: local 19 | hooks: 20 | - id: composer 21 | name: Ensure Composer is valid 22 | description: Runs Composer to validate configuration 23 | language: script 24 | entry: /bin/sh 25 | args: [-c, "composer validate"] 26 | files: composer.* 27 | - id: pint 28 | name: Check formatting (PHP) 29 | description: Runs Laravel Pint to automatically fix formatting issues. 30 | language: script 31 | entry: /bin/sh 32 | args: [-c, "([ -f ./vendor/bin/pint ] || composer install) && ./vendor/bin/pint -v"] 33 | types: [php] 34 | -------------------------------------------------------------------------------- /app/Models/Area.php: -------------------------------------------------------------------------------- 1 | hasMany(Training::class); 17 | } 18 | 19 | public function endorsements() 20 | { 21 | return $this->belongsToMany(Endorsement::class); 22 | } 23 | 24 | public function ratings() 25 | { 26 | return $this->belongsToMany(Rating::class)->withPivot('required_vatsim_rating', 'allow_bundling', 'hour_requirement', 'queue_length_low', 'queue_length_high'); 27 | } 28 | 29 | public function permissions() 30 | { 31 | return $this->belongsToMany(Group::class, 'permissions')->withPivot('area_id')->withTimestamps(); 32 | } 33 | 34 | public function mentors() 35 | { 36 | return $this->belongsToMany(User::class, 'permissions')->withPivot('group_id')->withTimestamps()->where('group_id', 3); 37 | } 38 | 39 | public function positions() 40 | { 41 | return $this->hasMany(Position::class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2020_03_09_204817_create_solo_endorsements_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('user_id')->unique(); 19 | $table->unsignedBigInteger('training_id'); 20 | $table->string('position', 30); 21 | $table->timestamp('expires_at'); 22 | $table->timestamps(); 23 | 24 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 25 | $table->foreign('training_id')->references('id')->on('trainings')->onDelete('cascade'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('solo_endorsements'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2023_08_26_184643_fix_utc_defaults.php: -------------------------------------------------------------------------------- 1 | timestamp('end_at')->default(null)->change(); 19 | }); 20 | 21 | Schema::table('api_keys', function (Blueprint $table) { 22 | $table->timestamp('created_at')->default(null)->change(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::table('votes', function (Blueprint $table) { 34 | $table->timestamp('end_at')->default(\DB::raw('CURRENT_TIMESTAMP')); 35 | }); 36 | 37 | Schema::table('api_keys', function (Blueprint $table) { 38 | $table->timestamp('created_at')->default(\DB::raw('CURRENT_TIMESTAMP')); 39 | }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/RosterController.php: -------------------------------------------------------------------------------- 1 | where('type', 'VISITING')->where('revoked', false)->whereHas('areas', function ($query) use ($areaId) { 23 | $query->where('area_id', $areaId); 24 | }); 25 | })->get(); 26 | 27 | $users = $users->merge($visitingUsers); 28 | 29 | // Get ratings that are not VATSIM ratings which belong to the area 30 | $ratings = Rating::whereHas('areas', function (Builder $query) use ($areaId) { 31 | $query->where('area_id', $areaId); 32 | })->whereNull('vatsim_rating')->get()->sortBy('name'); 33 | 34 | return view('roster', compact('users', 'ratings', 'area')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2024_07_08_194628_add_trainingactivity_pretraining_type.php: -------------------------------------------------------------------------------- 1 | string('type_new')->after('triggered_by_id'); 18 | }); 19 | 20 | DB::statement('UPDATE training_activity SET type_new = type'); 21 | 22 | Schema::table('training_activity', function (Blueprint $table) { 23 | $table->dropColumn('type'); 24 | }); 25 | 26 | Schema::table('training_activity', function (Blueprint $table) { 27 | $table->renameColumn('type_new', 'type'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | // Breaking but harmless change 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /docs/integrations/vatsim.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | There's no Control Center without VATSIM. 4 | 5 | Besides supporting [VATSIM Connect](./vatsim-connect.md), Control Center relies on: 6 | 7 | * [VATSIM API][vatsim-api] 8 | * [VATSIM Core API][vatsim-core-api] 9 | * [VATSIM ATC Bookings API][vatsim-atc-bookings-api] 10 | 11 | !!! tip "Use VATSIM APIs with Control Center" 12 | See [the configuration manual](../configuration/index.md#vatsim) to get started. 13 | 14 | ## VATSIM Core API 15 | 16 | [VATSIM Core API][vatsim-core-api] is used to retrieve members of a subdivision. 17 | 18 | !!! info 19 | VATSIM Core API key v2 is required to enable this feature, contact VATSIM Tech Department using VATSIM Support to get your key. 20 | 21 | ## VATSIM ATC Bookings API 22 | 23 | [VATSIM ATC Bookings API][vatsim-atc-bookings-api] is used get, publish, edit and remove controller bookings. 24 | 25 | !!! info 26 | VATSIM ATC Bookings API key is required to enable this feature, contact VATSIM Tech Department using VATSIM Support to get your key. 27 | 28 | [vatsim-api]: https://api.vatsim.net/api/ 29 | [vatsim-core-api]: https://vatsim.dev/api/core-api 30 | [vatsim-atc-bookings-api]: https://atc-bookings.vatsim.net/api-doc 31 | -------------------------------------------------------------------------------- /app/Mail/MentorNoticeMail.php: -------------------------------------------------------------------------------- 1 | mailSubject = $mailSubject; 29 | $this->textLines = $textLines; 30 | $this->actionUrl = $actionUrl; 31 | $this->actionText = $actionText; 32 | } 33 | 34 | /** 35 | * Build the message. 36 | * 37 | * @return $this 38 | */ 39 | public function build() 40 | { 41 | return $this->subject($this->mailSubject)->markdown('mail.mentornotice', [ 42 | 'textLines' => $this->textLines, 43 | 'actionUrl' => $this->actionUrl, 44 | 'actionText' => $this->actionText, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2021_02_25_075635_create_atc_activity_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->unsignedBigInteger('user_id'); 19 | $table->double('atc_hours'); 20 | $table->string('favourite_position')->nullable()->default(null); 21 | $table->boolean('inside_grace_period')->default(false); 22 | $table->timestamp('valid_until')->default(\Illuminate\Support\Facades\DB::raw('CURRENT_TIMESTAMP')); 23 | $table->timestamps(); 24 | 25 | $table->foreign('user_id')->references('id')->on('users')->onDelete('CASCADE')->onUpdate('CASCADE'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('atc_activity'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2020_08_24_191843_create_activity_logs.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->enum('type', ['DEBUG', 'INFO', 'WARNING', 'DANGER']); 19 | 20 | $table->unsignedBigInteger('user_id')->nullable(); 21 | $table->longText('message'); 22 | 23 | $table->string('ip_address')->nullable(); 24 | $table->string('user_agent')->nullable(); 25 | $table->timestamp('created_at')->nullable(); 26 | 27 | $table->foreign('user_id') 28 | ->references('id') 29 | ->on('users') 30 | ->onDelete('cascade'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('activity_logs'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/setup/custom.md: -------------------------------------------------------------------------------- 1 | # Custom container image 2 | 3 | Depending on the changes you'd like to make, you may choose to create a variant of Control Center based on the upstream version. 4 | 5 | !!! warning 6 | Creating a custom variant is limited to people with prior development experience. 7 | We recommend that **most**, if not all, **users** should use the [standard container image](../installation.md#with-docker). 8 | 9 | This is the most reliable way to make changes over time, as well as regularly synchronise the changes with the upstream. 10 | 11 | ## Custom Image 12 | 13 | ```Dockerfile title="Custom derivation of Control Center" 14 | FROM ghcr.io/vatsim-scandinavia/control-center:latest 15 | 16 | # Make your customisations here 17 | ... 18 | ``` 19 | 20 | ## Example 21 | 22 | ### Custom Theme 23 | 24 | You can customise the theme by [setting environment variables and running `/container/theme/build.sh`](./theme.md): 25 | 26 | ```Dockerfile title="Custom theme in Control Center" 27 | FROM ghcr.io/vatsim-scandinavia/control-center:latest 28 | 29 | # Add any relevant theming environment variables here 30 | ENV VITE_THEME_PRIMARY="#222222" 31 | # ... 32 | 33 | # Make the theme build script executable and run it 34 | RUN chmod +x container/theme/build.sh && container/theme/build.sh 35 | ``` 36 | -------------------------------------------------------------------------------- /app/Tasks/Types/SoloEndorsement.php: -------------------------------------------------------------------------------- 1 | subject_user_id); 27 | } 28 | 29 | public function create(Task $model) 30 | { 31 | parent::onCreated($model); 32 | } 33 | 34 | public function complete(Task $model) 35 | { 36 | parent::onCompleted($model); 37 | } 38 | 39 | public function decline(Task $model) 40 | { 41 | parent::onDeclined($model); 42 | } 43 | 44 | public function requireCheckboxConfirmation() 45 | { 46 | return 'The student has passed the required theoretical exam.'; 47 | } 48 | 49 | public function showConnectedRatings() 50 | { 51 | return false; 52 | } 53 | 54 | public function allowNonVatsimRatings() 55 | { 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Mail/TaskMail.php: -------------------------------------------------------------------------------- 1 | mailSubject = $mailSubject; 31 | $this->user = $user; 32 | $this->textLines = $textLines; 33 | } 34 | 35 | /** 36 | * Build the message. 37 | * 38 | * @return $this 39 | */ 40 | public function build() 41 | { 42 | return $this->subject($this->mailSubject)->markdown('mail.tasks', [ 43 | 'firstName' => $this->user->first_name, 44 | 'textLines' => $this->textLines, 45 | 'actionUrl' => route('tasks'), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Feature/ReportControllerTest.php: -------------------------------------------------------------------------------- 1 | adminUser = User::factory()->create(); 23 | $this->adminUser->groups()->attach(1, ['area_id' => Area::factory()->create()->id]); 24 | } 25 | 26 | public static function reportRoutesProvider(): array 27 | { 28 | return [ 29 | 'access' => ['reports.access'], 30 | 'trainings' => ['reports.trainings'], 31 | 'activities' => ['reports.activities'], 32 | 'mentors' => ['reports.mentors'], 33 | 'feedback' => ['reports.feedback'], 34 | ]; 35 | } 36 | 37 | #[Test] 38 | #[DataProvider('reportRoutesProvider')] 39 | public function can_visit_report_page(string $routeName): void 40 | { 41 | $response = $this->actingAs($this->adminUser)->get(route($routeName)); 42 | $response->assertOk(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_03_08_000010_create_groups_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('description'); 20 | }); 21 | 22 | DB::table('groups')->insert([ 23 | ['id' => 1, 'name' => 'Administrator', 'description' => 'Rank meant for vACC Director, Training Director and technicaians, access to whole system.'], 24 | ['id' => 2, 'name' => 'Moderator', 'description' => 'Access meant for FIR Director and Training assistants to have full control over trainings and statistics.'], 25 | ['id' => 3, 'name' => 'Mentor', 'description' => 'Access meant for mentors, to give them mentor-related functionality.'], 26 | ]); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('groups'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/Task.php: -------------------------------------------------------------------------------- 1 | TaskStatus::class, 17 | 'closed_at' => 'datetime', 18 | ]; 19 | 20 | public function creator() 21 | { 22 | return $this->belongsTo(User::class, 'creator_user_id'); 23 | } 24 | 25 | public function subject() 26 | { 27 | return $this->belongsTo(User::class, 'subject_user_id'); 28 | } 29 | 30 | public function subjectTraining() 31 | { 32 | return $this->belongsTo(Training::class, 'subject_training_id'); 33 | } 34 | 35 | public function subjectTrainingRating() 36 | { 37 | return $this->belongsTo(Rating::class, 'subject_training_rating_id'); 38 | } 39 | 40 | public function assignee() 41 | { 42 | return $this->belongsTo(User::class, 'assignee_user_id'); 43 | } 44 | 45 | public function type() 46 | { 47 | if ($this->type) { 48 | return app($this->type); 49 | } else { 50 | throw new \Exception('Invalid task type: ' . $this->type); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/migrations/2024_02_04_111709_remove_s1_endorsements.php: -------------------------------------------------------------------------------- 1 | string('type_temp', 32)->after('type'); 19 | }); 20 | 21 | DB::statement('UPDATE endorsements SET type_temp = type'); 22 | 23 | Schema::table('endorsements', function (Blueprint $table) { 24 | $table->dropColumn('type'); 25 | }); 26 | 27 | Schema::table('endorsements', function (Blueprint $table) { 28 | $table->renameColumn('type_temp', 'type'); 29 | }); 30 | 31 | // We won't delete the old S1 endorsements, to keep the history for training activity logs. However, they have no effect anymore in the code. 32 | 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | */ 38 | public function down(): void 39 | { 40 | // Breaking change 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /app/Policies/TrainingObjectAttachmentPolicy.php: -------------------------------------------------------------------------------- 1 | object->training->area; 21 | 22 | return ($user->can('view', $attachment->object) && $attachment->hidden != true) || $user->isMentor($attachmentArea); 23 | } 24 | 25 | /** 26 | * Determine whether the user can create training report attachments. 27 | * 28 | * @return bool 29 | */ 30 | public function create(User $user) 31 | { 32 | return $user->isMentorOrAbove(); 33 | } 34 | 35 | /** 36 | * Determine whether the user can destroy training object attachments. 37 | * 38 | * @return bool 39 | */ 40 | public function delete(User $user, TrainingObjectAttachment $attachment) 41 | { 42 | return $user->isModeratorOrAbove($attachment->object->training->area) || $user->is($attachment->file->owner); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2021_01_29_001536_add_mae_column_to_positions.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('mae')->nullable()->after('rating'); 18 | }); 19 | 20 | // Update endorsed positions 21 | DB::table('positions')->where('callsign', 'like', 'ENGM_%')->update(['mae' => 1]); 22 | DB::table('positions')->where('callsign', 'like', 'ESSA_%')->update(['mae' => 1]); 23 | DB::table('positions')->where('callsign', 'like', 'EKCH_%')->update(['mae' => 1]); 24 | DB::table('positions')->where('callsign', 'BICC_FSS')->update(['mae' => 1]); 25 | DB::table('positions')->where('callsign', 'ENOB_CTR')->update(['mae' => 1]); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::table('positions', function (Blueprint $table) { 36 | $table->dropColumn('mae'); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2021_03_02_202644_change_country_to_areas.php: -------------------------------------------------------------------------------- 1 | renameColumn('country_id', 'area_id'); 21 | }); 22 | 23 | Schema::table('permissions', function (Blueprint $table) { 24 | $table->renameColumn('country_id', 'area_id'); 25 | }); 26 | 27 | Schema::table('positions', function (Blueprint $table) { 28 | $table->renameColumn('country', 'area'); 29 | }); 30 | 31 | Schema::table('trainings', function (Blueprint $table) { 32 | $table->renameColumn('country_id', 'area_id'); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | // Let's just not support rollback, it's a mayor breaking update either way. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/actions/setup-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup dependencies 2 | 3 | inputs: 4 | path: 5 | description: Path to the checked out project 6 | required: true 7 | setup-node: 8 | description: Whether to setup Node or not 9 | default: 'false' 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | 15 | - name: Set up PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '8.3.2' 19 | 20 | - name: Validate composer 21 | run: composer validate 22 | shell: bash 23 | working-directory: ./${{ inputs.path }} 24 | 25 | - name: Cache composer 26 | uses: actions/cache@v4 27 | with: 28 | path: ${{ inputs.path }}/vendor 29 | key: composer-v1-${{ inputs.path }}-${{ hashFiles(format('{0}/composer.lock', inputs.path))}} 30 | 31 | - name: Install composer dependencies 32 | run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist 33 | shell: bash 34 | working-directory: ./${{ inputs.path }} 35 | 36 | - name: Setup node 37 | if: inputs.setup-node == 'true' 38 | uses: actions/setup-node@v6 39 | with: 40 | cache: 'npm' 41 | cache-dependency-path: ${{ inputs.path }} 42 | 43 | - name: Install node dependencies 44 | if: inputs.setup-node == 'true' 45 | run: npm ci 46 | shell: bash 47 | working-directory: ./${{ inputs.path }} 48 | 49 | --------------------------------------------------------------------------------