├── 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 |
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 | |
4 | {{ Illuminate\Mail\Markdown::parse($slot) }}
5 | |
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 |
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 |
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 |
4 |
5 |
6 | |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }}
8 | |
9 |
10 |
11 | |
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 |
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 |
4 |
5 |
6 | |
7 |
14 | |
15 |
16 |
17 | |
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 | [) }})]({{ 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 |
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 |
--------------------------------------------------------------------------------