diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e120198 --- /dev/null +++ b/.env.example @@ -0,0 +1,66 @@ +APP_NAME="VATSIM Germany" +APP_VERSION="2.1" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL= + +COOKIE_CONSENT_ENABLED=true + +LOG_CHANNEL=stack + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE= +DB_USERNAME= +DB_PASSWORD= + +BROADCAST_DRIVER=redis +CACHE_DRIVER=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +VATSIM_OAUTH_BASE=http://auth.vatsim.net +VATSIM_OAUTH_CLIENT= +VATSIM_OAUTH_SECRET= +VATSIM_OAUTH_SCOPES=full_name,email,vatsim_details,country + +VATSIM_API_BASE=https://api.vatsim.net/api + +FORUM_URL=https://board.vatsim-germany.org +FORUM_API_KEY= +FORUM_DEFAULT_GROUP=2 +FORUM_SUSPENDED_GROUP=56 +FORUM_GUEST_GROUP=55 +FORUM_NEWS_THREAD=97 + +FORUM_EXTRA_API_URL=http:// +FORUM_EXTRA_API_KEY= + +TS_HOST=127.0.0.1 +TS_USER=serveradmin +TS_PASS= +TS_PORT=9987 +TS_QUERY_PORT=10011 +TS_NEW_GROUP=Normal +TS_APIKEY= +TS_WEBQUERY_PORT=10080 +TS_SERVER_NR=1 +TS_HP_WEBAPIKEY= + +PASETO_KEY= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..967315d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9a6a85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +/.phpintel +/.idea +/.vscode +/node_modules +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +.lock +*-lock.json +*.lock +laravel-echo-server.json diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..1db61d9 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,13 @@ +php: + preset: laravel + disabled: + - unused_use + finder: + not-name: + - index.php + - server.php +js: + finder: + not-name: + - webpack.mix.js +css: true diff --git a/README.md b/README.md new file mode 100644 index 0000000..da633f2 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +### VATSIM Germany Webservice +Dieses Repository enthält den Quellcode der VATSIM Germany Webservices. + + +#### Installation + +1. Klone das Repository +2. Führe die folgenden Konsolenbefehle aus: + + 1. `composer update` (Installiert die notwendigen Abhängigkeiten via Composer) + + 2. `npm install` (Installiert die notwendigen Abhängigkeiten via NPM) + + 3. `npm run dev` (Um .css und .js Dateien zu generieren) + + 4. Jetzt muss die `.env` Datei angepasst werden: + + 1. `cp .env.example .env` + + 2. `nano .env` + + 3. Wenn alle Einstellungen in der .env Datei an das lokale System angepasst wurden, die Datei speichern und schließen + + 5. `php artisan migrate && php artisan db:seed` + Hiermit wird die Datenbank initialisiert und mit anfänglichen Daten bestückt + 6. `sudo crontab -e` + + 1. Füge folgenden Crontab hinzu `* * * * * cd /path/to/project && php artisan schedule:run >> /dev/null 2>&1` + + 7. Vorbereiten der "Echtzeit" Mitteilungen + + 1. `npm install -g laravel-echo-server` + + 2. Nur ausführen, wenn KEINE `laravel-echo-server.json` Datei mitgeliefert wurde: `laravel-echo-server init` + + 3. `nano laravel-echo-server.json` und die Datei dem lokalen System anpassen + + 8. Datenautomatisierung + + 1. `sudo nano /etc/supervisorctl/conf.d/vatsim-germany-worker.conf` + + 2. Der Queue-Worker + ```lang-bash + [program:vatsim-germany-worker] + process_name=%(program_name)s_%(process_num)02d + command=php /path/to/project/artisan queue:work --sleep=3 --tries=3 + autostart=true + autorestart=true + user=vagrant + numprocs=4 + redirect_stderr=true + stdout_logfile=/path/to/project/storage/logs/worker.log``` + + 3. `sudo nano /etc/supervisorctl/conf.d/vatsim-germany-echo-worker.conf` + + 4. Der Echo-Server Worker + + ```lang-bash + [program:vatsim-germany-echo-worker] + directory=/path/to/project + command=laravel-echo-server start + autostart=true + autorestart=true + user=vagrant + redirect_stderr=true + stdout_logfile=/path/to/project/storage/logs/echo.log``` + + + 5. `sudo supervisorctl reread` + + 6. `sudo supervisorctl reload` + + 7. `sudo supervisorctl start vatsim-germany-worker:*` + + 8. `sudo supervisorctl start vatsim-germany-echo-worker:*` + +3. Konfiguriere deinen Webserver so, dass es auf das public Verzeichnis verweist. + ```lang-bash + server { + listen 80; + server_name yoururl.com; + root /path/to/project/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + index index.php; + + charset utf-8; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\.(?!well-known).* { + deny all; + } + } + ``` \ No newline at end of file diff --git a/app/Charts/Statistics/Aerodrome/TrafficChart.php b/app/Charts/Statistics/Aerodrome/TrafficChart.php new file mode 100644 index 0000000..48a698b --- /dev/null +++ b/app/Charts/Statistics/Aerodrome/TrafficChart.php @@ -0,0 +1,128 @@ +_start->diffInDays($this->_end) + 1; + $hourCounter = 0; + $timeframe = $timespan * 24; // Days in hours + $immDate = $this->_start->copy()->toImmutable(); + while ($hourCounter < $timeframe) { + $s = $immDate->addHours($hourCounter); + $e = $immDate->addHours($hourCounter + 1); + if (0 == $hourCounter) { + $labels[0] = $this->_start->format('d.m.Y H:i'); + } elseif ($hourCounter == $timeframe) { + $this->_labels[] = $this->_end->format('d.m.Y H:i'); + } else { + $this->_labels[] = $s->format('d.m.Y H:i'); + } + $hourCounter++; + } + } + + private function calculateMovements() + { + $timespan = $this->_start->diffInDays($this->_end) + 1; + $hourCounter = 0; + $timeframe = $timespan * 24; // Days in hours + $immDate = $this->_start->copy()->toImmutable(); + while ($hourCounter < $timeframe) { + $s = $immDate->addHours($hourCounter); + $e = $immDate->addHours($hourCounter + 1); + $this->_arrivalsPerHour[] = $this->_arrivals->filter( + function ($arrival) use ($s, $e) { + return $arrival->arrived_at >= $s + && $arrival->arrived_at < $e; + } + )->count(); + $this->_departuresPerHour[] = $this->_departures->filter( + function ($departure) use ($s, $e) { + return $departure->departed_at >= $s + && $departure->departed_at < $e; + } + )->count(); + $hourCounter++; + } + } + + /** + * Handles the HTTP request for the given chart. + * It must always return an instance of Chartisan + * and never a string or an array. + */ + public function handler(Request $request): Chartisan + { + + set_time_limit(0); // Run as long as needed. Will not effect page load due to ajax implementation + + $from = \Carbon\Carbon::createFromFormat('d.m.Y', $request->from, 'utc'); + $from->setHours(0); + $from->setMinutes(0); + $from->setSeconds(0); + + $till = \Carbon\Carbon::createFromFormat('d.m.Y', $request->till, 'utc'); + $till->setHours(23); + $till->setMinutes(59); + $till->setSeconds(59); + + $this->_start = $from; + $this->_end = $till; + + $this->renderLabels(); + + $aerodrome = \App\Models\Navigation\Aerodrome::icao($request->aerodrome)->firstOrFail(); + + $departure = \App\Models\Network\PilotClient::where('departure_airport', $aerodrome->icao) + ->whereBetween('departed_at', [$from, $till]) + ->orderBy('departed_at', 'DESC') + ->get(); + + $arrival = \App\Models\Network\PilotClient::where('arrival_airport', $aerodrome->icao) + ->whereBetween('arrived_at', [$from, $till]) + ->orderBy('arrived_at', 'DESC') + ->get(); + + $this->_arrivals = \App\Models\Statistic\FlightData::where('arrival_airport', $aerodrome->icao) + ->whereBetween('arrived_at', [$from, $till]) + ->orderBy('arrived_at', 'DESC') + ->get(); + $this->_departures = \App\Models\Statistic\FlightData::where('departure_airport', $aerodrome->icao) + ->whereBetween('departed_at', [$from, $till]) + ->orderBy('departed_at', 'DESC') + ->get(); + + $this->_arrivals = $this->_arrivals->merge($arrival); + $this->_departures = $this->_departures->merge($departure); + + $this->calculateMovements(); + + return Chartisan::build() + ->labels($this->_labels) + ->dataset('Arrivals', $this->_arrivalsPerHour) + ->dataset('Departures', $this->_departuresPerHour); + } +} \ No newline at end of file diff --git a/app/Charts/Statistics/Flightdata/ActivityChart.php b/app/Charts/Statistics/Flightdata/ActivityChart.php new file mode 100644 index 0000000..33361a6 --- /dev/null +++ b/app/Charts/Statistics/Flightdata/ActivityChart.php @@ -0,0 +1,112 @@ +_end = \Carbon\Carbon::now()->utc(); + $this->_start = $this->_end->copy()->subDays(14); + $this->_start->setTime(0, 0, 0, 0); + $this->_end->setTime(23, 59, 59, 999); + + $this->renderLabels(); + } + + private function renderLabels() + { + $timespan = $this->_start->diffInDays($this->_end) + 1; + $hourCounter = 0; + $timeframe = $timespan * 24; // Days in hours + $immDate = $this->_start->copy()->toImmutable(); + while ($hourCounter < $timeframe) { + $s = $immDate->addHours($hourCounter); + $e = $immDate->addHours($hourCounter + 1); + if (0 == $hourCounter) { + $labels[0] = $this->_start->format('d.m.Y H:i'); + } elseif ($hourCounter == $timeframe) { + $this->_labels[] = $this->_end->format('d.m.Y H:i'); + } else { + $this->_labels[] = $s->format('d.m.Y H:i'); + } + $hourCounter++; + } + } + + private function calculateMovements() + { + $timespan = $this->_start->diffInDays($this->_end) + 1; + $hourCounter = 0; + $timeframe = $timespan * 24; // Days in hours + $immDate = $this->_start->copy()->toImmutable(); + while ($hourCounter < $timeframe) { + $s = $immDate->addHours($hourCounter); + $e = $immDate->addHours($hourCounter + 1); + $this->_flightsPerHour[] = $this->_flights->filter( + function ($f) use ($s, $e) { + return $f->connected_at >= $s + && $f->connected_at < $e; + } + )->count(); + $hourCounter++; + } + } + + /** + * Handles the HTTP request for the given chart. + * It must always return an instance of Chartisan + * and never a string or an array. + */ + public function handler(Request $request): Chartisan + { + $validated = $request->validate( + [ + 'searchString' => 'string|required' + ] + ); + + // Do the search + $fd = null; + // Is it a callsign? + if(preg_match('/[A-Z]{3}\w*/', $validated['searchString'])) { + $fd = FlightData::callsign($validated['searchString'])->orderBy('connected_at', 'DESC')->get(); + } + // Is it an icao? + if(preg_match('/[a-zA-Z]{4}/', $validated['searchString'])) { + $fd = FlightData::icao($validated['searchString'])->orderBy('connected_at', 'DESC')->get(); + } + // Is it a cid? + if(preg_match('/\[0-9\]{7}/', $validated['searchString']) || preg_match('/[0-9]{6}/', $validated['searchString'])) { + $fd = FlightData::cid($validated['searchString'])->orderBy('connected_at', 'DESC')->get(); + } + + if($fd != null) { + $this->_flights = $fd->filter(function($flight) { + if($flight->departure_airport != '' && $flight->arrival_airport != '') return true; + return false; + }); + } + + $this->calculateMovements(); + + return Chartisan::build() + ->labels($this->_labels) + ->dataset('Flights', $this->_flightsPerHour); + } +} \ No newline at end of file diff --git a/app/Console/Commands/Account/ClearUncompletedRegistrations.php b/app/Console/Commands/Account/ClearUncompletedRegistrations.php new file mode 100644 index 0000000..5076009 --- /dev/null +++ b/app/Console/Commands/Account/ClearUncompletedRegistrations.php @@ -0,0 +1,44 @@ +where('created_at', '<', \Carbon\Carbon::now()->subMinutes(30))->get(); + \App\Jobs\Membership\CleanIncompletedRegistrationsJob::dispatch($accounts); + } +} diff --git a/app/Console/Commands/Account/UpdateApiAccounts.php b/app/Console/Commands/Account/UpdateApiAccounts.php new file mode 100644 index 0000000..546abd8 --- /dev/null +++ b/app/Console/Commands/Account/UpdateApiAccounts.php @@ -0,0 +1,47 @@ +where('begins_at', '<=', Carbon::now()->utc())->get(); + foreach ($eventroutes as $eventroute) + { + DatafeedLookupJob::dispatch($eventroute); + } + } +} diff --git a/app/Console/Commands/Forum/UpdateAccounts.php b/app/Console/Commands/Forum/UpdateAccounts.php new file mode 100644 index 0000000..299a3b6 --- /dev/null +++ b/app/Console/Commands/Forum/UpdateAccounts.php @@ -0,0 +1,42 @@ +defFilePath)) { + $defFile = Storage::get($this->defFilePath); + $this->info('Definition file read'); + $sectors = []; + $currentSector = []; + foreach (explode("\n", $defFile) as $key => $line) { + $line = str_replace("\r", '', $line); + $lineSplit = explode('|', $line); + if (10 == sizeof($lineSplit)) { + if ($key > 0) { + $sectors[] = $currentSector; + } + $currentSector = [ + 'icao' => $lineSplit[0], + 'isOceanic' => $lineSplit[1], + 'isExtension' => $lineSplit[2], + 'points' => [], + ]; + } else { + if ('' != $line) { + $currentSector['points'][] = [$lineSplit[0], $lineSplit[1]]; + } + } + } + if (!empty($currentSector)) { + $sectors[] = $currentSector; + } + + $this->info('Total general sector count: '.sizeof($sectors)); + $output['general'] = $sectors; + } else { + $this->error('Unable to locate definition file at: '.$this->defFilePath); + } + // + // Build sector borders based on sct data + if (Storage::exists($this->sctFilePath)) { + $this->info('Reading sct sector file data'); + $sctFile = \Storage::get($this->sctFilePath); + $sectors = []; + $currentSector = ''; + $coordinates = []; + foreach (explode("\r\n", $sctFile) as $key => $line) { + if ('' !== $line) { + $lineSplit = preg_split('/[\s]+/', $line); + if (!\Illuminate\Support\Str::startsWith($lineSplit[0], 'ED')) { + continue; + } + if ($lineSplit[0] !== $currentSector) { + $sectors[$currentSector] = $coordinates; + $currentSector = $lineSplit[0]; + $coordinates = []; + } else { + if (0 == sizeof($coordinates)) { + $coordinates[] = [$this->convertDMSToDecimal($lineSplit[1]), $this->convertDMSToDecimal($lineSplit[2])]; + $coordinates[] = [$this->convertDMSToDecimal($lineSplit[3]), $this->convertDMSToDecimal($lineSplit[4])]; + } else { + $coordinates[] = [$this->convertDMSToDecimal($lineSplit[3]), $this->convertDMSToDecimal($lineSplit[4])]; + } + } + } + } + $sectors[$currentSector] = $coordinates; + + $output['sectors'] = $sectors; + } + // + // Build local sectors from local .ese file + if (Storage::exists($this->eseFilePath)) { + $this->info('Started reading from .ese file'); + $eseFile = Storage::get($this->eseFilePath); + $positions = []; + $airspaces = []; + $currentSection = null; + $currentAirspace = null; + $currentSector = null; + foreach (explode("\n", $eseFile) as $key => $line) { + $line = str_replace("\r", '', $line); + if ('' == $line) { + continue; + } + // Switch sections + if ('[POSITIONS]' === $line) { + // start with positions + $currentSection = 'positions'; + $this->info('Started reading positions from .ese file'); + } + if ('[AIRSPACE]' === $line) { + $currentSection = 'airspace'; + $this->info('Started reading airspaces from .ese file'); + } + + // Read positions + if ('positions' === $currentSection) { + if ('[POSITIONS]' == $line) { + continue; + } + // EDWW_A_CTR:Bremen Radar:123.920:WA:A:EDWW:CTR:::2450:2477:N053.14.15.580:E009.14.23.499 + // ::::::::: + // :[::[: ... ]] + $split = explode(':', $line); + if (sizeof($split) < 12) { + // $this->info($line); + $positions[$split[3]] = [ + 'name' => $split[0], + 'callsign' => $split[1], + 'frequency' => $split[2], + 'lat' => '', + 'lon' => '', + ]; + } else { + $positions[$split[3]] = [ + 'name' => $split[0], + 'callsign' => $split[1], + 'frequency' => $split[2], + 'lat' => $split[11], + 'lon' => $split[12], + ]; + } + } + // Read airspace + if ('airspace' == $currentSection) { + $split = explode(':', $line); + if (sizeof($split) >= 2) { + if ('SECTOR' == $split[0]) { + if (null != $currentAirspace) { + $airspaces[$currentAirspace['name']] = $currentAirspace; + $currentAirspace = null; + } + $currentAirspace = [ + 'name' => str_replace('·', '_', $split[1]), + 'lowerLimit' => $split[2], + 'upperLimit' => $split[3], + ]; + } + if (null != $currentAirspace && 'OWNER' == $split[0]) { + $length = sizeof($split); // Get the amount of owners HIGH -> LOW + $ownership = []; + for ($i = 1; $i < $length; ++$i) { + $ownership[$i] = $split[$i]; + } + $currentAirspace['owners'] = $ownership; + } + } + } + } + if (null !== $currentAirspace) { + $airspaces[$currentAirspace['name']] = $currentAirspace; + $currentAirspace = null; + } + $output['vatger'] = [ + 'positions' => $positions, + 'airspace' => $airspaces, + ]; + Storage::put($this->sectorOutputFile, json_encode($output, JSON_PRETTY_PRINT)); + $this->info('Finished reading .ese file.'); + } + } + + private function convertDMSToDecimal($latlng) + { + $valid = false; + $decimal_degrees = 0; + $degrees = 0; + $minutes = 0; + $seconds = 0; + $direction = 1; + // Determine if there are extra periods in the input string + $num_periods = substr_count($latlng, '.'); + if ($num_periods > 1) { + $temp = preg_replace('/\./', ' ', $latlng, $num_periods - 1); // replace all but last period with delimiter + $temp = trim(preg_replace('/[a-zA-Z]/', '', $temp)); // when counting chunks we only want numbers + $chunk_count = count(explode(' ', $temp)); + if ($chunk_count > 2) { + $latlng = preg_replace('/\./', ' ', $latlng, $num_periods - 1); // remove last period + } else { + $latlng = str_replace('.', ' ', $latlng); // remove all periods, not enough chunks left by keeping last one + } + } + + // Remove unneeded characters + $latlng = trim($latlng); + $latlng = str_replace('º', ' ', $latlng); + $latlng = str_replace('°', ' ', $latlng); + $latlng = str_replace("'", ' ', $latlng); + $latlng = str_replace('"', ' ', $latlng); + $latlng = str_replace(' ', ' ', $latlng); + $latlng = substr($latlng, 0, 1).str_replace('-', ' ', substr($latlng, 1)); // remove all but first dash + if ('' != $latlng) { + // DMS with the direction at the start of the string + if (preg_match("/^([nsewoNSEWO]?)\s*(\d{1,3})\s+(\d{1,3})\s*(\d*\.?\d*)$/", $latlng, $matches)) { + $valid = true; + $degrees = intval($matches[2]); + $minutes = intval($matches[3]); + $seconds = floatval($matches[4]); + if ('S' == strtoupper($matches[1]) || 'W' == strtoupper($matches[1])) { + $direction = -1; + } + } elseif (preg_match( + // DMS with the direction at the end of the string + "/^(-?\d{1,3})\s+(\d{1,3})\s*(\d*(?:\.\d*)?)\s*([nsewoNSEWO]?)$/", + $latlng, + $matches + ) + ) { + $valid = true; + $degrees = intval($matches[1]); + $minutes = intval($matches[2]); + $seconds = floatval($matches[3]); + if ('S' == strtoupper($matches[4]) || 'W' == strtoupper($matches[4]) || $degrees < 0) { + $direction = -1; + $degrees = abs($degrees); + } + } + if ($valid) { + // A match was found, do the calculation + $decimal_degrees = ($degrees + ($minutes / 60) + ($seconds / 3600)) * $direction; + } else { + // Decimal degrees with a direction at the start of the string + if (preg_match("/^([nsewNSEW]?)\s*(\d+(?:\.\d+)?)$/", $latlng, $matches)) { + $valid = true; + if ('S' == strtoupper($matches[1]) || 'W' == strtoupper($matches[1])) { + $direction = -1; + } + $decimal_degrees = $matches[2] * $direction; + } elseif (preg_match("/^(-?\d+(?:\.\d+)?)\s*([nsewNSEW]?)$/", $latlng, $matches)) { + // Decimal degrees with a direction at the end of the string + $valid = true; + if ('S' == strtoupper($matches[2]) || 'W' == strtoupper($matches[2]) || $degrees < 0) { + $direction = -1; + $degrees = abs($degrees); + } + $decimal_degrees = $matches[1] * $direction; + } + } + } + if ($valid) { + return $decimal_degrees; + } else { + return false; + } + } +} diff --git a/app/Console/Commands/Network/DatafeedUpdate.php b/app/Console/Commands/Network/DatafeedUpdate.php new file mode 100644 index 0000000..f756c0b --- /dev/null +++ b/app/Console/Commands/Network/DatafeedUpdate.php @@ -0,0 +1,42 @@ +_teamSpeak = new TeamSpeak('vACC Germany TeamSpeak Daemon'); + } catch(TeamSpeak3_Transport_Exception $e) { + } + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + return; //disable cmd + if($this->_teamSpeak->getStatus()) { + // Register Events + $this->_teamspeak->getInstance()->notifyRegister('server'); + TeamSpeak3_Helper_Signal::getInstance()->subscribe('notifyCliententerview', self::class.'::clientJoinedEvent'); + TeamSpeak3_Helper_Signal::getInstance()->subscribe('notifyClientleftview', self::class.'::clientLeftEvent'); + + while(true) { + try { + + } catch(TeamSpeak3_Transport_Exception $e) { + sleep(30); + } + } + } else { + sleep(30); + + return $this->handle(); + } + } + + public static function clientJoinedEvent(TeamSpeak3_Adapter_ServerQuery_Event $event, TeamSpeak3_Node_Host $host) + { + if (0 != $event['client_type']) { + return; + } + try { + $client = $host->serverGetSelected()->clientGetById($event->clid); + $account = self::checkClient($client); + self::checkMemberStanding($client, $account); + } catch (TeamSpeak3_Adapter_ServerQuery_Exception $e) { + return; + } + } + + public static function clientLeftEvent(TeamSpeak3_Adapter_ServerQuery_Event $event, TeamSpeak3_Node_Host $host) + { + } + + /** + * Perform the checks we need to do. + * + * @param TeamSpeak3_Node_Client $client [description] + * + * @return [type] [description] + */ + private static function checkClient(TeamSpeak3_Node_Client $client) + { + $account = TeamSpeak::checkClient($client); + if (false !== $account && $account instanceof Account) { + // Check client description is okay. + $client = TeamSpeak::clientDescription($client, $account); + } + + return $account; + } + + /** + * If the member has been banned from our systems.. Ban him from TS too + * + * @param TeamSpeak3_Node_Client $client [description] + * @param Account $account [description] + * @return [type] [description] + */ + private static function checkMemberStanding(TeamSpeak3_Node_Client $client, Account $account) + { + // Check if the account has local / global network bans + if($account->isCurrentlyBanned) { + $cb = $account->currentBan; + + TeamSpeak::kickClient($client, $cb->reason); + sleep(5); // Make sure client is gone prior to the next step! + + $now = \Carbon\Carbon::now(); + TeamSpeak::banClient($client, $cb->reason, $now->diffInSeconds($cb->banned_till)); + } + } + +} diff --git a/app/Console/Commands/TeamSpeak/TeamSpeakWebqueryCommand.php b/app/Console/Commands/TeamSpeak/TeamSpeakWebqueryCommand.php new file mode 100644 index 0000000..e501d50 --- /dev/null +++ b/app/Console/Commands/TeamSpeak/TeamSpeakWebqueryCommand.php @@ -0,0 +1,43 @@ +command('network:datafeed')->everyMinute()->withoutOverlapping(); + // Update the statistics hourly + $schedule->command('stats:update')->hourlyAt(5)->withoutOverlapping(); + // Update the completed event routes + $schedule->command('eventroute:update')->hourlyAt(10)->withoutOverlapping(); + + // ONLY in production env + $schedule->command('forum:sync')->hourly()->withoutOverlapping()->environments(['production']); + + // ONLY in production env + $schedule->command('ts:webquery')->hourlyAt(30)->withoutOverlapping()->environments(['production']); + + // Remove unfinished registrations prior to account updates + $schedule->command('account:clear-unfinished-registrations')->dailyAt('02:00')->withoutOverlapping(); + + $schedule->command('account:connectupdater')->hourlyAt(45)->withoutOverlapping(60); + + // ONLY in production or staging env + $schedule->command('account:apiupdater')->hourlyAt(20)->withoutOverlapping()->environments(['staging', 'production']); + $schedule->command('account:apiupdater')->hourlyAt(40)->withoutOverlapping()->environments(['staging', 'production']); + // + $schedule->command('activitylog:clean')->daily(); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..59c585d --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,55 @@ +id . "/" + ); + $responseRatingsTimes = self::_fetchData( + $account, + "/ratings/" . $account->id . "/rating_times/" + ); + if ($responseRatings == false || collect($responseRatings)->isEmpty()) { + return false; + } + if ( + collect( + $responseRatingsTimes == false || $responseRatingsTimes + )->isEmpty() + ) { + return false; + } + $result = self::_insertData( + $account, + $responseRatings, + $responseRatingsTimes + ); + return $result; + } + + private static function _insertData( + Account $account, + $responseRatings, + $responseRatingsTimes + ) { + try { + $account->loadMissing("data"); + $account->data->rating_atc = $responseRatings->rating; + $account->data->registered_at = Carbon::parse( + $responseRatings->reg_date + ); + $account->data->time_atc = $responseRatingsTimes->atc; + $account->data->time_pilot = $responseRatingsTimes->pilot; + + $account->data->save(); + $account->save(); + try { + if ($account->data->rating_atc == 0) { + XenBridge::banForumAccount($account); + } + } catch (Exception $exception) { + } + return true; + } catch (Exception $exception) { + Log::error(" " . $exception->getMessage()); + return false; + } + } + + private static function _fetchData(Account $account, $urlEndpoint) + { + $uri = config("vatsim_api.base"); + $client = new Client([ + "base_uri" => $uri, + "headers" => ["Content-type" => "application/json"], + "connect_timeout" => 25, + ]); + try { + $uri .= $urlEndpoint; + $response = $client->get($uri); + if ( + $response->getStatusCode() < 200 || + $response->getStatusCode() > 299 + ) { + return false; + } + return json_decode($response->getBody()); + } catch (Exception $exception) { + return false; + } + } +} diff --git a/app/Helpers/ConnectHelper.php b/app/Helpers/ConnectHelper.php new file mode 100644 index 0000000..c7e53fa --- /dev/null +++ b/app/Helpers/ConnectHelper.php @@ -0,0 +1,139 @@ +token_expires)->isPast()) { + $refresh = self::_refreshToken($account); + if (!$refresh) { + try { + $prevaccount = Auth::user(); + Auth::setUser($account); + Auth::logout(); + Auth::setUser($prevaccount); + } catch (\Throwable $th) { + //throw $th; + } + return false; + //$account->access_token = null; + //$account->refresh_token = null; + //$account->token_expires = null; + //$account->save(); + } + $account->access_token = $refresh->access_token; + $account->refresh_token = $refresh->refresh_token; + $account->token_expires = now()->addSeconds($refresh->expires_in)->timestamp; + $account->save(); + } + $response = self::_fetchAccount($account); + if ($response == false ||collect($response)->isEmpty()) return false; + $result = self::_insertData($account, $response); + return $result; + } + + private static function _insertData(Account $account, $response) + { + try { + $account->loadMissing('data'); + if (!Account::where('email', $response->data->personal->email)->where('id', '!=', $account->id)->exists()){ + $account->email = $response->data->personal->email; // Need to check first if dupe + } + $account->firstname = mb_convert_encoding($response->data->personal->name_first, "Windows-1252", "UTF-8"); + $account->lastname = mb_convert_encoding($response->data->personal->name_last, "Windows-1252", "UTF-8"); + + $account->data->rating_atc = $response->data->vatsim->rating->id; + $account->data->rating_pilot = $response->data->vatsim->pilotrating->id; + + $account->data->active = $account->data->rating_atc > 0; + $account->data->suspended = $account->data->rating_atc < 0; + + /* + * Update accounts region/division associations + */ + // User country + $account->data->country_code = $response->data->personal->country->id; + $account->data->country_name = $response->data->personal->country->name; + // User Region + $account->data->region_code = $response->data->vatsim->region->id; + $account->data->region_name = $response->data->vatsim->region->name; + // User Division + $account->data->division_code = $response->data->vatsim->division->id; + $account->data->division_name = $response->data->vatsim->division->name; + // User Subdivision + $account->data->subdivision_code = $response->data->vatsim->subdivision->id; + $account->data->subdivision_name = $response->data->vatsim->subdivision->name; + $account->data->save(); + $account->save(); + //to many logs + //activity() + // ->causedBy($account) + // ->performedOn($account) + // ->log("Die SSO Daten wurden automatisch aktualisiert!"); + return true; + } catch (\Exception $exception) { + return false; + } + } + + private static function _fetchAccount(Account $account) + { + $uri = config('vatsim_auth.base'); + $client = new Client([ + 'base_uri' => $uri, + 'headers' => [ + 'Content-type' => 'application/json', + 'Authorization' => 'Bearer ' . $account->access_token, + ], + ]); + try { + $response = $client->get($uri . '/api/user'); + return json_decode($response->getBody()); + } catch (\Exception $exception) { + $account->token_expires = 1; + $account->save(); + return false; + } + + } + + private static function _refreshToken(Account $account) + { + $uri = config('vatsim_auth.base'); + $client = new Client([ + 'base_uri' => $uri, + 'headers' => [ + 'Content-type' => 'application/json', + ], + ]); + try { + $response = $client->post($uri . '/oauth/token', [ + 'form_params' => [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('vatsim_auth.id'), + 'client_secret' => config('vatsim_auth.secret'), + 'scope' => str_replace(',', ' ', config('vatsim_auth.scopes')), + ], + ]); + + if ($response->getStatusCode() != 200) { + return false; + } + + return json_decode($response->getBody()); + } catch (\Exception $exception) { + return false; + } + + } +} diff --git a/app/Helpers/GroupHelper.php b/app/Helpers/GroupHelper.php new file mode 100644 index 0000000..cfc31ee --- /dev/null +++ b/app/Helpers/GroupHelper.php @@ -0,0 +1,42 @@ +assignGroup($groups); + return true; + } catch(Exception $exception) { + return false; + } + } + + public static function assignViaId($user_id, $groups = []) { + $user = Account::find($user_id); + if($user == null) + return false; + return self::assign($user, $groups); + } + + public static function revoke(Account $user, $groups = []) { + try { + $user->revokeGroup($groups); + return true; + } catch(Exception $exception) { + return false; + } + } + + public static function revokeViaId($user_id, $groups = []) { + $user = Account::find($user_id); + if($user == null) + return false; + return self::revoke($user, $groups); + } +} diff --git a/app/Helpers/PermissionHelper.php b/app/Helpers/PermissionHelper.php new file mode 100644 index 0000000..8390f35 --- /dev/null +++ b/app/Helpers/PermissionHelper.php @@ -0,0 +1,28 @@ +assignPermissions($permissions); + return true; + } catch(Exception $exception) { + return false; + } + } + + public function revoke(Account $user, $permissions = []) { + try { + $user->revokePermissions($permissions); + return true; + } catch(Exception $exception) { + return false; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/APIController.php b/app/Http/Controllers/APIController.php new file mode 100644 index 0000000..fef640a --- /dev/null +++ b/app/Http/Controllers/APIController.php @@ -0,0 +1,37 @@ +header('Authorization', ''); + $token = str_replace('Token ', '', $header); + if(strcmp($token, config('teamspeak.homepage_api_key')) != 0) + { + abort(400,'API incorrect or not provided'); + return; + } + $regs = Registration::query()->where('account_id',$account_id)->get(); + $res = []; + foreach ($regs as $r) + { + $res[] = $r->dbid; + } + return response()->json($res); + } + + public function isger(string $account_id, Request $request) + { + $acc = Account::query()->where('id', $account_id)->first(); + if(empty($acc)) return response()->json(false); + $acc->loadMissing('regionalgroups'); + return response()->json($acc->regionalgroups->count() > 0); + } +} diff --git a/app/Http/Controllers/ATD/ATDController.php b/app/Http/Controllers/ATD/ATDController.php new file mode 100644 index 0000000..09cc51b --- /dev/null +++ b/app/Http/Controllers/ATD/ATDController.php @@ -0,0 +1,34 @@ +ajax()) abort(403); + + if(Auth::check() && $this->account->setup_completed) { + $solos = \App\Models\ATD\SoloEndorsement::orderBy('ends_at', 'ASC') + ->with('station', 'candidate:id,firstname,lastname') + ->get(); + } else { + $solos = \App\Models\ATD\SoloEndorsement::orderBy('ends_at', 'ASC') + ->with('station') + ->get(); + } + return $solos; + } + + +} diff --git a/app/Http/Controllers/ATD/TrainingController.php b/app/Http/Controllers/ATD/TrainingController.php new file mode 100644 index 0000000..1d14b0f --- /dev/null +++ b/app/Http/Controllers/ATD/TrainingController.php @@ -0,0 +1,198 @@ + [ + 'pattern' => '/\[color\=(.*?)\](.*?)\[\/color]/s', + 'replace' => '$2', + 'content' => '$2', + ], + 'center' => [ + 'pattern' => '/\[center\](.*?)\[\/center]/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'size' => [ + 'pattern' => '/\[size\=(.*?)\](.*?)\[\/size]/s', + 'replace' => '$2', + 'content' => '$2', + ], + 'table' => [ + 'pattern' => '/\[table\]\n?(.*?)\[\/table\]\n?/s', + 'replace' => '$1
', + 'content' => '$1', + ], + 'table-row' => [ + 'pattern' => '/\[tr\]\n?(.*?)\[\/tr\]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'table-data' => [ + 'pattern' => '/\[td\]\n?(.*?)\[\/td\]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'table-head' => [ + 'pattern' => '/\[th\]\n?(.*?)\[\/th]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'link' => [ + 'pattern' => '/\[url\](.*?)\[\/url\]/s', + 'replace' => '
$1', + 'content' => '$1' + ], + 'namedlink' => [ + 'pattern' => '/\[url\=\'?(.*?)\'?\](.*?)\[\/url\]/s', + 'replace' => '$2', + 'content' => '$2' + ], + ]; + + function __construct() + { + parent::__construct(); + } + + public function index(Request $request) + { + if($request->ajax()) { + $trainings = Training::forTrainee($this->account->id)->with('regionalgroup.fir', 'sessions.station', 'sessions.mentor', 'sessions.secondMentor')->get(); + + return $trainings; + } + } + + public function create(Request $request) + { + if($request->ajax()) { + + $validated = $request->validate( + [ + 'rg' => 'required|exists:regionalgroups_regionalgroups,id', + ] + ); + + // Check if the user is at least a guest member of that regionalgroup + $rg = Regionalgroup::find($validated['rg']); + if($this->account->isMemberOfRegionalgroup($rg) || $this->account->isGuestOfRegionalgroup($rg)) { + + // Disable training requests until further notice from VATEUD + // regarding European trainingssystems. + // + // // Check if there is a training already for that regionalgroup. + // $openTraining = Training::where('trainee_id', $this->account->id)->where('regionalgroup_id', $validated['rg'])->exists(); + // if($openTraining) { + // // Send a broadcast notification the the user + // $this->account->notify(new \App\Notifications\ATD\TrainingAlreadyRequestedNotification); + // } else { + // $training = new Training; + // $training->trainee_id = $this->account->id; + // $training->regionalgroup_id = $validated['rg']; + // $training->save(); + // } + // return response()->json(['redirect' => true]); + // + $this->account->notify(new \App\Notifications\ATD\SeeForumNotification); + return response()->json(['redirect' => false]); + } else { + $this->account->notify(new \App\Notifications\ATD\NoMemberOfRegionalgroupNotification($rg->name)); + return response()->json(['redirect' => false]); + } + } + } + + public function createSession(Request $request, Training $training) + { + if($request->ajax()) { + $validated = $request->validate( + [ + 'newSessionRequest.type' => 'required|in:1,2,3', + 'newSessionRequest.station' => 'required|exists:navigation_stations,id', + 'newSessionRequest.start' => 'required|date_format:"d.m.Y H:i"', + 'newSessionRequest.end' => 'required|date_format:"d.m.Y H:i"', + ] + ); + + $openRequests = $training->sessions()->where('accepted', false)->exists(); + if($openRequests) { + // Break here and send a broadcast notification the the user + $this->account->notify(new \App\Notifications\ATD\SessionAlreadyRequestedNotification); + die; + } + + $newSession = new TrainingSession; + $newSession->training_id = $training->id; + $newSession->type = $validated['newSessionRequest']['type']; + $newSession->station_id = $validated['newSessionRequest']['station']; + $newSession->started_at = $validated['newSessionRequest']['start']; + $newSession->ended_at = $validated['newSessionRequest']['end']; + $newSession->save(); + + $newSession->loadMissing('station'); + + return $newSession; + } + } + + public function removeSession(Request $request, Training $training, TrainingSession $trainingsession) + { + if($request->ajax()) { + $result = $trainingsession->delete(); + return response()->json(['result' => $result]); + } + } + + public function getAvailableRegionalgroups(Request $request) + { + if($request->ajax()) { + return Regionalgroup::orderBy('name', 'ASC')->get(); + } + } + + /** + * Get the regionalgroup training status from the forum + * @return [type] [description] + */ + public function status() + { + if( $this->account->setting->language == 'de') { + $post = \App\Libraries\XenBridge::getPost('833800')->post; + } + else{ + $post = \App\Libraries\XenBridge::getPost('833802')->post; + } + + $status = $post->message; + + $bbCode = new \Genert\BBCode\BBCode(); + $bbCode->addLinebreakParser(); + foreach ($this->customParsers as $name => $parser) { + $bbCode->addParser( + $name, + $parser['pattern'], + $parser['replace'], + $parser['content'], + ); + } + // Parse the message through the bbcode + $status = $bbCode->convertToHtml($status, \Genert\BBCode\BBCode::CASE_SENSITIVE); + $status = $bbCode->stripBBCodeTags($status); + + return response()->json( + $status, + 200 + ); + } + +} diff --git a/app/Http/Controllers/Administration/ATD/ATDController.php b/app/Http/Controllers/Administration/ATD/ATDController.php new file mode 100644 index 0000000..098293e --- /dev/null +++ b/app/Http/Controllers/Administration/ATD/ATDController.php @@ -0,0 +1,334 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.atd')) + abort(403); + + return $next($request); + }); + } + + public function index(Request $request) + { + if(!$request->ajax()) abort(403); + } + + /** + * Get all solo endorsements inclusive requests + * + * @param Request $request [description] + * @return [type] [description] + */ + public function solos(Request $request) + { + if(!$request->ajax()) abort(403); + + $solos = \App\Models\ATD\SoloEndorsement::orderBy('ends_at', 'ASC') + ->with('candidate', 'phase', 'station') + ->get(); + + $regionalgroups = \App\Models\Regionalgroups\Regionalgroup::all(); + + $stations = \App\Models\Navigation\Station::orderBy('name', 'ASC')->get(); + $phases = \App\Models\ATD\SoloPhase::all(); + + return response()->json( + [ + 'solos' => $solos, + 'regionalgroups' => $regionalgroups, + 'stations' => $stations, + 'phases' => $phases, + 'success' => true, + ], + 200 + ); + } + + /** + * Approve or revoke a solo endorsement. + * + * @param Request $request [description] + * + * @return [type] [description] + */ + public function approveSolo(Request $request) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'solo' => 'required|exists:uts_atd_solo_clearances,id', + 'approved' => 'required|boolean', + ] + ); + + $solo = \App\Models\ATD\SoloEndorsement::find($validated['solo']); + $solo->loadMissing('candidate', 'station'); + $solo->approved = $validated['approved']; + $solo->save(); + + // Notify the candidate and also provide feedback for the authorized user + if ($solo->approved) { + $solo->candidate->notify(new SoloApprovedNotification($solo->station->ident)); + } else { + $solo->candidate->notify(new SoloDeniedNotification($solo->station->ident)); + } + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } + + /** + * Deletes a solo endorsement. + * + * @param Request $request [description] + * + * @return [type] [description] + */ + public function deleteSolo(Request $request) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'solo' => 'required|exists:uts_atd_solo_clearances,id', + ] + ); + + $solo = \App\Models\ATD\SoloEndorsement::find($validated['solo']); + $solo->loadMissing('candidate', 'station'); + + $c = $solo->candidate; + $s = $solo->station->ident; + + $solo->delete(); + + $c->notify(new SoloDeniedNotification($s)); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } + + /** + * Extends a solo by 30 days. + * + * @param Request $request [description] + * @param \App\Models\ATD\SoloEndorsement $solo [description] + * + * @return [type] [description] + */ + public function extendSolo(Request $request, \App\Models\ATD\SoloEndorsement $solo) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'extension' => 'required|integer', + ] + ); + + $solo->loadMissing('candidate', 'station'); + + $solo->extensions = $validated['extension']; + $solo->ends_at = $solo->calculateEndDate($solo->ends_at, true); + $solo->save(); + + $solo->candidate->notify(new SoloExtendedNotification($solo->station->ident)); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } + + /** + * Switch the station an endorsement is valid for. + * + * @param Request $request [description] + * @param \App\Models\ATD\SoloEndorsement $solo [description] + * + * @return [type] [description] + */ + public function switchStation(Request $request, \App\Models\ATD\SoloEndorsement $solo) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'station' => 'required|exists:navigation_stations,id', + ] + ); + + $solo->loadMissing('candidate', 'station'); + + $solo->candidate->notify(new SoloDeniedNotification($solo->station->ident)); + + $solo->station_id = $validated['station']; + $solo->save(); + + $solo->refresh(); + $solo->loadMissing('candidate', 'station'); + $solo->candidate->notify(new SoloApprovedNotification($solo->station->ident)); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } + + /** + * Forwards a soloendorsement to a new phase. + * Recalculating the end date based upon the current end date. + * + * @param Request $request [description] + * @param \App\Models\ATD\SoloEndorsement $solo [description] + * + * @return [type] [description] + */ + public function forwardPhase(Request $request, \App\Models\ATD\SoloEndorsement $solo) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'phase' => 'required|exists:uts_atd_solophases,id', + ] + ); + + $solo->loadMissing('candidate', 'station'); + + $solo->solophase_id = $validated['phase']; + $solo->ends_at = $solo->calculateEndDate($solo->ends_at, false); + if (false !== $solo->ends_at) { + $solo->save(); + + $solo->candidate->notify(new SoloModifiedNotification($solo->station->ident)); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } else { + return response()->json( + [ + 'success' => false, + ], + 200 + ); + } + } + + /** + * Switch the phase of an endorsement completely. + * Recalculating the new end date based on the original begin. + * + * @param Request $request [description] + * @param \App\Models\ATD\SoloEndorsement $solo [description] + * + * @return [type] [description] + */ + public function switchPhase(Request $request, \App\Models\ATD\SoloEndorsement $solo) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate( + [ + 'phase' => 'required|exists:uts_atd_solophases,id', + ] + ); + + $solo->loadMissing('candidate', 'station'); + + $solo->solophase_id = $validated['phase']; + $solo->ends_at = $solo->calculateEndDate($solo->begins_at, false); + if (false !== $solo->ends_at) { + $solo->save(); + + $solo->candidate->notify(new SoloModifiedNotification($solo->station->ident)); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } else { + + return response()->json( + [ + 'success' => false, + ], + 200 + ); + } + } + + /** + * Create a new solo entry + * @param Request $request [description] + * @return [type] [description] + */ + public function createSolo(Request $request) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate([ + 'newSolo.cid' => 'required|exists:membership_accounts,id', + 'newSolo.begin' => 'required', + 'newSolo.phase' => 'required|exists:uts_atd_solophases,id', + 'newSolo.station' => 'required|exists:navigation_stations,id', + ]); + + $newSolo = new \App\Models\ATD\SoloEndorsement; + $newSolo->station_id = $validated['newSolo']['station']; + $newSolo->solophase_id = $validated['newSolo']['phase']; + $newSolo->candidate_id = $validated['newSolo']['cid']; + $newSolo->begins_at = \Carbon\Carbon::createFromFormat('d.m.Y', $validated['newSolo']['begin']); + $newSolo->ends_at = $newSolo->calculateEndDate($newSolo->begins_at); + $newSolo->approved = true; + $newSolo->save(); + $newSolo->refresh(); + $newSolo->loadMissing('candidate', 'station'); + $newSolo->candidate->notify(new SoloApprovedNotification($newSolo->station->ident)); + + return response()->json( + [ + 'newSolo' => $newSolo, + 'success' => true, + ], + 200 + ); + } + +} diff --git a/app/Http/Controllers/Administration/ATD/EuroScopeController.php b/app/Http/Controllers/Administration/ATD/EuroScopeController.php new file mode 100644 index 0000000..64bde85 --- /dev/null +++ b/app/Http/Controllers/Administration/ATD/EuroScopeController.php @@ -0,0 +1,183 @@ + 0001, 'max' => 7777]; + private $_initialPseudoPilot = ''; + + function __construct() + { + parent::__construct(); + } + + /** + * Parameter validation and initial calculations + * + * @param Request $request [description] + * @return [type] [description] + */ + public function createScenario(Request $request) + { + $validated = $request->validate([ + 'icao' => 'required|string|size:4', + 'holdings' => 'nullable', + 'trafficScale' => 'required|numeric|min:1|max:100', + 'depArrScale' => 'required|numeric|min:0|max:100', + 'minSquawk' => 'required|numeric|min:0001|max:7777|lt:maxSquawk', + 'maxSquawk' => 'required|numeric|min:0001|max:7777|gt:minSquawk', + 'initialPseudo' => 'required|string', + ]); + + $this->_aerodrome = \App\Models\Navigation\Aerodrome::icao($validated['icao'])->with('stations', 'runways')->first(); + $this->_holdings = $validated['holdings'] ?? false; + $this->_squawkRange['min'] = $validated['minSquawk']; + $this->_squawkRange['max'] = $validated['maxSquawk']; + $this->_initialPseudoPilot = $validated['initialPseudo']; + + $totalTraffic = $this->_maximumFlightsPerSession * ($validated['trafficScale'] / 100); + + $this->_departures = round($totalTraffic * (($validated['depArrScale'] / 100))); + $this->_arrivals = round($totalTraffic * (1 - $validated['depArrScale'] / 100)); + + $this->_scenario = "BUILDING SCENARIO WITH:\n"; + $this->_scenario.= "Total Traffic Count: ".$totalTraffic."\n"; + $this->_scenario.= "Departure Traffic Count: ".$this->_departures."\n"; + $this->_scenario.= "Arrival Traffic Count: ".$this->_arrivals."\n;=============================================\n"; + + return $this->_buildScenario($request); + } + + /** + * Logic to build up a scenario + * + * @param Request $request [description] + * @return [type] [description] + */ + private function _buildScenario(Request $request) + { + $this->_scenario.= "AIRPORT_ALT:".$this->_aerodrome->elevation."\n"; + + $this->_scenario.= "\n"; + foreach ($this->_aerodrome->runways as $runway) { + $this->_scenario.= "ILS".$runway->ident.":::".$runway->heading."\n"; + } + + if($this->_holdings !== false) { + $this->_scenario.= "\n"; + $holdings = explode("\n", $this->_holdings); + foreach ($holdings as $h) { + $this->_scenario.= 'HOLDING:'.$h."\n"; + } + } + + $this->_scenario.= "\n"; + foreach ($this->_aerodrome->stations as $station) { + $this->_scenario.= "CONTROLLER:".$station->ident.":".$station->fixedFrequency."\n"; + } + + $this->_scenario.= "\n"; + $this->_departureRoutes = $this->_findFlightplans($this->_aerodrome->icao, true); + $this->_arrivalRoutes = $this->_findFlightplans($this->_aerodrome->icao, false); + + foreach ($this->_departureRoutes as $dr) { + $this->_renderFlight($dr); + } + foreach ($this->_arrivalRoutes as $ar) { + $this->_renderFlight($ar); + } + + if($request->ajax()) { + return $this->_scenario; + } + } + + /** + * Build the flightplan format for the scenario file + * + * @param [type] $flight [description] + * @return [type] [description] + */ + protected function _renderFlight($flight) + { + // @:::1::::0::0 + // $FP:*A::::::::::::::: + $this->_scenario.="@N:$flight->callsign:".$this->_renderSquawk().":1:$flight->current_latitude:$flight->current_longitude:$flight->current_altitude:0:".$this->_calculateInitialHeading($flight->current_heading).":0\n"; + $this->_scenario.='$FP'."$flight->callsign:*A:$flight->flight_type:$flight->aircraft::$flight->departure_airport:".$flight->connected_at->format("Hi").":".$flight->connected_at->format("Hi").":$flight->cruise_altitude:$flight->arrival_airport:::::$flight->alternative_airport:$flight->remarks:$flight->route\n"; + $this->_scenario.='$ROUTE:FPA'."\n".'START:'.rand(0,1)."\n".'DELAY:'.rand(0,2).":".rand(2,5)."\nINITIALPSEUDOPILOT:$this->_initialPseudoPilot\n\n"; + } + + /** + * Grab some flightplans from our realtime tracking + * + * @param [type] $icao [description] + * @param boolean $from [description] + * @return [type] [description] + */ + protected function _findFlightplans($icao, $from = true) + { + if($from) { + // Find Departure Routes + return \App\Models\Network\PilotClient::online()->where('departure_airport', $icao)->take($this->_departures)->get(); + } else { + return \App\Models\Network\PilotClient::online()->where('arrival_airport', $icao)->take($this->_arrivals)->get(); + } + } + + /** + * Calculate the initail heading to match EuroScope's screenspace + * + * @param [type] $hdg [description] + * @return [type] [description] + */ + protected function _calculateInitialHeading($hdg) + { + return ((int) ( $hdg * 2.88 + 0.5 ) << 2 ); + } + + /** + * Find a squawk + * + * @return [type] [description] + */ + protected function _renderSquawk() + { + $squawk = rand($this->_squawkRange['min'], $this->_squawkRange['max']); + while(!$this->_isSquawkValid($squawk)) + $squawk = rand($this->_squawkRange['min'], $this->_squawkRange['max']); + return $squawk; + } + + /** + * Is a given 4 digit squawk code within valid range? + * So that any digit is less or equal to 7. + * + * @param [type] $sq [description] + * @return boolean [description] + */ + protected function _isSquawkValid($sq) { + return preg_match("/^[0-7]{4}/", $sq) && $sq < 7778; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Administration/Dashboard/DashboardController.php b/app/Http/Controllers/Administration/Dashboard/DashboardController.php new file mode 100644 index 0000000..699417d --- /dev/null +++ b/app/Http/Controllers/Administration/Dashboard/DashboardController.php @@ -0,0 +1,58 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration')) + abort(403); + + return $next($request); + }); + } + + public function index() + { + return $this->viewMake('administration.dashboard.index'); + } + + public function getStatistics(Request $request) + { + if(!$request->ajax()) abort(403); + + $accounts = \App\Models\Membership\Account::with('data')->get(); + $memberCount = $accounts->count(); + $inactive = $accounts->where('data.active', false)->count(); + $nonDivision = $accounts->whereNotIn('data.subdivision_code', ['GER'])->count(); + $division = $accounts->where('data.subdivision_code', 'GER')->count(); + + $now = \Carbon\CarbonImmutable::now(); + $yesterday = $now->subDay(); + + $flights = \App\Models\Statistic\FlightData::whereBetween('disconnected_at', [$yesterday, $now])->count(); + $atcs = \App\Models\Statistic\AtcData::whereBetween('disconnected_at', [$yesterday, $now])->count(); + + return response()->json( + [ + 'members' => $memberCount, + 'inactive' => $inactive, + 'nonDivision' => $nonDivision, + 'division' => $division, + 'flights' => $flights, + 'atcs' => $atcs, + ] + ); + + } + +} diff --git a/app/Http/Controllers/Administration/Events/EventRoutesController.php b/app/Http/Controllers/Administration/Events/EventRoutesController.php new file mode 100644 index 0000000..2a02ccc --- /dev/null +++ b/app/Http/Controllers/Administration/Events/EventRoutesController.php @@ -0,0 +1,134 @@ +middleware(function ($request, $next) { + if(!$request->ajax()) abort(400); + if(!$this->account->hasPermission('administration.event.routes')) abort(403); + return $next($request); + }); + } + + public function listRoutes() + { + return EventRoute::all(); + } + + public function routeDetails(EventRoute $er) + { + return $er->legs()->with('accounts:id,firstname,lastname')->with('departureAerodrome')->with('arrivalAerodrome')->get(); + } + + + public function createRoute(Request $request){ + $validated = $request->validate([ + 'route.name' => 'required|string', + 'route.begins_at' => 'required|date', + 'route.ends_at' => 'required|date', + 'route.require_order' => 'required|integer', + 'route.flight_rules' => 'nullable|string', + //'route.aircrafts' => 'string', + ]); + $er = new EventRoute(); + $er->name = $validated['route']['name']; + $er->description = ""; + $er->link = ""; + $er->img_url = ""; + $er->begins_at = $validated['route']['begins_at']; + $er->ends_at = $validated['route']['ends_at']; + $er->require_order = $validated['route']['require_order']; + $er->flight_rules = $validated['route']['flight_rules']; + $er->aircrafts = ''; + + $er->save(); + return $er; + } + + public function editRoute(EventRoute $er, Request $request){ + + //todo do all fields + + $validated = $request->validate([ + 'route.name' => 'required|string', + 'route.description' => 'string|nullable', + 'route.link' => 'string|nullable', + 'route.img_url' => 'string|nullable', + 'route.begins_at' => 'required|date', + 'route.ends_at' => 'required|date', + 'route.require_order' => 'required|integer', + 'route.flight_rules' => 'nullable|string', + 'route.aircrafts' => 'string|nullable', + 'route.forum_badge_id' => 'integer|nullable', + ]); + $er->name = $validated['route']['name']; + $er->description = empty($validated['route']['description']) ? '' : $validated['route']['description']; + $er->link = empty($validated['route']['link']) ? '' : $validated['route']['link']; + $er->img_url = empty($validated['route']['img_url']) ? '' : $validated['route']['img_url']; + $er->begins_at = $validated['route']['begins_at']; + $er->ends_at = $validated['route']['ends_at']; + $er->require_order = $validated['route']['require_order']; + $er->flight_rules = $validated['route']['flight_rules']; + $er->aircrafts = empty($validated['route']['aircrafts']) ? '' : $validated['route']['aircrafts']; + $er->forum_badge_id = empty($validated['route']['forum_badge_id']) ? NULL : $validated['route']['forum_badge_id']; + + $er->save(); + return $er; + } + + public function deleteRoute(EventRoute $er){ + foreach ($er->legs as $leg) $this->deleteLeg($er, $leg); + $er->delete(); + } + + public function createLeg(EventRoute $er, Request $request){ + $validated = $request->validate([ + 'leg.arrivalaerodrome_icao' => 'required|exists:App\Models\Navigation\Aerodrome,icao', + 'leg.departureaerodrome_icao' => 'required|exists:App\Models\Navigation\Aerodrome,icao', + ]); + $leg = new RouteLeg(); + $leg->route_id = $er->id; + $leg->arrivalaerodrome_id = Aerodrome::where('icao', 'LIKE', $validated['leg']['arrivalaerodrome_icao'])->firstOrFail()->id; + $leg->departureaerodrome_id = Aerodrome::where('icao', 'LIKE', $validated['leg']['departureaerodrome_icao'])->firstOrFail()->id; + $leg->save(); + + // attach the already participating accounts + if($er->legs()->count() > 0){ + foreach ($er->legs()->first()->accounts as $acc) + $leg->accounts()->attach($acc); + } + return $leg; + } + + public function deleteLeg(EventRoute $er, RouteLeg $leg){ + $leg->accounts()->detach(); + return $leg->delete(); + } + + public function manualCompleteLeg(EventRoute $er, RouteLeg $leg, Request $request){ + $validated = $request->validate([ + 'account_id' => 'required|exists:App\Models\Membership\Account,id', + 'completed_at' => 'required|date', + ]); + + $leg->accounts()->updateExistingPivot($validated['account_id'],['completed_at' => $validated['completed_at'], 'fight_data_id' => null]); + } + + public function showFlightData(FlightData $flightdata){ + return $flightdata; + } +} diff --git a/app/Http/Controllers/Administration/Filebase/DownloadController.php b/app/Http/Controllers/Administration/Filebase/DownloadController.php new file mode 100644 index 0000000..0ad72d6 --- /dev/null +++ b/app/Http/Controllers/Administration/Filebase/DownloadController.php @@ -0,0 +1,27 @@ +filePath) && Storage::exists( urldecode($request->filePath))) + return response()->download(storage_path('app').'/'.urldecode($request->filePath))->deleteFileAfterSend(true); + else + abort(404); + } + +} diff --git a/app/Http/Controllers/Administration/Filebase/ImageController.php b/app/Http/Controllers/Administration/Filebase/ImageController.php new file mode 100644 index 0000000..0676355 --- /dev/null +++ b/app/Http/Controllers/Administration/Filebase/ImageController.php @@ -0,0 +1,122 @@ +account->hasPermission('administration.images.manage')) + { + if($this->account->hasPermission('administration.images.upload')) { + return Image::accountId($this->account->id)->orderBy('name', 'ASC')->get(); + } + abort(403); + } + + return Image::with('account')->orderBy('name', 'ASC')->get(); + } + + public function myImages(Request $request) + { + if($this->account->hasPermission('administration.images.upload')) { + return Image::accountId($this->account->id)->orderBy('name', 'ASC')->get(); + } + abort(403); + } + + public function getImage(Request $request, Image $image) + { + if(!$this->account->hasPermission('administration.images.manage')) + abort(403); + + if(Storage::exists($image->path)) + return base64_encode(Storage::get($image->path)); + else { + $image->delete(); + return false; + } + } + + public function uploadImage(Request $request) + { + if(!$this->account->hasPermission('administration.images.upload')) + abort(403); + + $validated = $request->validate([ + 'imgFile' => 'required|file|image|max:5000', + 'imgLicense' => 'required|accepted' + ]); + + $filename = $request->file('imgFile')->getClientOriginalName(); + $fileext = $request->file('imgFile')->extension(); + + $now = \Carbon\Carbon::now()->timestamp; + + $filename = $now.'_'.$filename; + + // Store the file + $path = Storage::putFileAs('images/'.$this->account->id, $request->file('imgFile'), $filename); + + // Insert into database here + $image = new Image; + $image->account_id = $this->account->id; + $image->path = $path; + $image->name = $filename; + $image->ext = $fileext; + $image->approved = true; // For now auto approve images + $image->save(); + + // return a response + return response()->json([ + 'image' => $image + ]); + } + + public function approveImage(Request $request, Image $image) + { + if(!$this->account->hasPermission('administration.images.manage')) + abort(403); + + $image->approved = true; + return $image->save(); + } + + public function denyImage(Request $request, Image $image) + { + if(!$this->account->hasPermission('administration.images.manage')) + abort(403); + + $image->approved = false; + return $image->save(); + } + + public function deleteImage(Request $request, Image $image) + { + if(!$this->account->hasPermission('administration.images.manage')) + { + if(!$this->account->hasPermission('administration.images.upload') && $image->account_id != $this->account->id) + { + abort(403); + } + } + + if(Storage::exists($image->path)) { + Storage::delete($image->path); + } + + return $image->delete(); + } + +} diff --git a/app/Http/Controllers/Administration/Forum/ForumgroupController.php b/app/Http/Controllers/Administration/Forum/ForumgroupController.php new file mode 100644 index 0000000..82a3ac2 --- /dev/null +++ b/app/Http/Controllers/Administration/Forum/ForumgroupController.php @@ -0,0 +1,77 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.forumgroups')) + abort(403); + + return $next($request); + }); + } + + /** + * Gets all forumgroups known to our local system + * + * @param Request $request [description] + * @return [type] [description] + */ + public function index(Request $request) { + if(!$request->ajax()) return abort(403); + + $forumgroups = ForumGroup::all(); + + return $forumgroups; + } + + /** + * Adds a new forumgroup to our local system + * + * @param Request $request [description] + * @return [type] [description] + */ + public function create(Request $request) { + $validated = $request->validate([ + 'name' => 'required|string', + 'id' => 'required|numeric', + ]); + + /** + * If there is already a group with that id, we must not continue + */ + if(ForumGroup::where('forum_id', $validated['id'])->exists()) { + $this->account->notify(new \App\Notifications\Administration\Forum\GroupAlreadyExistsNotification($validated['id'])); + return response()->json(false); + } + + $fg = new ForumGroup; + $fg->forum_id = $validated['id']; + $fg->name = $validated['name']; + $fg->save(); + + return $fg; + } + + public function remove(Request $request, ForumGroup $fg) + { + $fid = $fg->id; + + $fg->groups()->detach(); + + $fg->delete(); + + return $fid; + } + +} diff --git a/app/Http/Controllers/Administration/Membership/AccountController.php b/app/Http/Controllers/Administration/Membership/AccountController.php new file mode 100644 index 0000000..a43aa12 --- /dev/null +++ b/app/Http/Controllers/Administration/Membership/AccountController.php @@ -0,0 +1,188 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.membership')) + abort(403); + + return $next($request); + }); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + $allAccounts = Account::orderBy('lastname', 'ASC')->get(); + + return $allAccounts; + } + + /** + * Display the specified resource. + * + * @param Account $account + * @return View + */ + public function show(Account $account) + { + $actionsOnAccount = Activity::forSubject($account)->get(); + $actionsCausedBy = Activity::causedBy($account)->get(); + $account->loadMissing('data', 'setting', 'regionalgroups', 'notes.author', 'bans.author', 'teamspeakRegistrations'); + return ['account' => $account, 'actions' => $actionsOnAccount->merge($actionsCausedBy)]; + } + + /** + * @param Request $request + * @param Account $account + * @return RedirectResponse + */ + public function addNote(Request $request, Account $account) + { + + $validated = $request->validate( + [ + 'newNote' => 'required|string', + ] + ); + + // Create a new note and assign to the account + $newNote = new Note([ + 'note' => $validated['newNote'], + 'author_id' => $this->account->id, + 'account_id' => $account->id, + ]); + $newNote->save(); + + if($request->ajax()){ + return $newNote; + } + return redirect()->back(); + } + + /** + * Removes a note from the user + * @param Request $request [description] + * @param Account $account [description] + * @param Note $note [description] + * @return [type] [description] + */ + public function removeNote(Request $request, Account $account, Note $note) + { + if($note->account_id == $account->id) { + $note->delete(); + } + + if($request->ajax()) { + return true; + } + return redirect()->back(); + } + + /** + * Banns the user for a given reason and for a given timeframe + * The timeframe can be indefinite by marking the permanent flat to true + * + * @param Request $request [description] + * @param Account $account [description] + */ + public function addBan(Request $request, Account $account) + { + if(!$request->ajax()) { + return; + } + $validated = $request->validate( + [ + 'permanent' => 'required|boolean', + 'till' => 'required_without:permanent|nullable|date', + 'reason' => 'required|string', + 'homepage' => 'required|boolean', + 'forum' => 'required|boolean', + 'teamspeak' => 'required|boolean', + ] + ); + + $newBan = new Ban([ + 'reason' => $validated['reason'], + 'author_id' => $this->account->id, + 'account_id' => $account->id, + 'homepage' => $validated['homepage'], + 'forum' => $validated['forum'], + 'teamspeak' => $validated['teamspeak'], + ]); + + if($validated['permanent']) { + // We need to create a permanent ban here + $newBan->permanent = true; + $newBan->banned_till = null; + } else { + // we have an end date given + $newBan->permanent = false; + $newBan->banned_till = Carbon::createFromFormat('Y-m-d', $validated['till'], 'UTC'); + $newBan->banned_till->setTime(23,59,59); + } + + $newBan->save(); + + // Update the forum + try{ + if ($account->is_currently_forum_banned) { + XenBridge::banForumAccount($account); + } + } catch (Throwable $th) { + //throw $th; + } + + + // Let the TS know that user may be banned + TeamSpeakWebquery::checkAccount($account); + + return $newBan; + + } + + /** + * Removes a ban from the user + * @param Request $request [description] + * @param Account $account [description] + * @param Ban $ban [description] + * @return [type] [description] + */ + public function removeBan(Request $request, Account $account, Ban $ban) + { + if($ban->account_id == $account->id) { + $ban->delete(); + } + if($request->ajax()) + { + return true; + } + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Administration/Membership/GroupController.php b/app/Http/Controllers/Administration/Membership/GroupController.php new file mode 100644 index 0000000..1b82f21 --- /dev/null +++ b/app/Http/Controllers/Administration/Membership/GroupController.php @@ -0,0 +1,157 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.membership')) + abort(403); + + return $next($request); + }); + } + + /** + * Get a list of all groups + * @param Request $request [description] + * @return [type] [description] + */ + public function index(Request $request) + { + if($request->ajax()) { + return Group::all(); + } + } + + /** + * Get details of a single group + * @param Request $request [description] + * @param Group $group [description] + * @return [type] [description] + */ + public function show(Request $request, Group $group) + { + if($request->ajax()) { + $group->loadMissing('users', 'permissions', 'forumgroups'); + return $group; + } + } + + /** + * Adds a permission to a group + * @param Request $request [description] + * @param Group $group [description] + */ + public function addPermission(Request $request, Group $group) + { + if($request->ajax()) { + $group->assignPermissions($request->permission); + return $this->returnUpdatedGroup($group); + } + } + + /** + * Removes a permission from a group + * @param Request $request [description] + * @param Group $group [description] + * @return [type] [description] + */ + public function removePermission(Request $request, Group $group) + { + if($request->ajax()) { + $group->revokePermissions($request->permission); + return $this->returnUpdatedGroup($group); + } + } + + /** + * Add a user to the given group + * @param Request $request [description] + * @param Group $group [description] + */ + public function addAccount(Request $request, Group $group) + { + if($request->ajax()) { + $account = \App\Models\Membership\Account::find($request->cid); + if($account != null) { + $group->assignUser($account); + } + return $this->returnUpdatedGroup($group); + } + } + + /** + * Removes a user from the given group + * @param Request $request [description] + * @param Group $group [description] + * @return [type] [description] + */ + public function removeAccount(Request $request, Group $group) + { + if($request->ajax()) { + $group->removeUser($request->cid); + return $this->returnUpdatedGroup($group); + } + } + + /** + * Assign a given forum group to this group + * @param Request $request [description] + * @param Group $group [description] + * @param ForumGroup $fg [description] + * @return [type] [description] + */ + public function assignForumGroup(Request $request, Group $group) + { + if($request->ajax()) { + $fg = ForumGroup::find($request->fg); + if($fg != null) + $group->forumgroups()->attach($fg); + return $this->returnUpdatedGroup($group); + } + } + + /** + * Detach the given forumgroup from this group + * @param Request $request [description] + * @param Group $group [description] + * @return [type] [description] + */ + public function unassignForumGroup(Request $request, Group $group) + { + if($request->ajax()) { + $fg = ForumGroup::find($request->fg); + if($fg != null) + $group->forumgroups()->detach($fg); + return $this->returnUpdatedGroup($group); + } + } + + /** + * Internal function to update a given group + * and return it with updated data + * + * @param Group $group [description] + * @return [type] [description] + */ + protected function returnUpdatedGroup(Group $group) + { + $group->save(); + $group->refresh(); + $group->loadMissing('users', 'permissions', 'forumgroups'); + return $group; + } + +} diff --git a/app/Http/Controllers/Administration/Membership/PermissionController.php b/app/Http/Controllers/Administration/Membership/PermissionController.php new file mode 100644 index 0000000..3fcc90c --- /dev/null +++ b/app/Http/Controllers/Administration/Membership/PermissionController.php @@ -0,0 +1,32 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.membership')) + abort(403); + + return $next($request); + }); + } + + public function index(Request $request) + { + if($request->ajax()) { + return Permission::orderBy('slug', 'ASC')->get(); + } + } + +} diff --git a/app/Http/Controllers/Administration/Navigation/AerodromeController.php b/app/Http/Controllers/Administration/Navigation/AerodromeController.php new file mode 100644 index 0000000..b4c46f3 --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/AerodromeController.php @@ -0,0 +1,476 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasAnyPermission('administration.navigation', 'administration.navigation.rg')) + abort(403); + + return $next($request); + }); + } + + /** + * Get aerodrome details based upon account permissions + * + * @param Request $request [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function show(Request $request, $icao) + { + if($request->ajax()) { + + // 1. Grab the aerodrome + $aerodrome = $this->_getAerodromeByIcao($icao); + // 2. Get all assigned regionalgroups + $regionalgroups = $aerodrome->regionalgroups; + // 3. If no regionalgroup is assigned... return airfield if one of the permissions is set + if(count($regionalgroups) == 0 && ($this->account->hasPermission('administration.navigation') || $this->account->hasPermission('administration.navigation.rg'))) { + return $aerodrome; + } + // 4. Regardless of regionalgroup assignment. + // vACC NAV always has the permissions to modify + if($this->account->hasPermission('administration.navigation')) + { + return $aerodrome; + } + + // Check if aerodrome is allowed to be modified by this account + // as it is assigned to at least one regionalgroup + if ($this->account->hasPermission('administration.navigation.rg')) { + foreach ($regionalgroups as $rg) { + if($this->account->isNavigatorOfRegionalgroup($rg)) { + return $aerodrome; + } + } + } + abort(403); + } + } + + /** + * Creates an aerodrome + * @param Request $request [description] + * @return [type] [description] + */ + public function createAerodrome(Request $request) + { + if($request->ajax()) { + $validated = $request->validate([ + 'newAerodrome.name' => 'required|string', + 'newAerodrome.icao' => 'required|string|size:4', + 'newAerodrome.iata' => 'string|size:3', + 'newAerodrome.description' => 'string|nullable', + 'newAerodrome.country' => 'required|string|size:2', + 'newAerodrome.state' => 'required|string', + 'newAerodrome.city' => 'required|string', + ]); + + // Set all general information even regionalgroup navigators can edit + $aerodrome = new Aerodrome(); + $aerodrome->name = $validated['newAerodrome']['name']; + $aerodrome->icao = $validated['newAerodrome']['icao']; + $aerodrome->iata = $validated['newAerodrome']['iata'] != null ? $validated['newAerodrome']['iata'] : ''; + $aerodrome->description = $validated['newAerodrome']['description'] != null ? $validated['newAerodrome']['description'] : ''; + $aerodrome->country = $validated['newAerodrome']['country']; + $aerodrome->state = $validated['newAerodrome']['state']; + $aerodrome->city = $validated['newAerodrome']['city']; + + return $this->_reloadAerodrome($aerodrome); + } + } + + /** + * Updates an aerodrome based upon the permissions a user has + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + * @return [type] [description] + */ + public function updateGeneral(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + $validated = $request->validate([ + 'aerodrome.id' => 'required|exists:navigation_aerodromes,id', + 'aerodrome.name' => 'required|string', + 'aerodrome.icao' => 'required|string|size:4', + 'aerodrome.iata' => 'string|size:3', + 'aerodrome.description' => 'string|nullable', + 'aerodrome.latitude' => 'required', + 'aerodrome.longitude' => 'required', + 'aerodrome.elevation' => 'required', + 'aerodrome.regionalgroups' => 'nullable', + 'aerodrome.major' => 'required|boolean', + 'aerodrome.military' => 'required|boolean', + 'aerodrome.civilian' => 'required|boolean', + 'aerodrome.active' => 'required|boolean', + 'aerodrome.country' => 'required|string|size:2', + 'aerodrome.state' => 'required|string', + 'aerodrome.city' => 'required|string', + 'aerodrome.wiki_link' => 'string|nullable', + ]); + + // Set all general information even regionalgroup navigators can edit + + $aerodrome->name = $validated['aerodrome']['name']; + $aerodrome->icao = $validated['aerodrome']['icao']; + $aerodrome->iata = $validated['aerodrome']['iata'] != null ? $validated['aerodrome']['iata'] : ''; + $aerodrome->description = $validated['aerodrome']['description'] != null ? $validated['aerodrome']['description'] : ''; + $aerodrome->latitude = floatval($validated['aerodrome']['latitude']); + $aerodrome->longitude = floatval($validated['aerodrome']['longitude']); + $aerodrome->elevation = intval($validated['aerodrome']['elevation']); + $aerodrome->major = $validated['aerodrome']['major']; + $aerodrome->military = $validated['aerodrome']['military']; + $aerodrome->civilian = $validated['aerodrome']['civilian']; + $aerodrome->active = $validated['aerodrome']['active']; + $aerodrome->country = $validated['aerodrome']['country']; + $aerodrome->state = $validated['aerodrome']['state']; + $aerodrome->city = $validated['aerodrome']['city']; + $aerodrome->wiki_link = $validated['aerodrome']['wiki_link'] != null ? $validated['aerodrome']['wiki_link'] : ''; + + // A normal regionalgroup navigator is not allowed to assign aerodromes to regionalgroups + if($this->account->hasPermission('administration.navigation')) + { + $aerodrome->regionalgroups()->detach(); + // If the account does not belong to the vACC Germany Navigation Team + // the regionalgroup assignment must not be made. + if($validated['aerodrome']['regionalgroups'] != null) { + foreach ($validated['aerodrome']['regionalgroups'] as $rg) { + $aerodrome->regionalgroups()->attach($rg['id']); + } + } + } + + return $this->_reloadAerodrome($aerodrome); + } + } + + /** + * Get a list of available regionalgroups + * + * @param Request $request [description] + * @return [type] [description] + */ + public function getAvailableRegionalgroups(Request $request) + { + if($request->ajax()) { + if($this->account->hasPermission('administration.navigation')) { + return \App\Models\Regionalgroups\Regionalgroup::orderBy('name', 'ASC')->get(); + } else { + return []; + } + } + abort(403); + } + + /** + * Updates the order of assigned stations + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + * @return [type] [description] + */ + public function updateStationOrder(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + $stations = $request->stations; + foreach ($stations as $s) { + $station = Station::find($s['id']); + $aerodrome->stations()->updateExistingPivot($station, ['order' => $s['pivot']['order']], false); + } + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + /** + * Adds a station to the aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + */ + public function addStation(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + $validated = $request->validate([ + 'station' => 'required|exists:navigation_stations,id' + ]); + + $station = Station::find($validated['station']); + if($station->id === $validated['station']) { + // Get the amount of attached stations to set the order + $stationsCount = $aerodrome->stations->count(); + $aerodrome->stations()->attach($station, ['order' => $stationsCount + 1]); + + return $this->_reloadAerodrome($aerodrome); + } + } + abort(403); + } + + /** + * Removes a station from the aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + * @param Station $station [description] + * @return [type] [description] + */ + public function removeStation(Request $request, Aerodrome $aerodrome, Station $station) + { + if($request->ajax()) { + $aerodrome->stations()->detach($station); + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + /** + * Adds a chart to the aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + */ + public function addChart(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'chart' => 'required|exists:navigation_charts,id' + ]); + + $chart = Chart::find($validated['chart']); + + $aerodrome->charts()->attach($chart); + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + /** + * Removes a chart from the aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + * @param Chart $chart [description] + * @return [type] [description] + */ + public function removeChart(Request $request, Aerodrome $aerodrome, Chart $chart) + { + if($request->ajax()) { + + $aerodrome->charts()->detach($chart); + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + /** + * Associate a given navaid with this aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + */ + public function addNavaid(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + $validated = $request->validate([ + 'navaid' => 'required|exists:navigation_navaids,id', + ]); + + $navaid = Navaid::find($validated['navaid']); + + $aerodrome->navaids()->attach($navaid); + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + /** + * Detach a given navaid from this aerodrome + * + * @param Request $request [description] + * @param Aerodrome $aerodrome [description] + * @param Navaid $navaid [description] + * @return [type] [description] + */ + public function removeNavaid(Request $request, Aerodrome $aerodrome, Navaid $navaid) + { + if($request->ajax()) { + $aerodrome->navaids()->detach($navaid); + + return $this->_reloadAerodrome($aerodrome); + } + abort(403); + } + + public function addRunway(Request $request, Aerodrome $aerodrome) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'newRunway.ident' => 'required|string|max:3', + 'newRunway.heading' => 'required', + 'newRunway.length' => 'required', + 'newRunway.width' => 'required', + 'newRunway.surface_type' => 'required|in:1,2,3,4,5,6', + 'newRunway.opposite_id' => 'nullable|exists:navigation_runways,id' + ]); + + $newRunway = new Runway; + $newRunway->aerodrome_id = $aerodrome->id; + $newRunway->ident = $validated['newRunway']['ident']; + $newRunway->heading = $validated['newRunway']['heading']; + $newRunway->length = $validated['newRunway']['length']; + $newRunway->width = $validated['newRunway']['width']; + $newRunway->surface_type = $validated['newRunway']['surface_type']; + $newRunway->opposite_id = $validated['newRunway']['opposite_id'] ?? null; + + $newRunway->save(); + + return $this->_reloadAerodrome($aerodrome); + } + } + + public function editRunway(Request $request, Aerodrome $aerodrome, Runway $runway) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'editRunway.ident' => 'required|string|max:3', + 'editRunway.heading' => 'required', + 'editRunway.length' => 'required', + 'editRunway.width' => 'required', + 'editRunway.surface_type' => 'required|in:1,2,3,4,5,6', + 'editRunway.opposite_id' => 'nullable|exists:navigation_runways,id' + ]); + + $runway->ident = $validated['editRunway']['ident']; + $runway->heading = $validated['editRunway']['heading']; + $runway->length = $validated['editRunway']['length']; + $runway->width = $validated['editRunway']['width']; + $runway->surface_type = $validated['editRunway']['surface_type']; + $runway->opposite_id = $validated['editRunway']['opposite_id'] ?? null; + + $runway->save(); + + return $this->_reloadAerodrome($aerodrome); + } + } + + public function removeRunway(Request $request, Aerodrome $aerodrome, Runway $runway) + { + if($request->ajax()) { + $runway->delete(); + + return $this->_reloadAerodrome($aerodrome); + } + } + + /** + * Download the current stand definition file of an aerodrome. + * + * @param Request $request [description] + * @param \App\Models\Navigation\Aerodrome $aerodrome [description] + * + * @return [type] [description] + */ + public function getStands(Request $request, Aerodrome $aerodrome) + { + $standFile = 'navigation/stands/'.strtolower($aerodrome->icao).'.csv'; + if (File::exists(storage_path().'/app/'.$standFile)) { + return Storage::download($standFile); + } else { + return Storage::download('navigation/stands/new.csv'); + } + } + + /** + * Update the stand definition file of an aerodrome. + * + * @param Request $request [description] + * @param \App\Models\Navigation\Aerodrome $aerodrome [description] + * + * @return [type] [description] + */ + public function updateStands(Request $request, Aerodrome $aerodrome) + { + $validated = $request->validate( + [ + 'newStandFile' => 'required|file', + ] + ); + + $newStandFile = $request->file('newStandFile'); + + $result = Storage::putFileAs( + 'navigation/stands/', + $newStandFile, + strtolower($aerodrome->icao).'.csv' + ); + + return $this->_reloadAerodrome($aerodrome); + } + + + /** + * Get an aerodrome by it's identifing icao code + * This also loads contry and regionalgroup details + * + * @param [type] $icao [description] + * @return [type] [description] + */ + private function _getAerodromeByIcao($icao) + { + // Load needed stuff here + $aerodrome = Aerodrome::where('icao', $icao)->firstOrFail(); + $aerodrome->loadMissing('countryDetail', 'regionalgroups', 'stations', 'charts', 'navaids', 'runways.opposite'); + // Return the completely loaded model + return $aerodrome; + + } + + /** + * Save and reload the aerodrome + * Then return the refreshed model + * + * @param Aerodrome $aerodrome [description] + * @return [type] [description] + */ + private function _reloadAerodrome(Aerodrome $aerodrome) + { + $aerodrome->save(); + $aerodrome->refresh(); + + $aerodrome->loadMissing('countryDetail', 'regionalgroups', 'stations', 'charts', 'navaids', 'runways.opposite'); + + $this->account->notify(new AerodromeUpdatedNotification($aerodrome)); + + return $aerodrome; + } + +} diff --git a/app/Http/Controllers/Administration/Navigation/ChartController.php b/app/Http/Controllers/Administration/Navigation/ChartController.php new file mode 100644 index 0000000..9cd3ce0 --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/ChartController.php @@ -0,0 +1,119 @@ +middleware(function($request, $next) { + + if(!$this->account->hasAnyPermission('administration.navigation', 'administration.navigation.rg')) + abort(403); + + return $next($request); + }); + } + + public function index(Request $request) + { + if($request->ajax()) { + return Chart::orderBy('name', 'ASC')->get(); + } + abort(403); + } + + public function create(Request $request) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'chart.name' => 'required|string', + 'chart.href' => 'required|string', + 'chart.published' => 'nullable|boolean', + 'chart.public_available' => 'nullable|boolean', + 'chart.airac' => 'required|numeric', + 'chart.type' => 'required|in:aoi,afc,agc,apc,sid,star,iac', + 'chart.fri' => 'required|in:ifr,vfr' + ]); + + $chart = new Chart(); + $chart->name = $validated['chart']['name']; + $chart->href = $validated['chart']['href']; + $chart->published = boolval($validated['chart']['published']); + $chart->public_available = boolval($validated['chart']['public_available']); + $chart->airac = intval($validated['chart']['airac']); + $chart->type = $validated['chart']['type']; + $chart->fri = $validated['chart']['fri']; + + $chart->save(); + + $this->account->notify(new ChartCreatedNotification($chart)); + + return $chart; + + } + abort(403); + } + + public function update(Request $request, Chart $chart) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'chart.id' => 'required|exists:navigation_charts,id', + 'chart.name' => 'required|string', + 'chart.href' => 'required|string', + 'chart.published' => 'nullable|boolean', + 'chart.public_available' => 'nullable|boolean', + 'chart.airac' => 'required|numeric', + 'chart.type' => 'required|in:aoi,afc,agc,apc,sid,star,iac', + 'chart.fri' => 'required|in:ifr,vfr' + ]); + + $chart->name = $validated['chart']['name']; + $chart->href = $validated['chart']['href']; + $chart->published = boolval($validated['chart']['published']); + $chart->public_available = boolval($validated['chart']['public_available']); + $chart->airac = intval($validated['chart']['airac']); + $chart->type = $validated['chart']['type']; + $chart->fri = $validated['chart']['fri']; + + $chart->save(); + + $this->account->notify(new ChartUpdatedNotification($chart)); + + return $chart; + + } + abort(403); + } + + public function remove(Request $request, Chart $chart) + { + if($request->ajax()) { + + $chartName = $chart->name; + + $chart->aerodromes()->detach(); + + $chart->delete(); + + $this->account->notify(new ChartRemovedNotification($chartName)); + + return $chartName; + } + abort(403); + } + +} diff --git a/app/Http/Controllers/Administration/Navigation/GroundlayoutController.php b/app/Http/Controllers/Administration/Navigation/GroundlayoutController.php new file mode 100644 index 0000000..981a46d --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/GroundlayoutController.php @@ -0,0 +1,99 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.navigation')) + abort(403); + + return $next($request); + }); + } + + public function render(Request $request) + { + if ($request->hasFile('gekml')) { + if (Str::endsWith($request->file('gekml')->getClientOriginalName(), '.kmz')) { + $zip = new ZipArchive(); + $res = $zip->open($request->file('gekml')); + if (true === $res) { + $zip->extractTo(storage_path('app').'/temp/'.$this->account->id.'/'); + $zip->close(); + } else { + return redirect()->back()->with('notification', ['type' => 'danger', 'title' => 'SCT Generator', 'message' => 'Failed to load Resource from Archive!']); + } + $xmlFileRead = file_get_contents(storage_path('app').'/temp/'.$this->account->id.'/doc.kml'); + // Storage::deleteDirectory('temp/'.$this->account->id); + } elseif (Str::endsWith($request->file('gekml')->getClientOriginalName(), '.kml')) { + $xmlFileRead = file_get_contents($request->file('gekml')); + } else { + return response()->json( + [ + 'output' => 'File type not supported! ('.$request->input('gekml').')', + ], + 200 + ); + } + + \App\Jobs\Navigation\ParseGoogleEarthKMZJob::dispatch($this->account, $xmlFileRead); + + return response()->json([ + 'output' => $xmlFileRead, + 'status' => 'Parsing started.' + ]); + } else { + return response()->json( + [ + 'output' => 'No file to handle! ('.$request->input('gekml').')', + ], + 200 + ); + } + } + + /** + * Function to handle file download of a given sct file content. + * + * @param Request $request [description] + * + * @return [type] [description] + */ + public function download(Request $request) + { + return response()->download(storage_path('app').'/temp/'.$this->account->id.'/sector.sct')->deleteFileAfterSend(true); + } + + /** + * Get the sample file. + * + * @param Request $request [description] + * + * @return [type] [description] + */ + public function downloadSample(Request $request) + { + return response()->download( + storage_path('app').'/navigation/glg/Groundlayouts.kmz', + 'Groundlayouts.kmz', + [ + 'Content-Type' => 'application/vnd.google-earth.kmz .kmz', + ] + ); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Administration/Navigation/NavaidController.php b/app/Http/Controllers/Administration/Navigation/NavaidController.php new file mode 100644 index 0000000..6b43113 --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/NavaidController.php @@ -0,0 +1,144 @@ +middleware(function($request, $next) { + + if(!$this->account->hasAnyPermission('administration.navigation', 'administration.navigation.rg')) + abort(403); + + return $next($request); + }); + } + + public function index(Request $request) + { + if($request->ajax()) { + return Navaid::all(); + } + } + + public function create(Request $request) + { + if($request->ajax()) { + // Create a new navaid + $validated = $request->validate( + [ + 'newNavaid.name' => 'required|string', + 'newNavaid.type' => 'required|in:1,2,3,4,5,6', + 'newNavaid.ident' => 'required|string|max:5', + 'newNavaid.heading' => ['nullable', 'required_if:editNavaid.type,'.Navaid::TYPE_ILS, new \App\Rules\Navigation\HeadingRule], + 'newNavaid.remarks' => 'nullable|string', + 'newNavaid.frequency' => ['required', new \App\Rules\Navigation\FrequencyRule], + 'newNavaid.frequency_band' => 'required|in:1,2', + ] + ); + + $navaid = new Navaid; + $navaid->name = $validated['newNavaid']['name']; + $navaid->type = $validated['newNavaid']['type']; + $navaid->ident = $validated['newNavaid']['ident']; + $navaid->heading = $validated['newNavaid']['heading'] ?? ''; + $navaid->remarks = $validated['newNavaid']['remarks'] ?? ''; + $navaid->frequency = $this->_modifyFrequency($validated['newNavaid']['frequency']); + $navaid->frequency_band = $validated['newNavaid']['frequency_band']; + + $navaid->save(); + + $this->account->notify(new \App\Notifications\Administration\Navigation\NavaidCreatedNotification($navaid)); + + return $navaid; + } + } + + public function update(Request $request, Navaid $navaid) + { + if($request->ajax()) { + // Update navaid data + $validated = $request->validate( + [ + 'editNavaid.name' => 'required|string', + 'editNavaid.type' => 'required|in:1,2,3,4,5,6', + 'editNavaid.ident' => 'required|string|max:5', + 'editNavaid.heading' => ['nullable', 'required_if:editNavaid.type,'.Navaid::TYPE_ILS, new \App\Rules\Navigation\HeadingRule], + 'editNavaid.remarks' => 'nullable|string', + 'editNavaid.frequency' => ['required', new \App\Rules\Navigation\FrequencyRule], + 'editNavaid.frequency_band' => 'required|in:1,2', + ] + ); + + $navaid->name = $validated['editNavaid']['name']; + $navaid->type = $validated['editNavaid']['type']; + $navaid->ident = $validated['editNavaid']['ident']; + $navaid->heading = $validated['editNavaid']['heading'] ?? ''; + $navaid->remarks = $validated['editNavaid']['remarks'] ?? ''; + $navaid->frequency = $this->_modifyFrequency($validated['editNavaid']['frequency']); + $navaid->frequency_band = $validated['editNavaid']['frequency_band']; + + $navaid->save(); + + $this->account->notify(new \App\Notifications\Administration\Navigation\NavaidUpdatedNotification($navaid)); + + return $navaid; + } + } + + public function delete(Request $request, Navaid $navaid) + { + if($request->ajax()) { + $navaid->aerodromes()->detach(); + + $nid = $navaid->id; + $nString = $navaid->name . ' | ' . $navaid->ident; + + $navaid->delete(); + + $this->account->notify(new \App\Notifications\Administration\Navigation\NavaidDeletedNotification($nString)); + + return $nid; + } + } + + private function _modifyFrequency($fs) + { + // Modify the frequency to be perfect + $frequencyString = $fs; + $frequency = 000.000; + if (7 == strlen($frequencyString)) { + if (!strpos($frequencyString, '.')) { + $frequency = floatval(substr($frequencyString, 0, 3).'.'.substr($frequencyString, 3, 3)); + } else { + $frequency = floatval($frequencyString); + } + } elseif (6 == strlen($frequencyString)) { + if (!strpos($frequencyString, '.')) { + $frequency = floatval(substr($frequencyString, 0, 3).'.'.substr($frequencyString, 3)); + } else { + $frequency = floatval($frequencyString); + } + } elseif (strlen($frequencyString) < 6 && strlen($frequencyString) >= 3) { + if (strlen($frequencyString) > 3) { + if (!strpos($frequencyString, '.')) { + $frequency = floatval(substr($frequencyString, 0, 3).'.'.substr($frequencyString, 3)); + } else { + $frequency = floatval($frequencyString); + } + } else { + $frequency = floatval($frequencyString); + } + } + return $frequency; + } + +} diff --git a/app/Http/Controllers/Administration/Navigation/SectorfileController.php b/app/Http/Controllers/Administration/Navigation/SectorfileController.php new file mode 100644 index 0000000..8a77680 --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/SectorfileController.php @@ -0,0 +1,758 @@ +account->id."/"; // For production + private $_localPath = "temp/testing/"; // For local testing ONLY + + private $_airacFile = "airac.zip"; + private $_customFile = "custom.zip"; + private $_customFile2 = "custom2.zip"; + + /** + * Download a given sectorfile url + * + * @param [type] $sectorfileUrl [description] + * @return [type] [description] + */ + function downloadSectorFile($sectorfileUrl) { + $curlHandler = curl_init(); + curl_setopt($curlHandler, CURLOPT_URL, $sectorfileUrl); + curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandler, CURLOPT_HTTPHEADER, array( + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", + "Referer: http://files.aero-nav.com/EDXX", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Encoding: gzip, deflate" + )); + + $downloadedData = curl_exec($curlHandler); + + curl_close($curlHandler); + + Storage::put($this->_localPath.$this->_airacFile, $downloadedData); + } + + /** + * Unzip the downloaded airac and uploaded custom + * sectorfiles + * + * @return [type] [description] + */ + private function unzipFiles() { + $unzipPathAirac = $this->_localPath.'airac'; + $unzipPathCustom = $this->_localPath.'custom'; + $unzipPathCustom2 = $this->_localPath.'custom2'; + + if(File::exists(storage_path('app').'/'.$this->_localPath.$this->_airacFile)) { + $zip = new \ZipArchive; + $res = $zip->open(storage_path('app').'/'.$this->_localPath.$this->_airacFile); + if($res === TRUE) { + $zip->extractTo(storage_path('app').'/'.$unzipPathAirac); + $zip->close(); + } + } + + if(File::exists(storage_path('app').'/'.$this->_localPath.$this->_customFile)) { + $zip = new \ZipArchive; + $res = $zip->open(storage_path('app').'/'.$this->_localPath.$this->_customFile); + if($res === TRUE) { + $zip->extractTo(storage_path('app').'/'.$unzipPathCustom); + $zip->close(); + } + } + + if(File::exists(storage_path('app').'/'.$this->_localPath.$this->_customFile2)) { + $zip = new \ZipArchive; + $res = $zip->open(storage_path('app').'/'.$this->_localPath.$this->_customFile2); + if($res === TRUE) { + $zip->extractTo(storage_path('app').'/'.$unzipPathCustom2); + $zip->close(); + } + } + } + + /** + * Parse an sct file to a data holding array + * + * @param [type] $filePath [description] + * @param boolean $isCustom [description] + * @return [type] [description] + */ + private function parseSectorFile($filePath, $isCustom = false) { + $result = array( + 'info' => [], + 'colors' => [], + 'vor' => [], + 'ndb' => [], + 'fixes' => [], + 'airport' => [], + 'runway' => [], + 'sid' => [], + 'star' => [], + 'artcc' => [], + 'artcc low' => [], + 'artcc high' => [], + 'geo' => [], + 'regions' => [], + 'airway high' => [], + 'airway low' => [], + ); + + $activeSection = null; + $lastLine = null; + $currentSID = []; + $currentSTAR = []; + $currentARTCC = []; + $currentGEO = []; + $currentRegion = null; + $currentRegionIterator = 0; + + foreach (file($filePath) as $line) { + $line = trim($line); + if(strlen($line) == 0) continue; + + switch ($line) { + case '[INFO]': + $activeSection = 'info'; + break; + case '[VOR]': + $activeSection = 'vor'; + break; + case '[NDB]': + $activeSection = 'ndb'; + break; + case '[FIXES]': + $activeSection = 'fixes'; + break; + case '[AIRPORT]': + $activeSection = 'airport'; + break; + case '[RUNWAY]': + $activeSection = 'runway'; + break; + case '[SID]': + $activeSection = 'sid'; + break; + case '[STAR]': + $activeSection = 'star'; + break; + case '[ARTCC HIGH]': + // $activeSection = 'artcc high'; + $activeSection = 'artcc'; + break; + case '[ARTCC]': + $activeSection = 'artcc'; + break; + case '[ARTCC LOW]': + // $activeSection = 'artcc low'; + $activeSection = 'artcc'; + break; + case '[GEO]': + $activeSection = 'geo'; + break; + case '[REGIONS]': + $activeSection = 'regions'; + break; + case '[HIGH AIRWAY]': + $activeSection = 'airway high'; + break; + case '[LOW AIRWAY]': + $activeSection = 'airway low'; + break; + default: + # code... + break; + } + + // Work with the line + if(Str::startsWith($line, '[')) continue; // Skip Section Markers for further parsing + + if(Str::startsWith($line, '#define')) { + $activeSection = null; // Colors have a own section in our data array + // This line defines a color + $ce = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + $result['colors'][$ce[1]] = $ce[2]; + } + + $ls = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + + switch ($activeSection) { + case 'info': + if(!$isCustom) + $result['info'][] = $line; + break; + case 'vor': + $result['vor'][$ls[0]] = ['freq' => $ls[1], 'lat' => $ls[2], 'lon' => $ls[3]]; + break; + case 'ndb': + $result['ndb'][$ls[0]] = ['freq' => $ls[1], 'lat' => $ls[2], 'lon' => $ls[3]]; + break; + case 'fixes': + $result['fixes'][$ls[0]] = ['lat' => $ls[1], 'lon' => $ls[2]]; + break; + case 'airport': + $result['airport'][$ls[0]] = ['twrfreq' => $ls[1],'lat' => $ls[2], 'lon' => $ls[3], 'as' => $ls[4]]; + break; + case 'runway': + $result['runway'][$ls[8]][] = $line; + break; + case 'sid': + if(sizeof($ls) == 8) { + $currentSID = ['icao' => $ls[0], 'rwy' => $ls[2], 'id' => $ls[3]]; + $result['sid'][$ls[0]][$ls[2]][$ls[3]][] = ['lat_from' => $ls[4], 'lon_from' => $ls[5], 'lat_to' => $ls[6], 'lon_to' => $ls[7]]; + } else { + $result['sid'][$currentSID['icao']][$currentSID['rwy']][$currentSID['id']][] = ['lat_from' => $ls[0], 'lon_from' => $ls[1], 'lat_to' => $ls[2], 'lon_to' => $ls[3]]; + } + break; + case 'star': + // Filter out holdings for now + if($ls[sizeof($ls) - 1] == 'COLOR_Holding') break; + // Parse actual stars + if(sizeof($ls) == 4) { + // Append coordinates to current active STAR + $result['star'][$currentSTAR['icao']][$currentSTAR['id']][] = [ + 'lat_from' => $ls[0], + 'lon_from' => $ls[1], + 'lat_to' => $ls[2], + 'lon_to' => $ls[3], + ]; + } else { + // New STAR found + $starIdentIndices = sizeof($ls) - 4; // Last for line split items are coordinates + $starId = ''; + for($i = 1; $i < $starIdentIndices; $i++) + $starId.= ' '.$ls[$i]; + $currentSTAR = ['icao' => $ls[0], 'id' => trim($starId)]; + $result['star'][$currentSTAR['icao']][$currentSTAR['id']][] = [ + 'lat_from' => $ls[sizeof($ls) - 4], + 'lon_from' => $ls[sizeof($ls) - 3], + 'lat_to' => $ls[sizeof($ls) - 2], + 'lon_to' => $ls[sizeof($ls) - 1], + ]; + } + break; + case 'artcc': + // If a color is defined for the airspace we need to extract that + if(sizeof($ls) > 4) { + // Maybe color definition at the end or artcc name at the beginning + if(preg_match('/^[WE]{1}[0-9]{3}\.[0-9]{2}\.[0-9]{2}\.[0-9]{3}/', $ls[sizeof($ls) - 1])) { + // Last index of the line split is a coordinate + $artccIdentIndices = sizeof($ls) - 4; + $artccIdent = ''; + for($i = 0; $i < $artccIdentIndices; $i++) + $artccIdent.= ' '.$ls[$i]; + $currentARTCC = ['id' => trim($artccIdent)]; + $result['artcc'][$currentARTCC['id']][] = [ + 'lat_from' => $ls[sizeof($ls) - 4], + 'lon_from' => $ls[sizeof($ls) - 3], + 'lat_to' => $ls[sizeof($ls) - 2], + 'lon_to' => $ls[sizeof($ls) - 1], + ]; + } else { + // Last index is a color definition + $artccIdentIndices = sizeof($ls) - 5; + if($artccIdentIndices >= 1) { + $artccIdent = ''; + for($i = 0; $i < $artccIdentIndices; $i++) + $artccIdent.= ' '.$ls[$i]; + $currentARTCC = ['id' => trim($artccIdent), 'color' => $ls[sizeof($ls) - 1]]; + } else { + $currentARTCC['color'] = $ls[sizeof($ls) - 1]; + } + $result['artcc'][$currentARTCC['id']][] = [ + 'lat_from' => $ls[sizeof($ls) - 5], + 'lon_from' => $ls[sizeof($ls) - 4], + 'lat_to' => $ls[sizeof($ls) - 3], + 'lon_to' => $ls[sizeof($ls) - 2], + ]; + } + } else { + $result['artcc'][$currentARTCC['id']][] = [ + 'lat_from' => $ls[0], + 'lon_from' => $ls[1], + 'lat_to' => $ls[2], + 'lon_to' => $ls[3], + ]; + } + break; + case 'geo': + if(!Str::startsWith($line, ';')) { + if(sizeof($ls) > 5) { + // New section + $geoIndices = sizeof($ls) - 5; + $geoIdent = ''; + for($i = 0; $i < $geoIndices; $i++) { + $geoIdent.= ' '.$ls[$i]; + } + $currentGEO['id'] = trim($geoIdent); + } + $result['geo'][$currentGEO['id']]['coords'][] = [ + 'lat_from' => $ls[sizeof($ls) - 5], + 'lon_from' => $ls[sizeof($ls) - 4], + 'lat_to' => $ls[sizeof($ls) - 3], + 'lon_to' => $ls[sizeof($ls) - 2], + 'color' => $ls[sizeof($ls) - 1], + ]; + } + break; + case 'regions': + if(!Str::startsWith($line, ';')) { + if(Str::startsWith($line, 'REGIONNAME')) { + $rn = ''; + for($i = 1; $i < sizeof($ls); $i++) { + $rn.= ' '.$ls[$i]; + } + $rn = trim($rn); + if($currentRegion === null) { + // New to this section + $currentRegionIterator = 0; + $currentRegion['name'] = $rn; + $currentRegion['regions'] = array(); + } else { + if($currentRegion['name'] != $rn) { + $result['regions'][$currentRegion['name']] = $currentRegion['regions']; + $currentRegion['name'] = $rn; + $currentRegion['regions'] = array(); + $currentRegionIterator = 0; + } + } + } else { + if(sizeof($ls) == 3) { + $currentRegionIterator++; + $currentRegion['regions'][$currentRegionIterator]['color'] = $ls[0]; + $currentRegion['regions'][$currentRegionIterator]['coords'][] = ['lat' => $ls[1], 'lon' => $ls[2]]; + } + if(sizeof($ls) == 2) { + $currentRegion['regions'][$currentRegionIterator]['coords'][] = ['lat' => $ls[0], 'lon' => $ls[1]]; + } + } + } + break; + case 'airway high': + $result['airway high'][$ls[0]][] = $ls[1].' '.$ls[2].' '.$ls[3].' '.$ls[4]; + break; + case 'airway low': + $result['airway low'][$ls[0]][] = $ls[1].' '.$ls[2].' '.$ls[3].' '.$ls[4]; + break; + default: + break; + } + + + $lastLine = $line; + } + + if(!array_key_exists($currentRegion['name'], $result['regions'])) { + $result['regions'][$currentRegion['name']] = $currentRegion['regions']; + } + + // Clear buffers + unset($currentSID); + unset($currentSTAR); + unset($currentARTCC); + unset($currentGEO); + unset($currentRegion); + + return $result; + } + + private function buildSectorfile($airac, $custom) { + // Cycle through the sections in the airac array + // and combine it with the custom array if things are altered + // or non existent in the airac array + + $result = $airac; + + foreach ($custom as $section => $data) { + foreach ($data as $key => $value) { + if(!array_key_exists($key, $result[$section])) { + $result[$section][$key] = $value; + } else { + if($result[$section][$key] != $value) { + $result[$section][$key] = $value; + } + } + } + } + + return $result; + } + + /** + * Combine the airac and custom sectorfile sets + * + * @param boolean $nav [description] + * @param boolean $geo [description] + * @param boolean $regions [description] + * @param boolean $colors [description] + * @return [type] [description] + */ + private function combineFiles($nav = true, $geo = false, $regions = false, $colors = false) { + // Go through the files and combine sections that are marked to be combined + $combinedSectorFile = ''; + $combinedExtensionFile = ''; + + // Grab custom and airac .sct and .ese files + $airacDirectory = Storage::files($this->_localPath.'airac'); + $customDirectory = Storage::files($this->_localPath.'custom'); + $customDirectory2 = Storage::files($this->_localPath.'custom2'); + + $airacSectorFile = false; + $airacSectorExtensionFile = false; + + foreach ($airacDirectory as $f) { + $ext = explode('.', $f)[1]; + if($ext == 'sct') { + $airacSectorFile = $f; + } + if($ext == 'ese') { + $airacSectorExtensionFile = $f; + } + } + + $customSectorFile = false; + $customSectorExtensionFile = false; + + foreach ($customDirectory as $f) { + $ext = explode('.', $f)[1]; + if($ext == 'sct') { + $customSectorFile = $f; + } + if($ext == 'ese') { + $customSectorExtensionFile = $f; + } + } + + $customSectorFile2 = false; + $customSectorExtensionFile2 = false; + + foreach ($customDirectory2 as $f) { + $ext = explode('.', $f)[1]; + if($ext == 'sct') { + $customSectorFile2 = $f; + } + if($ext == 'ese') { + $customSectorExtensionFile2 = $f; + } + } + + if($airacSectorFile) + $sctAirac = $this->parseSectorFile(storage_path('app').'/'.$airacSectorFile); + if($customSectorFile) + $sctCustom = $this->parseSectorFile(storage_path('app').'/'.$customSectorFile, ($airacSectorFile) ? true : false); + if($customSectorFile2) + $sctCustom2 = $this->parseSectorFile(storage_path('app').'/'.$customSectorFile2, true); + + // Combine the results to a single sct file + $result = false; + if($airacSectorFile && $customSectorFile) + $result = $this->buildSectorfile($sctAirac, $sctCustom); + if($result && $customSectorFile2) + $result = $this->buildSectorfile($result, $sctCustom2); + if(!$result && $customSectorFile && $customSectorFile2) + $result = $this->buildSectorfile($sctCustom, $sctCustom2); + + return $result; + } + + private function generateCombinedSectorfile($sctData) { + $sctOutput = '; ==================================================
'; + $sctOutput.= '; VATSIM GERMANY SECTORFILE COMBINER
'; + $sctOutput.= '; This sectorfile has been generated by VATSIM Germany Sectorfile Combiner.
'; + $sctOutput.= '; This file MUST NOT be distributed to anyone outside the VATSIM Network.
'; + $sctOutput.= '; For use on the VATSIM Network ONLY.
'; + $sctOutput.= '; For FLIGHTSIMULATION use ONLY.
'; + $sctOutput.= '; ==================================================




'; + // Build Info Section + $sctOutput.= '; ==================================================
'; + $sctOutput.= '[INFO]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['info'] as $infoline) { + $sctOutput.= $infoline.'
'; + } + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '; Color Definitions
'; + $sctOutput.= '; (BLUE x 65536) + (GREEN x 256) + RED
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['colors'] as $cn => $cv) { + $sctOutput.= '#define '.$cn.' '.$cv.'
'; + } + // VOR Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[VOR]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['vor'] as $ident => $data) { + $sctOutput.= $ident.' '.$data['freq'].' '.$data['lat'].' '.$data['lon'].'
'; + } + // NDB Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[NDB]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['ndb'] as $ident => $data) { + $sctOutput.= $ident.' '.$data['freq'].' '.$data['lat'].' '.$data['lon'].'
'; + } + // FIXES Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[FIXES]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['fixes'] as $ident => $data) { + $sctOutput.= $ident.' '.$data['lat'].' '.$data['lon'].'
'; + } + // AIRPORT Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[AIRPORT]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['airport'] as $icao => $data) { + $sctOutput.= $icao.' '.$data['twrfreq'].' '.$data['lat'].' '.$data['lon'].' '.$data['as'].'
'; + } + // RUNWAY Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[RUNWAY]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['runway'] as $icao => $data) { + $sctOutput.= '; '.$icao.'
'; + foreach ($data as $rwyLine) { + $sctOutput.= $rwyLine.'
'; + } + } + // SID / STAR SECTOR FILE FORMAT DEFINITION + // An individual diagram consists of one or more lines in the sector file. Each line defines a single line segment in the diagram. The first line of the diagram definition contains the name of the diagram. The name field must be exactly 26 characters in length. If the name of the diagram is shorter than 26 characters, trailing spaces must be added to fill the 26 characters. After the first 26 characters, there can be one or more optional spaces, followed by the latitude and longitude of the start and end points of the line segment, followed by an optional color name or value. If no color name or value is given, the diagram will be drawn using the default SID or STAR color as defined in the radar client settings. + // Subsequent lines in a diagram definition must have 26 spaces, followed by the latitude and longitude for the start and end points of the current line segment, followed by an optional color name or value. In other words, subsequent segment definitions are identical to the starting segment definition, except that only the starting segment contains the name of the diagram in the first 26 characters. VRC will continue reading lines and adding them to the current diagram definition until it encounters the start of a new diagram (signified by a name present in the first 26 characters) or the start of a new section. + // SID Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[SID]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['sid'] as $icao => $runways) { + $sctOutput.= '; '.$icao.'
'; + foreach ($runways as $runway => $sids) { + $sctOutput.= '; '.$runway.'
'; + foreach ($sids as $sid => $coords) { + $firstLine = true; // New SID... + $sidIdentifier = $icao.' '.$sid; + foreach ($coords as $coord) { + if($firstLine) { + $sctOutput.= $sidIdentifier; + for($i = 0; $i < 26 - strlen($sidIdentifier) + 1; $i++) { // 26 chars ident + 1 additional whitespace + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + $firstLine = false; + } else { + for($i = 0; $i < 27; $i++) { // 26 + 1 whitespaces + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + } + } + } + } + } + // STAR Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[STAR]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['star'] as $icao => $stars) { + $sctOutput.= '; '.$icao.'
'; + foreach ($stars as $star => $coords) { + $firstLine = true; // New STAR... + $starIdentifier = $icao.' '.$star; + foreach ($coords as $coord) { + if($firstLine) { + $sctOutput.= $starIdentifier; + for($i = 0; $i < 26 - strlen($starIdentifier) + 1; $i++) { // 26 chars ident + 1 additional whitespace + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + $firstLine = false; + } else { + for($i = 0; $i < 27; $i++) { // 26 + 1 whitespaces + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + } + } + } + } + // ARTCC Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[ARTCC]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['artcc'] as $id => $coords) { + if(Str::startsWith($id, 'Restricted')) continue; + + $sctOutput.= '; '.$id.'
'; + $firstLine = true; + foreach ($coords as $coord) { + if($firstLine) { + $sctOutput.= $id; + for($i = 0; $i < 26 - strlen($id) + 1; $i++) { // 26 chars ident + 1 additional whitespace + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + $firstLine = false; + } else { + for($i = 0; $i < 27; $i++) { // 26 + 1 whitespaces + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].'
'; + } + } + } + // GEO Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[GEO]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['geo'] as $id => $geo) { + if(strlen($id) >= 26) { + // Trim it down to max 26 chars + $gid = substr($id, 0, 26); + } else { + $gid = $id; + } + $sctOutput.= '; '.$gid.'
'; + $firstLine = true; + foreach ($geo['coords'] as $coord) { + if($firstLine) { + $sctOutput.= $gid; + for($i = 0; $i < 26 - strlen($gid) + 1; $i++) { // 26 chars ident + 1 additional whitespace + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].' '.$coord['color'].'
'; + $firstLine = false; + } else { + for($i = 0; $i < 27; $i++) { // 26 + 1 whitespaces + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat_from'].' '.$coord['lon_from'].' '.$coord['lat_to'].' '.$coord['lon_to'].' '.$coord['color'].'
'; + } + } + } + // REGIONS Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[REGIONS]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['regions'] as $id => $regions) { + if(strlen($id) >= 26) { + // Trim it down to max 26 chars + $rid = substr($id, 0, 26); + } else { + $rid = $id; + } + $sctOutput.= '; Region '.$rid.'
'; + foreach ($regions as $region) { + $firstLine = true; + $sctOutput.= 'REGIONNAME '.$rid.'
'; + foreach($region['coords'] as $coord) { + if($firstLine) { + $sctOutput.= $region['color']; + for($i = 0; $i < 26 - strlen($region['color']) + 1; $i++) { // 26 chars ident + 1 additional whitespace + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat'].' '.$coord['lon'].'
'; + $firstLine = false; + } else { + for($i = 0; $i < 27; $i++) { // 26 + 1 whitespaces + $sctOutput.= ' '; + } + $sctOutput.= $coord['lat'].' '.$coord['lon'].'
'; + } + } + } + } + // AIRWAYS Section + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[HIGH AIRWAY]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['airway high'] as $airway => $points) { + foreach ($points as $p) { + $sctOutput.= $airway.' '.$p.'
'; + } + } + $sctOutput.= '

; ==================================================
'; + $sctOutput.= '[LOW AIRWAY]
'; + $sctOutput.= '; ==================================================
'; + foreach ($sctData['airway low'] as $airway => $points) { + foreach ($points as $p) { + $sctOutput.= $airway.' '.$p.'
'; + } + } + return $sctOutput; + } + + /** + * Called from the web to upload a custom sectorfile set + * and then combine it with a given airac cycle sectorfile set. + * + * @param Request $request [description] + * @return [type] [description] + */ + function combineSectorFiles(Request $request) { + + $validated = $request->validate([ + 'airacUrl' => 'url|nullable', + 'sector_one' => 'required|file', + 'sector_two' => 'file' + ]); + + if($request->has('airacUrl')) { + if(!Storage::exists($this->_localPath.$this->_airacFile)) { + $this->downloadSectorFile($validated['airacUrl']); + } + } + + // Store the custom uploaded sector kit + $request->file('sector_one')->storeAs($this->_localPath, $this->_customFile); + + if($request->hasFile('sector_two')) { + $request->file('sector_two')->storeAs($this->_localPath, $this->_customFile2); + } + + $this->unzipFiles(); + + $result = $this->combineFiles(); + + Storage::deleteDirectory($this->_localPath); + + return $this->generateCombinedSectorfile($result); + } + + /** + * Function to "parse" the ENR-2.1 PDF from EAD - DE + * + * The output will depend on the $mode switch + * + * @param Request $request + * @param mixed $mode The mode Switch + * @return Response + */ + function buildFromAIP(Request $request, $mode) { + // $aip = new \App\Models\Navigation\AIP('navigation/ED_ENR_2_1_en_2020-08-13.pdf'); + // $aip->parse(); + + // if($mode == 'sct') + // $output = $aip->buildSct(); + // elseif($mode == 'ese') + // $output = $aip->buildEse(); + // else + // $output = 'Unbekannter Modus. sct oder ese sind erlaubt.'; + + // return response($output, 200) + // ->header('Content-Type', 'text/plain'); + return "Aufgrund von rechtlichen Bedenken ist dieses Feature seitens NAV-Germany gesperrt und bis zu einer abschließenden Klärung der Rechtsgrundlage nicht nutzbar."; + } + + function test(Request $request) { + return $this->viewMake('testing.sectorcombine'); + } + +} diff --git a/app/Http/Controllers/Administration/Navigation/StationController.php b/app/Http/Controllers/Administration/Navigation/StationController.php new file mode 100644 index 0000000..61db61b --- /dev/null +++ b/app/Http/Controllers/Administration/Navigation/StationController.php @@ -0,0 +1,163 @@ +middleware(function ($request, $next) { + + if(!$this->account->hasPermission('administration.navigation')) + abort(403); + + return $next($request); + }); + } + + /** + * Get stations + * + * @param Request $request [description] + * @return [type] [description] + */ + public function index(Request $request) + { + if($request->ajax()) { + return Station::orderBy('ident', 'ASC')->get(); + } + abort(403); + } + + /** + * Updates a station + * + * @param Request $request [description] + * @param Station $station [description] + * @return [type] [description] + */ + public function updateStation(Request $request, Station $station) + { + if($request->ajax()) { + $validated = $request->validate([ + 'station.name' => 'required|string', + 'station.ident' => 'required|string', + 'station.frequency' => 'required|numeric', + 'station.atis' => 'required|boolean', + 'station.bookable' => 'required|boolean', + ]); + + $validator = Validator::make($request->all(), [ + 'station.frequency' => [ + 'required', + 'numeric', + function($attribute, $value, $fail) { + if(preg_match("/(\d{4,}(\.|,)|(\.|,)\d{4,})/", $value)) { + $fail('The frequency must have 3 digits before and not more than 3 digits behind the dot.'); + } + } + ] + ])->validate(); + + $station->name = $validated['station']['name']; + $station->ident = $validated['station']['ident']; + $station->frequency = floatval($validated['station']['frequency']); + $station->atis = boolval($validated['station']['atis']); + $station->bookable = boolval($validated['station']['bookable']); + + $station->save(); + + $this->account->notify(new StationUpdatedNotification($station)); + + return $station; + } + abort(403); + } + + /** + * Creates a station + * + * @param Request $request [description] + * @return [type] [description] + */ + public function createStation(Request $request) + { + if($request->ajax()) { + $validated = $request->validate([ + 'station.name' => 'required|string', + 'station.ident' => 'required|string', + 'station.frequency' => 'required|numeric', + 'station.atis' => 'required|boolean', + 'station.bookable' => 'required|boolean', + ]); + + $validator = Validator::make($request->all(), [ + 'station.frequency' => [ + 'required', + 'numeric', + function($attribute, $value, $fail) { + if(preg_match("/(\d{4,}(\.|,)|(\.|,)\d{4,})/", $value)) { + $fail('The frequency must have 3 digits before and not more than 3 digits behind the dot.'); + } + } + ] + ])->validate(); + + $station = new Station(); + $station->name = $validated['station']['name']; + $station->ident = $validated['station']['ident']; + $station->frequency = floatval($validated['station']['frequency']); + $station->atis = boolval($validated['station']['atis']); + $station->bookable = boolval($validated['station']['bookable']); + + $station->save(); + + $this->account->notify(new StationCreatedNotification($station)); + + return $station; + } + abort(403); + } + + /** + * Deletes a station + * + * @param Request $request [description] + * @param Station $station [description] + * @return [type] [description] + */ + public function deleteStation(Request $request, Station $station) + { + if($request->ajax()) { + $station->aerodromes()->detach(); + + if($station->bookable) { + // Find all bookings for that station and remove those + $bookings = \App\Models\Booking\AtcSessionBooking::forStation($station->id)->get(); + foreach ($bookings as $b) { + // TODO: When vatbook hook is implemented... + // Also remove vatbook entry from here + $b->delete(); + } + } + $ident = $station->ident; + $station->delete(); + $this->account->notify(new StationRemovedNotification($ident)); + + return $ident; + } + abort(403); + } + +} diff --git a/app/Http/Controllers/Administration/Regionalgroup/RegionalgroupController.php b/app/Http/Controllers/Administration/Regionalgroup/RegionalgroupController.php new file mode 100644 index 0000000..969d380 --- /dev/null +++ b/app/Http/Controllers/Administration/Regionalgroup/RegionalgroupController.php @@ -0,0 +1,209 @@ +middleware(function ($request, $next) { + + if (!$this->account->hasAnyPermission('administration.regionalgroup', 'administration.regionalgroup.rg')) { + abort(403); + } + + return $next($request); + }); + } + + /** + * Get all regionalgroups a user can administrate + * + * @param Request $request [description] + * @return [type] [description] + */ + public function index(Request $request) + { + if ($request->ajax()) { + $regionalgroups = Regionalgroup::select('id','name','email','fir_id','chief_id','deputy_id')->with('fir:id,name')->orderBy('name', 'ASC')->get(); + if ($this->account->hasPermission('administration.regionalgroup')) { + return $regionalgroups; + } elseif ($this->account->hasPermission('administration.regionalgroup.rg')) { + // Only if the account is in the staff team we grant access + return $regionalgroups->filter(function ($rg) { + return $this->account->id == $rg->chief_id || $this->account->id == $rg->deputy_id; + }); + + } + } + abort(403); + } + + /** + * Get a detailed presentation of a regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return [type] [description] + */ + public function show(Request $request, Regionalgroup $regionalgroup) + { + if (!$request->ajax()) abort(403); + if ($this->hasRGPermission($regionalgroup)){ + $regionalgroup->loadMissing('fir', 'accounts', 'accounts.data', 'chief', 'deputy', 'mentors', 'navigators', 'eventler', 'requests.account', 'requests.account.setting', 'requests.account.data', 'templates'); + return $regionalgroup; + } + abort(422); + } + + /** + * Update forum group assignments of this regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return [type] [description] + */ + public function update(Request $request, Regionalgroup $regionalgroup) + { + if ($request->ajax()) { + // As this permission is only for systemadministrators and only those shall be able to set these! + if ($this->account->hasPermission('administration.regionalgroup')) { + + $validated = $request->validate([ + 'staff_group_id' => 'required|integer|exists:forumgroups,id', + 'voting_group_id' => 'required|integer|exists:forumgroups,id', + 'mentor_group_id' => 'required|integer|exists:forumgroups,id', + 'navler_group_id' => 'required|integer|exists:forumgroups,id', + 'eventler_group_id' => 'required|integer|exists:forumgroups,id', + 'member_group_id' => 'required|integer|exists:forumgroups,id', + 'guest_group_id' => 'required|integer|exists:forumgroups,id', + ]); + + $regionalgroup->staff_group_id = $validated['staff_group_id']; + $regionalgroup->voting_group_id = $validated['voting_group_id']; + $regionalgroup->mentor_group_id = $validated['mentor_group_id']; + $regionalgroup->navler_group_id = $validated['navler_group_id']; + $regionalgroup->eventler_group_id = $validated['eventler_group_id']; + $regionalgroup->member_group_id = $validated['member_group_id']; + $regionalgroup->guest_group_id = $validated['guest_group_id']; + + $regionalgroup->save(); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroup) + ->log("Hat die Regionalgruppe {$regionalgroup->name} verändert!"); + + return $regionalgroup; + } + } + abort(403); + } + + /** + * Removes an account from the regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @param Account $account [description] + * @return int [description] + */ + public function removeAccount(Request $request, Regionalgroup $regionalgroup, Account $account) + { + if (!$request->ajax()) abort(403); + + $regionalgroup->mentors()->detach($account); + $regionalgroup->navigators()->detach($account); + $regionalgroup->eventler()->detach($account); + + $regionalgroup->accounts()->detach($account); + $regionalgroup->save(); + + $this->updateAccountPermissionsAndGroups($account); + + activity() + ->causedBy($this->account) + ->performedOn($account) + ->log("Wurde aus der Regionalgruppe {$regionalgroup->name} entfernt!"); + + return $account->id; + } + + + + protected function updateAccountPermissionsAndGroups(Account $account) { + // Then find out if the account has any remaining positions within any regionalgroup + $regionalgroups = Regionalgroup::all(); + $isMemberAnywhere = false; + $isStaffAnywhere = false; + $isMentorAnywhere = false; + $isNavAnywhere = false; + $isEventAnywhere = false; + foreach ($regionalgroups as $rg) { + if ($account->isMemberOfRegionalgroup($rg)) { + $isMemberAnywhere = true; + if($rg->chief_id == $account->id + || $rg->deputy_id == $account->id) { + $isStaffAnywhere = true; + } + $isMentorAnywhere = $account->isMentorOfRegionalgroup($rg); + $isNavAnywhere = $account->isNavigatorOfRegionalgroup($rg); + $isEventAnywhere = $account->isEventlerOfRegionalgroup($rg); + break; + } + } + // Now regarding to the standings. Remove permissions and group assignments as needed + if(!$isMemberAnywhere) { + // Not a member anywhere, but still might be guest + if($isStaffAnywhere) { // Can not be is a staff group as guest only + GroupHelper::revoke($account, ['rg.leitung']); + } + } else { + // Member anywhere + if(!$isStaffAnywhere) { + GroupHelper::revoke($account, ['rg.leitung']); + } + } + if(!$isMentorAnywhere) { + GroupHelper::revoke($account, ['rg.mentor']); + } + if(!$isNavAnywhere) { + GroupHelper::revoke($account, ['rg.nav']); + } + if(!$isEventAnywhere) { + GroupHelper::revoke($account, ['rg.event']); + } + // do the gitlab stuff + //$gitlab = new Gitlab(); + //$gitlab->checkNAVAssignments($account); + } + + /** + * Current Account can manage all RGs or is RG Chief/Deputy + * + * @param Regionalgroup $regionalgroup [description] + * @return bool + */ + protected function hasRGPermission(Regionalgroup $regionalgroup): bool + { + if ($this->account->hasPermission('administration.regionalgroup')) return true; + if ($this->account->hasPermission('administration.regionalgroup.rg')){ + if ($this->account->id == $regionalgroup->chief_id || $this->account->id == $regionalgroup->deputy_id) return true; + } + return false; + } + +} diff --git a/app/Http/Controllers/Administration/Regionalgroup/RequestController.php b/app/Http/Controllers/Administration/Regionalgroup/RequestController.php new file mode 100644 index 0000000..49d390d --- /dev/null +++ b/app/Http/Controllers/Administration/Regionalgroup/RequestController.php @@ -0,0 +1,214 @@ +ajax()) { + $validated = $request->validate([ + 'notificationDetails' => 'string|nullable', + ]); + // Handle join requests + if ($regionalgroupRequest->topic == 'join') { + $this->handleJoinRequest($regionalgroup, $regionalgroupRequest, $validated['notificationDetails']); + } + if ($regionalgroupRequest->topic == 'leave') { + $this->handleLeaveRequest($regionalgroup, $regionalgroupRequest, $validated['notificationDetails']); + } + if ($regionalgroupRequest->topic == 'change') { + $this->handleChangeRequest($regionalgroup, $regionalgroupRequest, $validated['notificationDetails']); + } + // Request type is not known + // or our work has been done + // Remove and return + $rrid = $regionalgroupRequest->id; + $regionalgroupRequest->delete(); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroupRequest->account) + ->log("Hat die Regionalgruppeanfrage mit der RRID {$rrid} akzeptiert!"); + + return $rrid; + } + abort(403); + } + + /** + * Just deny and delete a regionalgroup request + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @param RegionalgroupRequest $regionalgroupRequest [description] + * @return [type] [description] + */ + public function denyRequest(Request $request, Regionalgroup $regionalgroup, RegionalgroupRequest $regionalgroupRequest) + { + if ($request->ajax()) { + $validated = $request->validate([ + 'notificationDetails' => 'string|nullable', + ]); + $regionalgroupRequest->account->notify(new RequestDeniedNotification($regionalgroup, $validated['notificationDetails'] )); + + // Remove and return + $rrid = $regionalgroupRequest->id; + $regionalgroupRequest->delete(); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroupRequest->account) + ->log("Hat die Regionalgruppenanfrage mit der RRID {$rrid} abgelehnt!"); + + return $rrid; + } + abort(403); + } + + /** + * Handle any form of join requests + * + * @param Regionalgroup $regionalgroup [description] + * @param RegionalgroupRequest $regionalgroupRequest [description] + * @return [type] [description] + */ + private function handleJoinRequest(Regionalgroup $regionalgroup, RegionalgroupRequest $regionalgroupRequest, $notificationDetails = null) + { + // If the user wants to join as a guest, we can accept regardless of any other regionalgroup assignments + // But if the user is a fullmember anywhere else and wants to join as a member at this regionalgroup, we must deny. + if ($regionalgroupRequest->type == 'guest') { + /* + $asPilot = $regionalgroupRequest->as == 'pilot'; + $asController = $regionalgroupRequest->as == 'controller'; + if ($regionalgroupRequest->as == 'both') { + $asPilot = $asController = true; + } + */ + $regionalgroup->accounts()->attach($regionalgroupRequest->account, ['guest' => true, 'pilot' => true, 'controller' => true]); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroupRequest->account) + ->log("Ist der Regionalgruppe {$regionalgroup->name} als Gastmitglied beigetreten!"); + + $regionalgroupRequest->account->notify(new RequestAcceptedNotification($regionalgroup, $notificationDetails)); + + return true; + } + if ($regionalgroupRequest->type == 'member') { + // First let's check for any other regionalgroup membership + $regionalgroups = Regionalgroup::all(); + $targetAccount = $regionalgroupRequest->account; + + $isMemberAnywhere = false; + foreach ($regionalgroups as $rg) { + if ($targetAccount->isMemberOfRegionalgroup($rg)) { + $isMemberAnywhere = true; + break; + } + } + if ($isMemberAnywhere) { + return false; + } + + // Now let's put the user into the group + /* + $asPilot = $regionalgroupRequest->as == 'pilot'; + $asController = $regionalgroupRequest->as == 'controller'; + if ($regionalgroupRequest->as == 'both') { + $asPilot = $asController = true; + } + */ + $regionalgroup->accounts()->attach($targetAccount, ['guest' => false, 'pilot' => true, 'controller' => true]); + + activity() + ->causedBy($this->account) + ->performedOn($targetAccount) + ->log("Ist der Regionalgruppe {$regionalgroup->name} als Vollmitglied beigetreten!"); + + $regionalgroupRequest->account->notify(new RequestAcceptedNotification($regionalgroup, $notificationDetails)); + + return true; + } + return false; + } + + /** + * Handle any kind of leave request + * + * @param Regionalgroup $regionalgroup [description] + * @param RegionalgroupRequest $regionalgroupRequest [description] + * @return [type] [description] + */ + private function handleLeaveRequest(Regionalgroup $regionalgroup, RegionalgroupRequest $regionalgroupRequest, $notificationDetails = null) + { + // first kick the user out of the regionalgroup + $regionalgroup->accounts()->detach($regionalgroupRequest->account); + $regionalgroup->save(); + + $this->updateAccountPermissionsAndGroups($regionalgroupRequest->account); + + $regionalgroupRequest->account->notify(new RequestAcceptedNotification($regionalgroup, $notificationDetails)); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroupRequest->account) + ->log("Hat die Regionalgruppe {$regionalgroup->name} verlassen!"); + + + return true; + } + + /** + * Handle any kind of change request + * + * @param Regionalgroup $regionalgroup [description] + * @param RegionalgroupRequest $regionalgroupRequest [description] + * @return [type] [description] + */ + private function handleChangeRequest(Regionalgroup $regionalgroup, RegionalgroupRequest $regionalgroupRequest, $notificationDetails = null) + { + $regionalgroup->accounts()->detach($regionalgroupRequest->account); + $regionalgroup->save(); + + $this->updateAccountPermissionsAndGroups($regionalgroupRequest->account); + + // Then add to the same RG with new type + $asGuest = $regionalgroupRequest->type == 'guest'; + $regionalgroup->accounts()->attach($regionalgroupRequest->account, ['guest' => $asGuest, 'pilot' => true, 'controller' => true]); + + activity() + ->causedBy($this->account) + ->performedOn($regionalgroupRequest->account) + ->log("Hat seinen Mitgliedschaftstypus in der Regionalgruppe {$regionalgroup->name} auf " . ($asGuest ? 'Gastmitglied' : 'Vollmitglied') . " verändert!"); + + $regionalgroupRequest->account->notify(new RequestAcceptedNotification($regionalgroup, $notificationDetails)); + + return true; + } + +} diff --git a/app/Http/Controllers/Administration/Regionalgroup/StaffController.php b/app/Http/Controllers/Administration/Regionalgroup/StaffController.php new file mode 100644 index 0000000..1281ad3 --- /dev/null +++ b/app/Http/Controllers/Administration/Regionalgroup/StaffController.php @@ -0,0 +1,248 @@ +ajax()) abort(403); + + $validated = $request->validate([ + 'mentor' => 'required|exists:membership_accounts,id', + ]); + + // We know, that the given "Mentor-Id" is actually an account + // Now we need to check if the id is assigned to the regionalgroup + $mentor = Account::find($validated['mentor']); + + if ($mentor != null) { + if ($mentor->isMemberOfRegionalgroup($regionalgroup) || $mentor->isGuestOfRegionalgroup($regionalgroup)) { + if (collect($regionalgroup->mentors)->where('id', $mentor->id)->count() > 0) abort(422, 'User is already part of the team.'); + $regionalgroup->mentors()->attach($mentor); + $regionalgroup->save(); + $this->updateAccountPermissionsAndGroups($mentor); + GroupHelper::assign($mentor, ['rg.mentor']); + return $mentor; + } + } + abort(422); + } + + /** + * Removes a mentor from a regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @param Account $mentor [description] + * @return Account [description] + */ + public function removeMentor(Request $request, Regionalgroup $regionalgroup, Account $mentor) + { + if (!$request->ajax()) abort(403); + + $regionalgroup->mentors()->detach($mentor); + $regionalgroup->save(); + $this->updateAccountPermissionsAndGroups($mentor); + return $mentor; + } + + /** + * Assigns a new navigator to the regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return Account [description] + */ + public function assignNavigator(Request $request, Regionalgroup $regionalgroup) + { + if (!$request->ajax()) abort(403); + + $validated = $request->validate([ + 'navigator' => 'required|exists:membership_accounts,id', + ]); + + // We know, that the given "Id" is actually an account + // Now we need to check if the id is assigned to the regionalgroup + $navigator = Account::find($validated['navigator']); + + if ($navigator != null) { + if ($navigator->isMemberOfRegionalgroup($regionalgroup) || $navigator->isGuestOfRegionalgroup($regionalgroup)) { + //check is member a navigator already + if (collect($regionalgroup->navigators)->where('id', $navigator->id)->count() > 0) abort(422, 'User is already part of the team.'); + $regionalgroup->navigators()->attach($navigator); + $this->updateAccountPermissionsAndGroups($navigator); + $regionalgroup->save(); + GroupHelper::assign($navigator, ['rg.nav']); + return $navigator; + } + } + abort(422); + } + + /** + * Removes a navigator from a regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @param Account $navigator [description] + * @return Account [description] + */ + public function removeNavigator(Request $request, Regionalgroup $regionalgroup, Account $navigator) + { + if (!$request->ajax()) abort(403); + + $regionalgroup->navigators()->detach($navigator); + $this->updateAccountPermissionsAndGroups($navigator); + $regionalgroup->save(); + return $navigator; + } + + /** + * Assigns a new event team member to the regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return Account [description] + */ + public function assignEventler(Request $request, Regionalgroup $regionalgroup) + { + if (!$request->ajax()) abort(403); + + $validated = $request->validate([ + 'eventler' => 'required|exists:membership_accounts,id', + ]); + + // We know, that the given "Id" is actually an account + // Now we need to check if the id is assigned to the regionalgroup + $eventler = Account::find($validated['eventler']); + + if ($eventler != null) { + if ($eventler->isMemberOfRegionalgroup($regionalgroup) || $eventler->isGuestOfRegionalgroup($regionalgroup)) { + if (collect($regionalgroup->eventler)->where('id', $eventler->id)->count() > 0) abort(422, 'User is already part of the team.'); + $regionalgroup->eventler()->attach($eventler); + $regionalgroup->save(); + $this->updateAccountPermissionsAndGroups($eventler); + GroupHelper::assign($eventler, ['rg.event']); + return $eventler; + } + } + abort(422); + } + + /** + * Removes a event team member from a regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @param Account $eventler [description] + * @return Account [description] + */ + public function removeEventler(Request $request, Regionalgroup $regionalgroup, Account $eventler) + { + if (!$request->ajax()) abort(403); + + $regionalgroup->eventler()->detach($eventler); + $regionalgroup->save(); + $this->updateAccountPermissionsAndGroups($eventler); + return $eventler; + } + + /** + * Change the chief of a regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return Account [description] + */ + public function assignChief(Request $request, Regionalgroup $regionalgroup) + { + if (!$request->ajax()) abort(403); + + if ($request->has('newChief') && $request->newChief == -1) { + //try to remove permisson group of old chief + GroupHelper::revokeViaId($regionalgroup->chief_id, ['rg.leitung']); + + $regionalgroup->chief_id = null; + $regionalgroup->save(); + return null; + } + $validated = $request->validate([ + 'newChief' => 'required|exists:membership_accounts,id', + ]); + + // The new chief must be at lease a full member of the regionalgroup + $newChief = Account::find($validated['newChief']); + + if ($newChief != null && $newChief->isMemberOfRegionalgroup($regionalgroup) || $newChief->isGuestOfRegionalgroup($regionalgroup)) { + //try to remove permisson group of old chief + GroupHelper::revokeViaId($regionalgroup->chief_id, ['rg.leitung']); + + $regionalgroup->chief()->associate($newChief); + $regionalgroup->save(); + + //try to give permisson group to new chief + GroupHelper::assignViaId($regionalgroup->chief_id, ['rg.leitung']); + + return $newChief; + } + abort(422); + } + + /** + * Change the deputy of the regionalgroup + * + * @param Request $request [description] + * @param Regionalgroup $regionalgroup [description] + * @return Account [description] + */ + public function assignDeputy(Request $request, Regionalgroup $regionalgroup) + { + if (!$request->ajax()) abort(403); + + if ($request->has('newDeputy') && $request->newDeputy == -1) { + //try to remove permisson group of old deputy + GroupHelper::revokeViaId($regionalgroup->deputy_id, ['rg.leitung']); + + $regionalgroup->deputy_id = null; + $regionalgroup->save(); + return null; + } + $validated = $request->validate([ + 'newDeputy' => 'required|exists:membership_accounts,id', + ]); + + // The new deputy must be at lease a full member of the regionalgroup + $newDeputy = Account::find($validated['newDeputy']); + + if ($newDeputy != null && $newDeputy->isMemberOfRegionalgroup($regionalgroup) || $newDeputy->isGuestOfRegionalgroup($regionalgroup)) { + //try to remove permisson group of old deputy + GroupHelper::revokeViaId($regionalgroup->deputy_id, ['rg.leitung']); + + $regionalgroup->deputy()->associate($newDeputy); + $regionalgroup->save(); + + //try to give permisson group to new deputy + GroupHelper::assignViaId($regionalgroup->deputy_id, ['rg.leitung']); + + return $newDeputy; + } + + abort(422); + } + +} diff --git a/app/Http/Controllers/Administration/Regionalgroup/TemplateController.php b/app/Http/Controllers/Administration/Regionalgroup/TemplateController.php new file mode 100644 index 0000000..f6b8155 --- /dev/null +++ b/app/Http/Controllers/Administration/Regionalgroup/TemplateController.php @@ -0,0 +1,73 @@ +middleware(function ($request, $next) { + if (! $request->ajax()) abort(422); + + return $next($request); + }); + } + + public function create(Regionalgroup $regionalgroup, Request $request){ + if (! $this->hasRGPermission($regionalgroup)) abort(403); + + $validated = $request->validate( + [ + 'name' => 'required|string|max:25', + 'message' => 'required|string|max:1024', + 'order' => 'integer|min:0', + ] + ); + + $template = new RegionalgroupTemplate(); + $template->order = $validated['order'] ? $validated['order'] : 0; + $template->regionalgroup_id = $regionalgroup->id; + $template->message = $validated['message']; + $template->name = $validated['name']; + $template->save(); + + return $template; + } + + public function edit(Regionalgroup $regionalgroup, RegionalgroupTemplate $template, Request $request){ + if (! $this->hasRGPermission($regionalgroup)) abort(403); + if ($template->regionalgroup_id != $regionalgroup->id) abort(403); + + $validated = $request->validate( + [ + 'name' => 'required|string|max:25', + 'message' => 'required|string|max:1024', + 'order' => 'integer|min:0', + ] + ); + + $template->order = $validated['order'] ? $validated['order'] : 0; + $template->message = $validated['message']; + $template->name = $validated['name']; + $template->save(); + + return $template; + } + + public function delete(Regionalgroup $regionalgroup,RegionalgroupTemplate $template, Request $request){ + if (! $this->hasRGPermission($regionalgroup)) abort(403); + if ($template->regionalgroup_id != $regionalgroup->id) abort(403); + + $template_id = $template->id; + $template->delete(); + + return $template; + } +} diff --git a/app/Http/Controllers/Administration/Services/GitlabController.php b/app/Http/Controllers/Administration/Services/GitlabController.php new file mode 100644 index 0000000..f8c43d0 --- /dev/null +++ b/app/Http/Controllers/Administration/Services/GitlabController.php @@ -0,0 +1,37 @@ +middleware(function ($request, $next) { + + if (!$this->account->hasPermission('administration.services.gitlab')) { + abort(403); + } + + return $next($request); + }); + } + + public function createAccount() + { + //$gitlab = new Gitlab(); + //$status = $gitlab->createAccount($this->account); + //$gitlab->checkNAVAssignments($this->account); + //if($status == true) return; + //else + abort(499); + } + + public function getSettings() + { + return \Response::json($this->account->setting); + } + +} diff --git a/app/Http/Controllers/Authentication/VatsimConnectController.php b/app/Http/Controllers/Authentication/VatsimConnectController.php new file mode 100644 index 0000000..bc72aba --- /dev/null +++ b/app/Http/Controllers/Authentication/VatsimConnectController.php @@ -0,0 +1,225 @@ +_provider = new VatauthProvider(); + } + + public function login(Request $request) + { + if(!$request->has('code') || !$request->has('state')) + { + try{ + $response = \Illuminate\Support\Facades\Http::timeout(3)->get(config('vatsim_auth.base')); + if($response->successful()) { + // Unkown authentication process state. + // Begin at step 1 + $authUrl = $this->_provider->getAuthorizationUrl(); + $request->session()->put('vatsimauthstate', $this->_provider->getState()); + return redirect()->away($authUrl); + } else { + // Vatsimauth not available. Fallback to local sign in process. + return redirect()->route('vatauth.failed'); + } + } catch(\Illuminate\Http\Client\ConnectionException $ce) { + // Vatsimauth not available. Fallback to local sign in process. + return redirect()->route('vatauth.failed'); + } + } elseif ($request->input('state') !== session()->pull('vatsimauthstate')) { + // Wrong state detected. Fallback to failed state + return redirect()->route('vatauth.failed'); + } else { + return $this->_verifyLogin($request); // Do the login!!! + } + } + + protected function _verifyLogin(Request $request) + { + try { + $accessToken = $this->_provider->getAccessToken('authorization_code', [ + 'code' => $request->input('code') + ]); + } catch (IdentityProviderException $e) { + return redirect()->route('vatauth.failed'); + } + + /** + * Get the resource owner + * @var ResourceOwner Object + */ + try{ + $resourceOwner = json_decode(json_encode($this->_provider->getResourceOwner($accessToken)->toArray())); + } catch(UnexpectedValueException $e){ + return redirect()->route('vatauth.failed'); + } + + if( !isset($resourceOwner->data) || + !isset($resourceOwner->data->cid) || + !isset($resourceOwner->data->personal->name_first) || + !isset($resourceOwner->data->personal->name_last) || + !isset($resourceOwner->data->personal->email) || + $resourceOwner->data->oauth->token_valid !== "true" + ) { + return redirect()->route('vatauth.failed'); + } + // Check for duplicate email + if(Account::where('email', $resourceOwner->data->personal->email)->where('id', '!=', $resourceOwner->data->cid)->exists()) + { + return $this->viewMake('frontend.authentication.dupemail'); + } + + activity()->disableLogging(); // Disable logging here to only log data updated from the automated system. + + $account = $this->_completeLogin($resourceOwner, $accessToken); + + auth()->login($account, true); + + activity()->enableLogging(); + + return redirect()->intended(route('membership.home')); + } + + + + protected function _completeLogin($resourceOwner, $accessToken) + { + // Find the Account. + $account = Account::find($resourceOwner->data->cid); + //If not we need to create a new one + if(!$account) { + // Create new one + $account = new Account; + $account->id = $resourceOwner->data->cid; + // Now we need to set a token for local api calls + $account->api_token = \Illuminate\Support\Str::random(80); + } else { + // Refresh the API token + $account->renewApiToken(); + } + $account->firstname = $resourceOwner->data->personal->name_first; + $account->lastname = $resourceOwner->data->personal->name_last; + + // $account->firstname = mb_convert_encoding($resourceOwner->data->personal->name_first, "Windows-1252", "UTF-8"); + // $account->lastname = mb_convert_encoding($resourceOwner->data->personal->name_last, "Windows-1252", "UTF-8"); + + $account->email = $resourceOwner->data->personal->email; + $account->save(); + + // + // If the user has given us permanent access to the data + if($resourceOwner->data->oauth->token_valid) { + $account->access_token = $accessToken->getToken(); + $account->refresh_token = $accessToken->getRefreshToken(); + $account->token_expires = $accessToken->getExpires(); + } + + $account->save(); + + $account->loadMissing('setting'); + if (null == $account->setting) { + /* + * This account does not have a setting association. + * It is a new account and we need to create that now + */ + $account->setting()->save( + new \App\Models\Membership\Account\Setting( + ['language' => Session::get('language')] + ) + ); + } else { + // Set the session language according to users setting + Session::put('language', $account->setting->language); + } + + $account->loadMissing('data'); + if (null == $account->data) { + $account->data()->save(new \App\Models\Membership\Account\Data(['account_id' => $account->id])); + $account->load('data'); + } + + $account->data->active = true; + $account->data->suspended = false; + + /** + * If the scope vatsim_details was granted + */ + if(isset($resourceOwner->data->vatsim) && isset($resourceOwner->data->personal->country)) { + $account->data->rating_atc = $resourceOwner->data->vatsim->rating->id; + $account->data->rating_pilot = $resourceOwner->data->vatsim->pilotrating->id; + + $account->data->active = $account->data->rating_atc > 0; + $account->data->suspended = $account->data->rating_atc < 0; + + /* + * Update accounts region/division associations + */ + // User country + $account->data->country_code = $resourceOwner->data->personal->country->id; + $account->data->country_name = $resourceOwner->data->personal->country->name; + // User Region + $account->data->region_code = $resourceOwner->data->vatsim->region->id; + $account->data->region_name = $resourceOwner->data->vatsim->region->name; + // User Division + $account->data->division_code = $resourceOwner->data->vatsim->division->id; + $account->data->division_name = $resourceOwner->data->vatsim->division->name; + // User Subdivision + $account->data->subdivision_code = $resourceOwner->data->vatsim->subdivision->id; + $account->data->subdivision_name = $resourceOwner->data->vatsim->subdivision->name; + } + + $account->data->save(); + + return $account; + } + + public function logout() + { + auth()->logout(); + + return redirect()->route('home'); + } + + public function failed() + { + return $this->viewMake('frontend.authentication.failed'); + } + + public function local(Request $request) + { + $validated = $request->validate( + [ + 'cid' => 'required|exists:membership_accounts,id', + 'lpwd' => 'required', + ] + ); + + if(Auth::attempt(['id' => $validated['cid'], 'password' => $validated['lpwd']])) { + return redirect()->route('membership.home'); + } else { + return redirect()->route('vatauth.failed'); + } + } + +} diff --git a/app/Http/Controllers/Authorization/AuthorizationController.php b/app/Http/Controllers/Authorization/AuthorizationController.php new file mode 100644 index 0000000..69b0654 --- /dev/null +++ b/app/Http/Controllers/Authorization/AuthorizationController.php @@ -0,0 +1,81 @@ +ajax()) abort(403); + + + if ($chart->public_available) { + return response()->json([ + 'access' => true, + 'token' => false + ]); + } + /** + * If it is an internal, non-public chart + * we need to have an authenticated and completely registered account. + */ + if(!Auth::check() || !$this->account->setup_completed) { + return response()->json([ + 'access' => false, + ]); + } + + + if(!$chart->getIsGitlabAttribute()) { + //nonpublic chart but not on gitlab server + $encodedSecretKey = config('paseto.paseto_key'); + if(empty($encodedSecretKey)) { + return response()->json([ + 'access' => false, + ]); + } + $secretKey = \ParagonIE\Paseto\Keys\AsymmetricSecretKey::fromEncodedString($encodedSecretKey); + $token = \ParagonIE\Paseto\Builder::getPublic($secretKey, new \ParagonIE\Paseto\Protocol\Version2()) + ->setExpiration(\Carbon\Carbon::now()->addSeconds(90)) + ->setIssuer('vatsim-germany.org') + ->setAudience('nav.vatsim-germany.org') + ->setSubject('VATSIM Germany chart download authorization') + ->setIssuedAt(\Carbon\Carbon::now()) + ->setNotBefore(\Carbon\Carbon::now()) + ->set('files', [basename($chart->href)]); + + return response()->json([ + 'access' => true, + 'token' => $token->toString(), + ]); + + } + //nonpublic chart on gitlab server + + // there is no access + return response()->json([ + 'access' => false, + ]); + + + } + +} diff --git a/app/Http/Controllers/Bookings/AtcBookingController.php b/app/Http/Controllers/Bookings/AtcBookingController.php new file mode 100644 index 0000000..8eb4e52 --- /dev/null +++ b/app/Http/Controllers/Bookings/AtcBookingController.php @@ -0,0 +1,475 @@ +vatbookBaseUrl = config('booking.vatbookBaseUrl'); + $this->eventBaseUrl = config('booking.eventBaseUrl'); + $this->testing = config('booking.testing'); + + // See https://stackoverflow.com/questions/30959210/set-default-query-string-in-guzzle-6 for details + // on this approach regarding the TEST parameter added to test bookings + $handler = \GuzzleHttp\HandlerStack::create(); + + // Add TEST parameter if we are only testing + if ($this->testing) { + $handler->push(\GuzzleHttp\Middleware::mapRequest(function (\Psr\Http\Message\RequestInterface $request) { + return $request->withUri(\GuzzleHttp\Psr7\Uri::withQueryValue($request->getUri(), 'TEST', '1')); + })); + } + + $this->client = new \GuzzleHttp\Client([ + 'base_uri' => $this->vatbookBaseUrl, + 'handler' => $handler, + ]); + + } + + /** + * Gets all bookings within the next 48 hours + * + * @param Request $request [description] + * @return [type] [description] + */ + public function index(Request $request) + { + if($request->ajax()) { + + return AtcSessionBooking::orderBy('starts_at', 'ASC') + ->with('station', 'controller') + ->whereBetween('ends_at', [Carbon::now()->utc(), Carbon::now()->utc()->addHours(48)]) + ->get(); + } + } + + /** + * Gets bookings between a given start date and an optional end date + * + * The start date will allways be set to the date with 00:00:00 hours set. + * The end date will in any case be set to the date with 23:59:59 appended. + * + * @param Request $request [description] + * @param [type] $start [description] + * @param [type] $end [description] + * @return [type] [description] + */ + public function dateRange(Request $request, $start, $end = null) + { + if($request->ajax()) { + try { + $s = Carbon::createFromFormat('d.m.Y', $start); + if ($end == null || $end == 'null') { + // Only a start date has been passed. + // So we will find any bookings that are made for that day + + // build a fake enddate that will be the startdate plus 23:59:59 + // so the end of that day + $e = Carbon::createFromFormat('d.m.Y', $start) + ->addHours(23) + ->addMinutes(59) + ->addSeconds(59); + } else { + $e = Carbon::createFromFormat('d.m.Y', $end) + ->addHours(23) + ->addMinutes(59) + ->addSeconds(59); + } + // As we now have our end date + // we can try to find stuff + return AtcSessionBooking::orderBy('starts_at', 'ASC') + ->with('station', 'controller') + ->whereBetween('starts_at', [$s, $e]) + ->orWhereBetween('ends_at', [$s, $e]) + ->get(); + } catch (InvalidArgumentException $e) { + return null; + } + } + } + + /** + * Gets all bookings of a given controller + * + * @param Request $request [description] + * @return [type] [description] + */ + public function personal(Request $request) + { + if($request->ajax()) { + return AtcSessionBooking::forAccountId($this->account->id) + ->with('station') + ->where('starts_at', '>=', Carbon::now()->utc()->subDays(1)) + ->orderBy('starts_at', 'ASC') + ->get(); + } + } + + /** + * Evaluates and creates a new booking + * + * @param Request $request [description] + * @return [type] [description] + */ + public function book(Request $request) + { + if($request->ajax()) + { + // Validate the requested data + // If this fails a 422 response with validation errors will be send back + $validated = $request->validate([ + 'station' => 'required|exists:navigation_stations,id', + 'from' => 'required|date_format:"d.m.Y H:i"', + 'till' => 'required|date_format:"d.m.Y H:i"', + 'training' => 'boolean', + 'event' => 'boolean', + 'voice' => 'boolean', + ]); + + // Let's validate even further + $this->account->loadMissing('data'); + + $now = \Carbon\CarbonImmutable::now('UTC'); + + $startsAt = Carbon::createFromFormat('d.m.Y H:i', $validated['from'], 'UTC'); + $endsAt = Carbon::createFromFormat('d.m.Y H:i', $validated['till'], 'UTC'); + // Perform important checks the default validation can not handle + // Check that the end is after the start date + // and that the start is maximum 2 hours in the past + + // Accept booking only if the start is maximum 2 hours in the past. + if($now->diffInHours($startsAt, false) < -2) { + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframePast'), + ], + ], + 422 + ); + } + + // Accept booking only if the begin is prior to the end + if ($endsAt <= $startsAt) { + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframeSense'), + ], + ], + 422 + ); + } + + // Check for timeframe size + // Session must be at least 60 minutes long and must not exceed 24 hours + $bookingLength = $startsAt->diffInHours($endsAt); + if ($bookingLength < 1 || $bookingLength >= 24) { + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframeLimits'), + ], + ], + 422 + ); + } + // The account must be eligable to controll on the network + if ($this->account->data->rating_atc <= 1) { + // event(new \App\Events\NotifyAccountEvent($this->account, 'error', 'ATC Booking', 'Failed to book because you are not eligable to book.')); + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.notEligable'), + ], + ], + 422 + ); + } + + // there is already a booking on this station + if(AtcSessionBooking::where('station_id', $validated['station']) + ->where( function($query) use ($startsAt, $endsAt) { + $query->where(function($query) use ($startsAt) { + $query->where('starts_at','<', $startsAt) + ->where('ends_at', '>', $startsAt); + })->orWhere(function($query) use ($endsAt){ + $query->where('starts_at','<', $endsAt) + ->where('ends_at', '>', $endsAt); + })->orWhere(function($query) use ($startsAt, $endsAt){ + $query->where('starts_at','>=', $startsAt) + ->where('ends_at', '<=', $endsAt); + }); + })->count() > 0){ + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.alreadyBooked'), + ], + ], + 422); + + } + + // the booking is way to far in the future + if ($now->diffInDays($startsAt, false) > 45){ + return response()->json(['errors' => [trans('booking.errors.toFarFuture'), ], ], 422); + } + + // All checks passed. + // Create and save the booking + $booking = new AtcSessionBooking(); + $booking->vatbook_id = 0; + $booking->station_id = $validated['station']; + $booking->controller_id = $this->account->id; + $booking->training = $validated['training'] ? true : false; + $booking->voice = $validated['voice'] ? true : false; + $booking->event = $validated['event'] ? true : false; + $booking->starts_at = $startsAt; + $booking->ends_at = $endsAt; + $booking->save(); + + + if ($this->sendInsertRequest($booking)) { + $this->account->notify(new \App\Notifications\Booking\AtcBookingCreatedNotification()); + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } else { + $this->account->notify(new \App\Notifications\Booking\AtcBookingCreatedNotification()); + return response()->json( + [ + 'success' => true, + 'errors' => [ + trans('booking.errors.failedVatbook.insert'), + ], + ], + 422 + ); + } + + } + } + + /** + * Evaluates and saves a given, modified booking + * + * @param Request $request [description] + * @param AtcSessionBooking $booking [description] + * @return [type] [description] + */ + public function edit(Request $request, AtcSessionBooking $booking) + { + if($request->ajax()) + { + // Validate the requested data + // If this fails a 422 response with validation errors will be send back + $validated = $request->validate([ + 'station' => 'required|exists:navigation_stations,id', + 'from' => 'required|date_format:"d.m.Y H:i"', + 'till' => 'required|date_format:"d.m.Y H:i"', + 'training' => 'boolean', + 'event' => 'boolean', + 'voice' => 'boolean', + ]); + + // Let's validate even further + $this->account->loadMissing('data'); + + $now = \Carbon\CarbonImmutable::now('UTC'); + + $startsAt = Carbon::createFromFormat('d.m.Y H:i', $validated['from'], 'UTC'); + $endsAt = Carbon::createFromFormat('d.m.Y H:i', $validated['till'], 'UTC'); + // Perform important checks the default validation can not handle + // Check that the end is after the start date + // and that the start is maximum 2 hours in the past + + // Accept booking only if the start is maximum 2 hours in the past. + if($now->diffInHours($startsAt, false) < -2) { + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframePast'), + ], + ], + 422 + ); + } + + // Accept booking only if the begin is prior to the end + if ($endsAt <= $startsAt) { + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframeSense'), + ], + ], + 422 + ); + } + + // Check for timeframe size + // Session must be at least 60 minutes long and must not exceed 24 hours + $bookingLength = $startsAt->diffInHours($endsAt); + if ($bookingLength < 1 || $bookingLength >= 24) { + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.timeframeLimits'), + ], + ], + 422 + ); + } + // The account must be eligable to controll on the network + if ($this->account->data->rating_atc <= 1) { + // event(new \App\Events\NotifyAccountEvent($this->account, 'error', 'ATC Booking', 'Failed to book because you are not eligable to book.')); + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.notEligable'), + ], + ], + 422 + ); + } + + // there is already a booking on this station + if(AtcSessionBooking::where('station_id', $validated['station']) + ->where('id', '!=', $booking->id) + ->where( function($query) use ($startsAt, $endsAt) { + $query->where(function($query) use ($startsAt) { + $query->where('starts_at','<', $startsAt) + ->where('ends_at', '>', $startsAt); + })->orWhere(function($query) use ($endsAt){ + $query->where('starts_at','<', $endsAt) + ->where('ends_at', '>', $endsAt); + })->orWhere(function($query) use ($startsAt, $endsAt){ + $query->where('starts_at','>=', $startsAt) + ->where('ends_at', '<=', $endsAt); + }); + })->count() > 0){ + + return response()->json( + [ + 'errors' => [ + trans('booking.errors.alreadyBooked') + ], + ], + 422); + + } + + // the booking is way to far in the future + if ($now->diffInDays($startsAt, false) > 45){ + return response()->json(['errors' => [trans('booking.errors.toFarFuture'), ], ], 422); + } + + + // All checks passed. + // Create and save the booking + //$booking->vatbook_id = 0; + $booking->station_id = $validated['station']; + $booking->training = $validated['training'] ? true : false; + $booking->voice = $validated['voice'] ? true : false; + $booking->event = $validated['event'] ? true : false; + $booking->starts_at = $startsAt; + $booking->ends_at = $endsAt; + $booking->save(); + + + if ($this->sendUpdateRequest($booking)) { + $this->account->notify(new \App\Notifications\Booking\AtcBookingUpdateNotification()); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } else { + $this->account->notify(new \App\Notifications\Booking\AtcBookingUpdateNotification()); + + return response()->json( + [ + 'success' => true, + 'errors' => [ + trans('booking.errors.failedVatbook.update'), + ], + ], + 422 + ); + } + + } + } + + /** + * Deletes a given booking + * + * @param Request $request [description] + * @param AtcSessionBooking $booking [description] + * @return [type] [description] + */ + public function delete(Request $request, AtcSessionBooking $booking) + { + if($request->ajax()) + { + if($booking->controller_id === $this->account->id) + { + + if ($booking->vatbook_id == 0) { + $booking->delete(); + return response()->json( ['success' => true, ], 200 ); + } + + if ($this->sendDeleteRequest($booking)) { + $booking->delete(); + + $this->account->notify(new \App\Notifications\Booking\AtcBookingDeletedNotification()); + + return response()->json( + [ + 'success' => true, + ], + 200 + ); + } else { + return response()->json( + [ + 'success' => false, + ], + 200 + ); + } + } else { + return response()->json( + [ + 'errors' => [ + trans('booking.errors.notController'), + ], + ], + 422 + ); + } + } + } +} diff --git a/app/Http/Controllers/Bookings/VatBookController.php b/app/Http/Controllers/Bookings/VatBookController.php new file mode 100644 index 0000000..267df47 --- /dev/null +++ b/app/Http/Controllers/Bookings/VatBookController.php @@ -0,0 +1,286 @@ + our own url + * Local_ID => our local ID of the booking + * EU_ID => vatbook booking id. Obviously only for update and delete requests. + * + * FOR TESTING WE CAN USE + * TEST=1 as parameter to prevent saving!!!!! + * + * BASED UPON: + * http://support.vroute.net/viewtopic.php?f=3&t=47 + */ + + /** + * VATBOOK Base URL. + * + * @var string + */ + protected $vatbookBaseUrl; + + /** + * Event Base URL. + */ + protected $eventBaseUrl; + + /** + * HTTP client used by this class. + * + * @var \GuzzleHttp\Client + */ + protected $client; + + /** + * Testing Flag + * Set to true for default to prevent saving to vatbook in case of errors. + * + * @var bool + */ + protected $testing = true; + + public function __construct() + { + parent::__construct(); + + // // See https://stackoverflow.com/questions/30959210/set-default-query-string-in-guzzle-6 for details + // // on this approach regarding the TEST parameter added to test bookings + // $handler = \GuzzleHttp\HandlerStack::create(); + + // // Add TEST parameter if we are only testing + // if ($this->testing) { + // $handler->push(\GuzzleHttp\Middleware::mapRequest(function (\Psr\Http\Message\RequestInterface $request) { + // return $request->withUri(\GuzzleHttp\Psr7\Uri::withQueryValue($request->getUri(), 'TEST', '1')); + // })); + // } + + // $this->client = new \GuzzleHttp\Client([ + // 'base_uri' => $this->vatbookBaseUrl, + // 'handler' => $handler, + // ]); + } + + /** + * Send VatBook Insert Request + * http://vatbook.euroutepro.com/atc/insert.php?Local_URL=&Local_ID=&b_day=&b_month=&b_year=&Controller=&Position=&sTime=&eTime=&T=&E=&voice=. + * + * Local_URL - as in common section + * Local_ID - as in common section + * b_day - day of month on which the ATC session will start + * b_month - month part of the start date + * b_year - year part of the start date (4 digits) + * Controller - booking user's full name + * cid - Controller VID (not required but recommended) + * Position - callsign on which the service will be provided (up to 11 chars) + * sTime - start time in UTC, 4 digits, no separator between hours and minutes + * eTime - end time in UTC, as above. If eTime < sTime, then it means booked session ends on the next day + * T - 0 for normal session, 1 for training (display applications show these differently) + * E - 0 for normal session, 1 if it is part of event. For events, additional parameter E_URL is required and it specifies a URL where the user will be redirected to + * voice - 0 for text, 1 for voice, 2 for unknown + * + * @param AtcSessionBooking $booking [description] + * + * @return [type] [description] + */ + public function sendInsertRequest(AtcSessionBooking $booking) + { + // Send request to VatBook + $res = $this->client->get($this->vatbookBaseUrl.'insert.asp', [ + 'query' => [ + 'Local_URL' => 'noredir', + 'Local_ID' => $booking->id, + 'b_day' => $booking->starts_at->format('d'), + 'b_month' => $booking->starts_at->format('m'), + 'b_year' => $booking->starts_at->format('Y'), + 'Controller' => \Illuminate\Support\Str::ascii($booking->controller->id), + 'cid' => $booking->controller->id, + 'Position' => $booking->station->ident, + 'sTime' => $booking->starts_at->format('Hi'), + 'eTime' => $booking->ends_at->format('Hi'), + 'T' => $booking->training ? 1 : 0, + 'E' => $booking->event ? 1 : 0, + 'E_URL' => $this->eventBaseUrl, + 'voice' => $booking->voice ? 1 : 0, + ], + ]); + if (200 == $res->getStatusCode()) { + $body = (string) $res->getBody(); // Cast the returned stream to a string we can work with + $body = trim($body); + + // We need to find the EU_ID... + $partials = explode(PHP_EOL, $body); + /* + * when an error occured!!! + * The returned data ALLWAYS IS AN ARRAY OF LENGTH 4 + */ + if (4 != count($partials)) { + return false; + } + + // Response Pattern is: + // array:4 [ + // 0 => "action=insert" + // 1 => "Local_ID=7" + // 2 => "EU_ID=384661917" + // 3 => "Event_ID=0" + // ] + // so take [2] and parse the id + $euid = explode('=', $partials[2])[1]; + + $booking->vatbook_id = $euid; + $booking->save(); + + return true; + } else { + $booking->delete(); // Unable to book!!!! + return false; + } + } + + /** + * Send VatBook Update request to + * http://vatbook.euroutepro.com/atc/update.php?Local_URL=&Local_ID=&EU_ID=&b_day=&b_month=&b_year=&Controller=&Position=&sTime=&eTime=&T=&E=&voice=. + * + * Local_URL - as in common section + * Local_ID - as in common section + * EU_ID - numeric ID of a booking previously made + * b_day - day of month on which the ATC session will start + * b_month - month part of the start date + * b_year - year part of the start date (4 digits) + * Controller - booking user's full name + * cid - Controller VID (not required but recommended) + * Position - callsign on which the service will be provided (up to 11 chars) + * sTime - start time in UTC, 4 digits, no separator between hours and minutes + * eTime - end time in UTC, as above. If eTime < sTime, then it means booked session ends on the next day + * T - 0 for normal session, 1 for training (display applications show these differently) + * E - 0 for normal session, 1 if it is part of event. For events, additional parameter E_URL is required and it specifies a URL where the user will be redirected to + * voice - 0 for text, 1 for voice, 2 for unknown + * + * @param AtcSessionBooking $booking [description] + * + * @return [type] [description] + */ + public function sendUpdateRequest(AtcSessionBooking $booking) + { + $res = $this->client->get($this->vatbookBaseUrl.'update.asp', [ + 'query' => [ + 'Local_URL' => 'noredir', + 'Local_ID' => $booking->id, + 'EU_ID' => $booking->vatbook_id, + 'b_day' => $booking->starts_at->format('d'), + 'b_month' => $booking->starts_at->format('m'), + 'b_year' => $booking->starts_at->format('Y'), + 'Controller' => \Illuminate\Support\Str::ascii($booking->controller->id), + 'cid' => $booking->controller->id, + 'Position' => $booking->station->ident, + 'sTime' => $booking->starts_at->format('Hi'), + 'eTime' => $booking->ends_at->format('Hi'), + 'T' => $booking->training ? 1 : 0, + 'E' => $booking->event ? 1 : 0, + 'E_URL' => $this->eventBaseUrl, + 'voice' => $booking->voice ? 1 : 0, + ], + ]); + + if (200 == $res->getStatusCode()) { + $body = (string) $res->getBody(); // Cast the returned stream to a string we can work with + $body = trim($body); + + // We need to find the EU_ID... + $partials = explode(PHP_EOL, $body); + + /* + * when an error occured!!! + * The returned data ALWAYS IS AN ARRAY OF LENGTH 4 + */ + if (4 != count($partials)) { + return false; + } + + // Response Pattern is: + // array:4 [ + // 0 => "action=insert" + // 1 => "Local_ID=7" + // 2 => "EU_ID=384661917" + // 3 => "Event_ID=0" + // ] + // so take [2] and parse the id + $euid = explode('=', $partials[2])[1]; + + $booking->vatbook_id = $euid; + $booking->save(); + + return true; + } else { + return false; + } + } + + /** + * Send VatBook Delete Request + * http://vatbook.euroutepro.com/atc/delete.php?Local_URL=noredir&EU_ID=573682004&Local_ID=112. + * + * Local_URL - as in common section + * Local_ID - as in common section + * EU_ID - as in common section + * + * @param AtcSessionBooking $booking [description] + * + * @return [type] [description] + */ + public function sendDeleteRequest(AtcSessionBooking $booking) + { + $res = $this->client->get($this->vatbookBaseUrl.'delete.asp', [ + 'query' => [ + 'Local_URL' => 'noredir', + 'Local_ID' => $booking->id, + 'EU_ID' => $booking->vatbook_id, + ], + ]); + + // $url = $this->vatbookBaseUrl.'delete.php?Local_URL=noredir&EU_ID='.$booking->vatbook_id.'&Local_ID='.$booking->id; + // $res = $client->get($url); + + if (200 == $res->getStatusCode()) { + $body = (string) $res->getBody(); // Cast the returned stream to a string we can work with + $body = trim($body); + + // We need to find the EU_ID... + $partials = explode(PHP_EOL, $body); + + /* + * when an error occured!!! + * The returned data ALLWAYS IS AN ARRAY OF LENGTH 4 + */ + if (4 != count($partials)) { + return false; + } + + // Response Pattern is: + // array:4 [ + // 0 => "action=insert" + // 1 => "Local_ID=7" + // 2 => "EU_ID=384661917" + // 3 => "Event_ID=0" + // ] + // so take [1] and parse the id + $lclid = explode('=', $partials[1])[1]; + if ($lclid == $booking->id) { + return true; + } + + return false; + } else { + return false; + } + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..9f64fc0 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,53 @@ +middleware( + function ($request, $next) { + // Check if we have a user signed in + $this->account = new Account(); //Make sure there always is a "User"-Object set + if (Auth::check() || Auth::guard('web')->check()) { + $this->account = Auth::user(); + } + + return $next($request); + } + ); + } + + /** + * Craft application views. + * + * @param [type] $view [description] + * + * @return [type] [description] + */ + protected function viewMake($view) + { + $view = View::make($view); + $view->with('_account', $this->account); + + return $view; + } +} diff --git a/app/Http/Controllers/Controllers/ControllerController.php b/app/Http/Controllers/Controllers/ControllerController.php new file mode 100644 index 0000000..9989752 --- /dev/null +++ b/app/Http/Controllers/Controllers/ControllerController.php @@ -0,0 +1,21 @@ +viewMake('controller.index'); + } + +} diff --git a/app/Http/Controllers/Controllers/TrainingController.php b/app/Http/Controllers/Controllers/TrainingController.php new file mode 100644 index 0000000..1e53f06 --- /dev/null +++ b/app/Http/Controllers/Controllers/TrainingController.php @@ -0,0 +1,21 @@ +viewMake('frontend.dataprotection.gdpr') + ->with('gdpr', $gdpr); + } + + public function imprint() + { + $imprint = \Illuminate\Support\Facades\Storage::get('static/imprint.html'); + return $this->viewMake('frontend.dataprotection.imprint') + ->with('imprint', $imprint); + } + +} diff --git a/app/Http/Controllers/Events/EventRouteController.php b/app/Http/Controllers/Events/EventRouteController.php new file mode 100644 index 0000000..a76562a --- /dev/null +++ b/app/Http/Controllers/Events/EventRouteController.php @@ -0,0 +1,43 @@ +ajax()) abort(403); + return EventRoute::withCount('legs')->get(); + } + + public function getRouteLegs(Request $request, EventRoute $route){ + if(!$request->ajax()) abort(403); + $legs = $route->legs()->with('departureAerodrome','arrivalAerodrome')->get(); + return $legs; + } + + public function signupEventRoute(Request $request, EventRoute $route){ + if(!$request->ajax()) abort(403); + foreach ($route->legs as $leg){ + $leg->accounts()->detach($this->account); + $leg->accounts()->attach($this->account); + } + } + + public function signoutEventRoute(Request $request, EventRoute $route){ + if(!$request->ajax()) abort(403); + foreach ($route->legs as $leg){ + $leg->accounts()->detach($this->account); + } + } + +} diff --git a/app/Http/Controllers/Forum/ForumController.php b/app/Http/Controllers/Forum/ForumController.php new file mode 100644 index 0000000..0b77241 --- /dev/null +++ b/app/Http/Controllers/Forum/ForumController.php @@ -0,0 +1,109 @@ + [ + 'pattern' => '/\[color\=(.*?)\](.*?)\[\/color]/s', + 'replace' => '$2', + 'content' => '$2', + ], + 'center' => [ + 'pattern' => '/\[center\](.*?)\[\/center]/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'size' => [ + 'pattern' => '/\[size\=(.*?)\](.*?)\[\/size]/s', + 'replace' => '$2', + 'content' => '$2', + ], + 'table' => [ + 'pattern' => '/\[table\]\n?(.*?)\[\/table\]\n?/s', + 'replace' => '$1
', + 'content' => '$1', + ], + 'table-row' => [ + 'pattern' => '/\[tr\]\n?(.*?)\[\/tr\]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'table-data' => [ + 'pattern' => '/\[td\]\n?(.*?)\[\/td\]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'table-head' => [ + 'pattern' => '/\[th\]\n?(.*?)\[\/th]\n?/s', + 'replace' => '$1', + 'content' => '$1', + ], + 'link' => [ + 'pattern' => '/\[url\](.*?)\[\/url\]/s', + 'replace' => '$1', + 'content' => '$1' + ], + 'namedlink' => [ + 'pattern' => '/\[url\=\'?(.*?)\'?\](.*?)\[\/url\]/s', + 'replace' => '$2', + 'content' => '$2' + ], + 'attach' => [ + 'pattern' => '/\[ATTACH type\=\"(.*?)\"\](.*?)\[\/ATTACH\]/s', + 'replace' => '', + 'content' => '', + ], + ]; + + function __construct() + { + parent::__construct(); + } + + public static function sortByDateStamp($a, $b) + { + if($a->post_date == $b->post_date) return 0; + return $a->post_date < $b->post_date ? -1 : 1; + } + + public function getNews(Request $request) + { + + $threads = \App\Libraries\XenBridge::getNewsThreads(); + if($threads == false) abort(503, 'forum unavailable'); + + $newsThreads = $threads->sticky; + + $ordered = usort($newsThreads, array("self", "sortByDateStamp")); + + if($ordered) { + $newsThreads = array_reverse($newsThreads); + $bbCode = new \Genert\BBCode\BBCode(); + $bbCode->addLinebreakParser(); + foreach ($this->customParsers as $name => $parser) { + $bbCode->addParser( + $name, + $parser['pattern'], + $parser['replace'], + $parser['content'], + ); + } + foreach ($newsThreads as $nt) { + // Get all the firsts posts from the threads + $nt->post = \App\Libraries\XenBridge::getPost($nt->first_post_id)->post; + $nt->post->parsedMessage = $bbCode->convertToHtml($nt->post->message, \Genert\BBCode\BBCode::CASE_SENSITIVE); + $nt->post->parsedMessage = $bbCode->stripBBCodeTags($nt->post->parsedMessage); + } + return $newsThreads; + } + } +} diff --git a/app/Http/Controllers/Landingpage/LandingpageController.php b/app/Http/Controllers/Landingpage/LandingpageController.php new file mode 100644 index 0000000..8bf06ce --- /dev/null +++ b/app/Http/Controllers/Landingpage/LandingpageController.php @@ -0,0 +1,45 @@ +with('_account', $this->account); + $view->with('partners', Partner::all()); + return $view; + } + + /** + * The partners page + * @param Request $request [description] + * @return \View [description] + */ + public function partners(Request $request) + { + $view = View::make('frontend.landingpage.partners'); + $view->with('_account', $this->account); + $view->with('partners', Partner::all()); + return $view; + } + + +} diff --git a/app/Http/Controllers/Membership/MembershipController.php b/app/Http/Controllers/Membership/MembershipController.php new file mode 100644 index 0000000..cc48c91 --- /dev/null +++ b/app/Http/Controllers/Membership/MembershipController.php @@ -0,0 +1,419 @@ +viewMake('membership.dashboard.index'); + } + + /** + * Display registration / initial login page + * + * @return View + */ + public function setup() + { + $account = Auth::user(); + if($account->setup_completed){ + return redirect()->route('membership.home'); + } + return $this->viewMake('membership.dashboard.setup'); + } + + /** + * This is displayed to banned users + * + * @return [type] [description] + */ + public function banned() + { + $account = Auth::user(); + if(! $account->isCurrentlyBanned){ + return redirect()->route('membership.home'); + } + return $this->viewMake('membership.dashboard.banned'); + } + + + /** + * This is displayed to inactive users + * + * @return [type] [description] + */ + public function inactive() + { + $account = Auth::user(); + if(!$account->isInactive){ + return redirect()->route('membership.home'); + } + return $this->viewMake('membership.dashboard.inactive'); + } + + /** + * API called endpoint to update Account information + * + * @param Request $request [description] + * @return [type] [description] + */ + public function update(Request $request) + { + $account = Auth::user(); + $account->loadMissing('setting'); + + $validated = $request->validate( + [ + 'gdpr' => 'required|boolean', + 'settings' => 'boolean', + 'backupPassword' => ['confirmed', new \App\Rules\StrongPasswordRule], + // 'backupPassword_confirmation' => 'required_with:backupPassword', + 'language' => 'required' + ] + ); + + // If the request is not made from the settings component + if(!isset($validated['settings']) && !$account->setup_completed) { + $account->setup_completed = $validated['gdpr']; + } + + // Update the backup password if set + if(!empty($validated['backupPassword'])){ + $account->password = Hash::make($validated['backupPassword']); + $account->notify(new \App\Notifications\Membership\PasswordUpdatedNotification()); + } + + // Set the prefered language + $account->setting->language = $validated['language']; + $account->setting->save(); + + $account->save(); + + + return $account->setup_completed; + + } + + /** + * Get the notifications for the authenticated user + * @param Request $request [description] + * @return [type] [description] + */ + public function notifications(Request $request) { + if($request->ajax()) { + return Auth::user()->notifications; + } else { + abort(403); + } + } + + /** + * Marks a given notification as read + * @param Request $request [description] + * @return [type] [description] + */ + public function markNotificationAsRead(Request $request) + { + if($request->ajax()) { + + $validated = $request->validate([ + 'notification' => 'required|exists:notifications,id', + ]); + + $account = Auth::user(); + $notification = \Illuminate\Notifications\DatabaseNotification::find($validated['notification']); + if(!is_null($notification) && $account->id === $notification->notifiable->id) { + $notification->markAsRead(); + return $notification; + } else { + return false; + } + } else { + abort(403); + } + } + + /** + * Does the account has a forum account + * + * @param Request $request [description] + * @return boolean [description] + */ + public function hasForumAccount(Request $request) + { + if($request->ajax()) { + $this->account->loadMissing('setting'); + + return $this->account->setting->forum_id != null ? true : false; + } + } + + /** + * Get the forum username for an account. + * + * @param Request $request + * + * @return String The username + */ + public function getForumAccountName(Request $request) + { + if($request->ajax()) { + $this->account->loadMissing('setting'); + + return \App\Libraries\XenBridge::getForumUsername($this->account); + } + } + + /** + * Create a new forum access for a user + * + * @param Request $request [description] + * @return [type] [description] + */ + public function createForumAccount(Request $request) + { + if($request->ajax()) { + $validated = $request->validate([ + 'password' => 'required|confirmed', + ]); + + if($this->account->setting->forum_id == null) { + $created = \App\Libraries\XenBridge::createForumAccount($this->account, $validated['password'], 0); + if($created) { + activity() + ->causedBy($this->account) + ->performedOn($this->account) + ->log('Forenaccount wurde erstellt!'); + } + } + } + return false; + } + + /** + * Get all active TeamSpeak Identities the account has + * + * @param Request $request [description] + * @return [type] [description] + */ + public function getTeamspeakIdentities(Request $request) + { + if($request->ajax()) { + $this->account->loadMissing('teamspeakRegistrations'); + + return $this->account->teamspeakRegistrations; + } + abort(403); + } + + /** + * Start the automatic teamspeak registration process + * + * @param Request $request [description] + * @return [type] [description] + */ + public function createTeamspeakIdentity(Request $request) + { + /*if(!$request->ajax()) abort(403); + + // Check if the 10 identities limit has already been reached + $this->account->loadMissing('teamspeakRegistrations'); + if(count($this->account->teamspeakRegistrations) >= 10) { + return response()->json([ + 'success' => false, + ]); + } + + // Go ahead and try to create a new registration + $registration = null; + if($this->account->new_teamspeak_registration) { + $registration = $this->account->new_teamspeak_registration->load('confirmation'); + } else { + $ipAddress = $request->ip(); + $registration = $this->createTeamSpeakRegistration($this->account, $ipAddress); + } + // Is the registration confirmed? + $confirmation = null; + if (!$registration->confirmation) { + $confirmation = $this->createTeamSpeakConfirmation( + $registration->id, + md5($registration->created_at->timestamp), + $this->account->id + ); + } else { + $confirmation = $registration->confirmation; + } + + $tsURL = 'ts3server://' . config('teamspeak.host') . '?nickname=' . $this->account->firstname . '%20'.$this->account->lastname . '&token=' . $confirmation->privilege_key; + + activity() + ->causedBy($this->account) + ->performedOn($this->account) + ->log('Automatische TS Registrierung abgeschlossen!'); + + return response()->json( + [ + 'success' => true, + 'tslink' => $tsURL, + ] + );*/ + } + + /** + * Manually create / set a ts id for an account + * + * @param Request $request [description] + */ + public function setTeamspeakIdentity(Request $request) + { + if(!$request->ajax()) abort(403); + + $validated = $request->validate([ + 'tsId' => 'required|string', + ]); + + + $this->account->loadMissing('teamspeakRegistrations'); + if(count($this->account->teamspeakRegistrations) >= 10) { + return response()->json([ + 'success' => false, + 'message' => 'Too many TSIDs.', + ]); + } + + if(\App\Models\TeamSpeak\Registration::where('uid', $validated['tsId'])->exists()){ + return response()->json([ 'success' => false, 'message' => 'UID already assined.',]); + } + + $result =\App\Libraries\TeamSpeakWebquery::registerViaUid($this->account, $request->ip(), $validated['tsId']); + + if ($result == false) return response()->json([ 'success' => false, 'message' => 'Unable to register.',]); + + $this->account->notify(new \App\Notifications\Membership\TeamSpeakIdentityCreated($validated['tsId'])); + + activity() + ->causedBy($this->account) + ->performedOn($this->account) + ->log('TS Identity eingetragen: ' . $validated['tsId'] . '!'); + + return response()->json( + [ + 'success' => true, + 'message' => $validated['tsId'].' successfully associated with your account.' + ] + ); + } + + /** + * Create a new TeamSpeak Registration. + * + * @param Account $account [description] + * @param [type] $ip [description] + * @param bool $shouldSave [description] + * + * @return [type] [description] + */ + protected function createTeamSpeakRegistration(Account $account, $ip, $shouldSave = true) + { + /*if ($account) { + $reg = new \App\Models\TeamSpeak\Registration(); + $reg->account_id = $account->id; + $reg->registration_ip = $ip; + if ($shouldSave) { + $reg->save(); + } + + return $reg; + + $this->account->notify(new \App\Notifications\Membership\TeamSpeakRegistrationCreated($ip)); + } + + return false;*/ + } + + /** + * Create a new confirmation for a new registration. + * + * @param [type] $regId [description] + * @param [type] $conString [description] + * @param [type] $accId [description] + * + * @return [type] [description] + */ + protected function createTeamSpeakConfirmation($regId, $conString, $accId) + { + /*$key_desc = 'CID:'.$accId.' RegID:'.$regId; + $key_custom = 'ident=registration_id value='.$regId; + $conf = new \App\Models\TeamSpeak\Confirmation(); + $conf->registration_id = $regId; + $_ts = new \App\Libraries\TeamSpeak('VATGER Registration'); + $conf->privilege_key = $_ts->getInstance() + ->serverGroupGetByName(config('teamspeak.default_group')) + ->privilegeKeyCreate($key_desc, $key_custom); + $conf->save(); + + return $conf;*/ + } + + /** + * Delete a teamspeak registration and also it's confirmation + * + * @param Request $request [description] + * @param [type] $tsreg [description] + * @return [type] [description] + */ + public function removeTeamspeakIdentity(Request $request, $tsreg) + { + if(!$request->ajax()) abort(403); + + $registration = \App\Models\TeamSpeak\Registration::findOrFail($tsreg); + $uid = $registration->uid; + + $removed = \App\Libraries\TeamSpeakWebquery::removeTSRegistation($registration); + + if($removed) { + $this->account->notify(new \App\Notifications\Membership\TeamSpeakIdentityDeleted($uid)); + + activity() + ->causedBy($this->account) + ->performedOn($this->account) + ->log('TS Identity ausgetragen! UID: ' . $uid . '!'); + + return response()->json(true); + } + + return response()->json(false); + } + + + public function getProfileData(Request $request) + { + if(!$request->ajax()) abort(403); + $data = [ + 'cid' => $this->account->id, + 'hp_email' => $this->account->email, + 'forum_email' => \App\Libraries\XenBridge::getForumEmail($this->account) + ]; + + return response()->json($data); + } + +} diff --git a/app/Http/Controllers/Membership/SurveyKeysController.php b/app/Http/Controllers/Membership/SurveyKeysController.php new file mode 100644 index 0000000..0f18568 --- /dev/null +++ b/app/Http/Controllers/Membership/SurveyKeysController.php @@ -0,0 +1,22 @@ +ajax()) abort(403); + $id = Auth::user()->id; + $keys = SurveyKey::query()->where('account_id', $id)->orderBy('id', 'DESC')->get(); + + return response()->json($keys); + } + +} diff --git a/app/Http/Controllers/Navigation/AerodromeController.php b/app/Http/Controllers/Navigation/AerodromeController.php new file mode 100644 index 0000000..e563eac --- /dev/null +++ b/app/Http/Controllers/Navigation/AerodromeController.php @@ -0,0 +1,156 @@ +ajax()) + { + if($local) + return Aerodrome::isDe()->with('stations')->get(); + else + return Aerodrome::with('stations')->get(); + } + } + + /** + * Get a detailed view of a aerodrome + * + * @param Request $request [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function getAerodrome(Request $request, $icao) + { + if($request->ajax()) + { + return Aerodrome::icao($icao)->with('stations', 'charts', 'countryDetail', 'runways', 'navaids')->first(); + } + } + + /** + * Get the current standstatus of an aerodrome + * + * @param Request $request [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function getStandStatus(Request $request, $icao) + { + if($request->ajax()) { + + $aerodrome = Aerodrome::icao($icao)->first(); + + if($aerodrome === null) return []; + + $standFilePath = storage_path('app') . '/navigation/stands/' .strtolower($aerodrome->icao) . '.csv'; + + if(File::exists($standFilePath)) { + $stands = (new StandStatus($aerodrome->icao, $standFilePath, $aerodrome->latitude, $aerodrome->longitude, false, null)) + ->setMaxAircraftAltitude($aerodrome->elevation + 300) + ->setMaxStandDistance(0.02) + ->setMaxDistanceFromAirport(5) + ->parseData(); + if($stands) { + $standsArray = []; + foreach ($stands->allStands() as $s) { + $standsArray[] = $s; + } + return $standsArray; + } + } else { + return []; + } + } + abort(403); + } + + public function getControllerActivity(Request $request, $icao) + { + if($request->ajax()) { + + $aerodrome = Aerodrome::icao($icao)->first(); + + return $aerodrome->controllerActivity; + } + abort(403); + } + + public function getPilotActivity(Request $request, $icao) + { + if($request->ajax()) { + + return PilotClient::online()->withinAirport($icao)->get();; + } + abort(403); + } + + public function getAirports(Request $request, $nonjson = false) + { + if($nonjson) { + return \Illuminate\Support\Facades\Cache::remember( + 'navigation.airport.livemap.nonjson', + 7 * 24 * 60 * 60, + function () { + $airportsNonJson = []; + $airports = \App\Models\Navigation\Aerodrome::where('civilian', true)->get(); + foreach ($airports as $airport) { + $airportsNonJson[] = [ + 'icao' => $airport->icao, + 'name' => $airport->icao . ' (' . $airport->name . ')', + 'lat' => floatval($airport->latitude), + 'lng' => floatval($airport->longitude), + ]; + } + return json_encode($airportsNonJson); + } + ); + } + + return \Illuminate\Support\Facades\Cache::remember( + 'navigation.airports.livemap', + 7 * 24 * 60 * 60, + function () { + $airportsJson = [ + 'type' => 'FeatureCollection', + 'features' => [], + ]; + // $airports = \App\Models\Navigation\Aerodrome::all(); + // $airports = \App\Models\Navigation\Aerodrome::isDe()->get(); + // $airports = \App\Models\Navigation\Aerodrome::where('major', true)->get(); + $airports = \App\Models\Navigation\Aerodrome::where('civilian', true)->get(); + foreach ($airports as $airport) { + $airportsJson['features'][] = [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'Point', + 'coordinates' => [floatval($airport->longitude), floatval($airport->latitude)], + ], + 'properties' => [ + 'name' => $airport->icao . ' (' . $airport->name . ')', + ], + ]; + } + return $airportsJson; + } + ); + } + +} diff --git a/app/Http/Controllers/Navigation/SectordataController.php b/app/Http/Controllers/Navigation/SectordataController.php new file mode 100644 index 0000000..2aad733 --- /dev/null +++ b/app/Http/Controllers/Navigation/SectordataController.php @@ -0,0 +1,267 @@ + [ + 'LJLA', 'LDZO', 'LQSB', 'LAAA', 'LWSS', 'LYBA', + ], + 'FTW_51_CTR' => [ + 'KZFW', + ], + 'ATL_43_CTR' => [ + 'KZTL', + ], + 'MTL_CTR' => [ + 'CZUL', + ], + 'BOS_CTR' => [ + 'KZBW', + ], + 'TOR_PI_CTR' => [ + 'CZYZ', + ], + 'CLE_CTR' => [ + 'KZOB', + ], + 'CHI_35_CTR' => [ + 'KZAU', + ], + 'MIA_46_CTR' => [ + 'KZMA', + ], + 'JAX_35_CTR' => [ + 'KZJX', + ], + 'IND_CTR' => [ + 'KZID', + ], + 'NY_CTR' => [ + 'KZNY', + ], + 'ASIA_W_FSS' => [ + 'OAKX', 'OPLR', 'VIDF', 'VNSM', 'VEGF', 'OPKR', 'VABF', 'VRMF', 'VCCF', 'VOMF', 'VECD', 'VGFR', + ], + 'ML-SNO_CTR' => [ + 'YSNO', 'YWON', 'YYWE', 'YEKW', 'YHUM', + ], + 'GUM_CTR' => [ + 'PGZU', + ], + 'LON_CTR' => [ + 'EGTT-N', 'EGTT-C', 'EGTT-S', 'EGTT-W', + ], + 'LON_SC_CTR' => [ + 'EGTT-S', 'EGTT-C', + ], + 'SCO_CTR' => [ + 'EGPX-E', 'EGPX-W', + ], + ]; + + protected $boundaryFile = 'navigation/sectors/fir_boundaries.json'; + + protected $airportsDataSource = 'navigation/sectors/Airports.dat'; + + protected $uirFile = 'navigation/sectors/UIR.dat'; + + protected $boundaries = null; + + protected $uirs = []; + + protected $vaccPositions = null; + + protected $connectedClients = null; + + protected $connectedATC = []; + + protected $weekLength = 7 * 24 * 60 * 60; + + protected $vaccGerAtcHandles = [ + 'ED', 'ETA', 'ETH', 'ETI', 'ETM', 'ETN', 'ETS', + ]; + + public function __construct() + { + parent::__construct(); + + $this->boundaries = json_decode(Storage::get($this->boundaryFile)); + + $this->vaccPositions = $this->boundaries->vatger->positions; + + // Load UIR Data + $uirFileContent = Storage::get($this->uirFile); + foreach (explode("\n", $uirFileContent) as $line) { + // ADR_E|Adria Radar|LYBA,LWSS,LAAA + $split = explode('|', $line); + $this->uirs[$split[0]] = $split[2]; + } + + if (Cache::has('network.data.connectedClients')) { + $this->connectedClients = Cache::get('network.data.connectedClients'); + } + + if (null != $this->connectedClients) { + foreach ($this->connectedClients->controllers as $controller) { + // Active ATC found.. let's find out if it is germany or not + foreach ($this->vaccPositions as $id => $pos) { + if ($pos->name == $controller->callsign) { + $this->connectedATC[$id] = $controller; + } + } + } + } + } + + /** + * Get a single sector boundary matching the callsing. + * + * @param [type] $callsign [description] + * + * @return [type] [description] + */ + public function getSector($callsign = null) + { + if (null == $callsign) { + return json_encode([]); + } + + if (!$this->_isEseSector($callsign)) { + return $this->_getSimpleSector($callsign); + } else { + return $this->_getEseSector($callsign); + } + } + + /** + * Load sector information from simple data. + * + * This is for any non vatger sector + * + * @param [type] $callsign [description] + * + * @return [type] [description] + */ + private function _getSimpleSector($callsign) + { + if (array_key_exists($callsign, $this->aliases)) { + $response['multiple'] = true; + $boundryLines = []; + foreach ($this->boundaries->general as $key => $fir) { + foreach ($this->aliases[$callsign] as $subsector) { + if ($fir->icao == $subsector) { + $boundryLines[] = $fir->points; + } + } + } + $response['points'] = $boundryLines; + + return json_encode($response); + } elseif (array_key_exists(explode('_', $callsign)[0], $this->uirs)) { + $response['multiple'] = true; + $sectors = []; + + $firs = explode(',', $this->uirs[explode('_', $callsign)[0]]); + foreach ($firs as $fir) { + foreach ($this->boundaries->general as $k => $f) { + if ($f->icao == $fir) { + $sectors[] = $f->points; + } + } + } + + $response['points'] = $sectors; + + return json_encode($response); + } else { + $possibleCandidates = []; + $possibleCandidates[] = $callsign; + $possibleCandidates[] = explode('_', $callsign)[0].'-'.explode('_', $callsign)[1]; + $possibleCandidates[] = explode('_', $callsign)[0]; + foreach ($this->boundaries->general as $k => $fir) { + foreach ($possibleCandidates as $pc) { + if ($fir->icao == $pc) { + $response['multiple'] = false; + $response['points'] = $fir->points; + + return $response; + } + } + } + } + + return json_encode(['multiple' => false, 'points' => []]); + } + + /** + * Get sector data from the ese section + * then make some sense of it and generate sector boundary. + * + * @param [type] $callsign [description] + * + * @return [type] [description] + */ + private function _getEseSector($callsign) + { + foreach ($this->vaccPositions as $ident => $position) { + if ($position->name == $callsign) { + // We have a matching position. Now find all sectors that are somehow interesting + $sectors = []; + + foreach ($this->boundaries->vatger->airspace as $airspace => $definition) { + if (!property_exists($definition, 'owners')) { + continue; + } + if (intval($definition->lowerLimit) < 5000 || intval($definition->upperLimit < 10500)) { + continue; + } + // If the airspace owners contains the position... add it + foreach ($definition->owners as $id => $cs) { + if ($cs == $ident) { + // Find the corresponding sector boundary in the sectors section + foreach ($this->boundaries->sectors as $sec => $coords) { + if ($airspace == $sec) { + $sectors[] = $coords; + } + } + } + } + } + + $response['multiple'] = true; + $response['points'] = $sectors; + + return json_encode($response); + } + } + } + + /** + * Is the callsign within the vacc definitions. + * + * @param [type] $callsign [description] + * + * @return bool [description] + */ + private function _isEseSector($callsign) + { + if (!in_array(substr($callsign, 0, 2), $this->vaccGerAtcHandles) && !in_array(substr($callsign, 0, 3), $this->vaccGerAtcHandles)) { + return false; + } + foreach ($this->vaccPositions as $id => $pos) { + if ($pos->name == $callsign) { + return true; + } + } + + return false; + } +} diff --git a/app/Http/Controllers/Navigation/StationController.php b/app/Http/Controllers/Navigation/StationController.php new file mode 100644 index 0000000..dd1c6af --- /dev/null +++ b/app/Http/Controllers/Navigation/StationController.php @@ -0,0 +1,22 @@ +ajax()) + { + if($bookable) + return \App\Models\Navigation\Station::bookable()->orderBy('ident', 'ASC')->get(); + else + return \App\Models\Navigation\Station::orderBy('ident', 'ASC')->get(); + } + } + +} diff --git a/app/Http/Controllers/Network/DatafeedApiController.php b/app/Http/Controllers/Network/DatafeedApiController.php new file mode 100644 index 0000000..6bd4017 --- /dev/null +++ b/app/Http/Controllers/Network/DatafeedApiController.php @@ -0,0 +1,109 @@ +ajax()) { + + if($dashboard && Auth::check()) { + return \App\Models\Network\AtcClient::online()->isDe()->orderBy('connected_at', 'ASC')->with('account')->get(); + } else { + return \App\Models\Network\AtcClient::online()->isDe()->orderBy('connected_at', 'ASC')->get(); + } + + } + abort(403); + } + + public function getOnlineAtc(Request $request) + { + if($request->ajax()) { + return \App\Models\Network\AtcClient::online()->orderBy('connected_at', 'ASC')->get(); + } + abort(403); + } + + public function getActiveFlights(Request $request) + { + if($request->ajax()){ + return \App\Models\Network\PilotClient::online()->orderBy('connected_at', 'ASC')->get(); + } + abort(403); + } + + public function getConnectedClients(Request $request) + { + // $connectedClients = Cache at 'network.data.connectedClients' + $connectedClients = \Illuminate\Support\Facades\Cache::get('network.data.connectedClients'); + + return json_encode($connectedClients, JSON_PRETTY_PRINT); + } + + + public function getWeather(Request $request, $icao) + { + /** + * We might have multiple records grab + */ + if(strlen($icao) > 4) { + $icao = str_replace(' ', '', $icao); // Get rid of any whitespace + $icaoSplit = explode(',', $icao); + $metars = []; + foreach ($icaoSplit as $i) { + if(strlen($i) == 4) + $metars[$i] = $this->getWeather($request, $i); + } + return $metars; + } + + /** + * We only have a 4 char long icao here ?! + */ + $icaoCode = strtoupper($icao); + if(preg_match('/^[A-Z]{4}$/', $icaoCode) == 1) { + // we have an uppercase 4 char long icao... + $metar = \Illuminate\Support\Facades\Cache::remember( + 'network.metar.'.$icaoCode, + 5 * 60, + function () use ($icaoCode) { + $client = new \GuzzleHttp\Client([ + 'base_uri' => 'https://metar.vatsim.net/', + ]); + + try { + $response = $client->get( + 'metar.php', + [ + 'query' => [ + 'id' => $icaoCode + ], + ] + ); + if($response->getStatusCode() == 200) { + if(0 === strpos((string) $response->getBody(), $icaoCode)) { + return (string) $response->getBody(); + } else { + return null; + } + } + } catch (\GuzzleHttp\Exception\TransferException $e) { + } + return null; + } + ); + return $metar ?? 'Currently the automated weather system is offline.'; + } else { + return 'No weather available for '.$icaoCode; + } + } + +} diff --git a/app/Http/Controllers/Pilots/PilotController.php b/app/Http/Controllers/Pilots/PilotController.php new file mode 100644 index 0000000..875156d --- /dev/null +++ b/app/Http/Controllers/Pilots/PilotController.php @@ -0,0 +1,21 @@ +viewMake('pilots.index'); + } + +} diff --git a/app/Http/Controllers/Regionalgroup/RegionalgroupController.php b/app/Http/Controllers/Regionalgroup/RegionalgroupController.php new file mode 100644 index 0000000..ba9b0bc --- /dev/null +++ b/app/Http/Controllers/Regionalgroup/RegionalgroupController.php @@ -0,0 +1,43 @@ +ajax()){ + + $regionalgroups = $this->account->regionalgroups; + foreach ($regionalgroups as $rg ) { + $rg->setAppends([]); + } + return $regionalgroups; + + } + abort(403); + } + + public function news(Request $request, Regionalgroup $regionalgroup) + { + if($request->ajax()) { + if($this->account->isMemberOfRegionalgroup($regionalgroup) || $this->account->isGuestOfRegionalgroup($regionalgroup)) { + // Grab the news + } + } + abort(403); + } + +} diff --git a/app/Http/Controllers/Regionalgroup/RequestController.php b/app/Http/Controllers/Regionalgroup/RequestController.php new file mode 100644 index 0000000..26fae34 --- /dev/null +++ b/app/Http/Controllers/Regionalgroup/RequestController.php @@ -0,0 +1,149 @@ +ajax()) { + $requests = $this->account->regionalgroupRequests()->with('regionalgroup:id,name')->get(); + foreach ($requests as $req ) { + $req->regionalgroup->setAppends([]); + } + return $requests; + } + abort(403); + } + + public function create(Request $request) + { + if($request->ajax()) { + $validated = $request->validate([ + 'newRequest.regionalgroup' => 'required|exists:regionalgroups_regionalgroups,id', + 'newRequest.reason' => 'required|string|min:25|max:150', + 'newRequest.topic' => 'required|in:join,change,leave', + 'newRequest.type' => 'required|in:member,guest,none', + // 'newRequest.as' => 'required|in:controller,pilot,both', + //'newRequest.destination' => 'nullable|required_if:newRequest.topic,change|exists:regionalgroups_regionalgroups,id', + ]); + + if(RegionalgroupRequest::where('account_id', $this->account->id)->where('regionalgroup_id', $validated['newRequest']['regionalgroup'])->exists()) { + //already has one request to the rg + $this->account->notify(new RegionalgroupRequestExistsNotification); + abort(422, trans('regionalgroup.request.errors.already_request_to_rg')); + } + if($validated['newRequest']['type'] == 'member' && $this->account->regionalgroups()->wherePivot('guest', false)->exists()){ + //is fullmember already otherwhere + abort(422, trans('regionalgroup.request.errors.already_fullmember_otherwhere')); + } + + if($validated['newRequest']['type'] == 'member' && RegionalgroupRequest::where('account_id', $this->account->id)->where('type','member')->exists()){ + //wants fullmember already otherwhere + abort(422, trans('regionalgroup.request.errors.already_request_fullmember_otherwhere')); + } + + $rg = Regionalgroup::query()->find($validated['newRequest']['regionalgroup']); + + if (!($validated['newRequest']['topic'] == 'change' || $validated['newRequest']['topic'] == 'leave') && ($this->account->isMemberOfRegionalgroup($rg) || $this->account->isGuestOfRegionalgroup($rg))) { + // is already part of this rg + abort(422, trans('regionalgroup.request.errors.already_part_of_rg')); + } + if($validated['newRequest']['topic'] == 'change' && !($this->account->isMemberOfRegionalgroup($rg) || $this->account->isGuestOfRegionalgroup($rg))){ + // wants type changed but is no member/guest of this rg + abort(422, trans('regionalgroup.request.errors.change_but_no_member')); + } + if ($validated['newRequest']['topic'] == 'change' && $validated['newRequest']['type'] == 'member' && $this->account->isMemberOfRegionalgroup($rg)){ + // wants type changed but is already member + abort(422, trans('regionalgroup.request.errors.change_but_is_member')); + } + if ($validated['newRequest']['topic'] == 'change' && $validated['newRequest']['type'] == 'guest' && $this->account->isGuestOfRegionalgroup($rg)){ + // wants type changed but is already guest + abort(422, trans('regionalgroup.request.errors.change_but_is_guest')); + } + + if ($validated['newRequest']['topic'] == 'leave' && !($this->account->isMemberOfRegionalgroup($rg) || $this->account->isGuestOfRegionalgroup($rg))){ + // wants leave but is no member/guest + abort(422, trans('regionalgroup.request.errors.leave_but_is_member')); + } + + $newRequest = new RegionalgroupRequest(); + $newRequest->regionalgroup_id = $validated['newRequest']['regionalgroup']; + $newRequest->account_id = $this->account->id; + $newRequest->topic = $validated['newRequest']['topic']; + $newRequest->reason = $validated['newRequest']['reason']; + if ($validated['newRequest']['topic'] == 'leave' && $this->account->isMemberOfRegionalgroup($rg)){ + $newRequest->type = 'member'; + } else if ($validated['newRequest']['topic'] == 'leave' && $this->account->isGuestOfRegionalgroup($rg)){ + $newRequest->type = 'guest'; + } else { + $newRequest->type = $validated['newRequest']['type']; + } + // $newRequest->as = $validated['newRequest']['as']; + $newRequest->as = 'both'; + $newRequest->save(); + $newRequest->loadMissing('regionalgroup'); + + //if the user wants to change to guest membership no appoval is needed + if ($newRequest->topic == 'change' && $newRequest->type == 'guest'){ + $this->_autoAcceptChangeRequest($newRequest); + } + + $newRequest->regionalgroup->setAppends([]); + return $newRequest; + } + abort(403); + } + + private function _autoAcceptChangeRequest(RegionalgroupRequest $regionalgroupRequest){ + $regionalgroup = $regionalgroupRequest->regionalgroup; + if ($regionalgroup->chief_id == $regionalgroupRequest->account->id || $regionalgroup->deputy_id == $regionalgroupRequest->account->id) { + return; //we better not do this for rg chief/deputy + } + + GroupHelper::revoke($regionalgroupRequest->account, ['rg.nav', 'rg.event', 'rg.mentor']); + $regionalgroup->mentors()->detach($regionalgroupRequest->account); + $regionalgroup->eventler()->detach($regionalgroupRequest->account); + $regionalgroup->navigators()->detach($regionalgroupRequest->account); + + // Shortly remove from RG + $regionalgroup->accounts()->detach($regionalgroupRequest->account); + + // Then add to the same RG with new type (only auto accept guest) + $regionalgroup->accounts()->attach($regionalgroupRequest->account, ['guest' => true, 'pilot' => true, 'controller' => true]); + + $regionalgroupRequest->delete(); + + activity() + ->causedBy($regionalgroupRequest->account) + ->performedOn($regionalgroupRequest->account) + ->log("Hat seinen Mitgliedschaftstypus in der Regionalgruppe {$regionalgroup->name} auf Gastmitglied verändert"); + activity() + ->causedBy($regionalgroupRequest->account) + ->performedOn($regionalgroupRequest->account) + ->log("Die Regionalgruppeanfrage mit der RRID {$regionalgroupRequest->id} wurde automatisch akzeptiert!"); + + $regionalgroupRequest->account->notify(new RequestAcceptedNotification($regionalgroup)); + } + + + public function delete(Request $request, RegionalgroupRequest $regionalgroupRequest) + { + if($request->ajax()) { + $rrid = $regionalgroupRequest->id; + $regionalgroupRequest->delete(); + + return $rrid; + } + abort(403); + } + +} diff --git a/app/Http/Controllers/Resources/ResourceController.php b/app/Http/Controllers/Resources/ResourceController.php new file mode 100644 index 0000000..b7ef97f --- /dev/null +++ b/app/Http/Controllers/Resources/ResourceController.php @@ -0,0 +1,40 @@ +headers->get('referer'); + // if($referer == null || !preg_match("/^https?:\/\/(\w+\.)?".config('APP_URL')."/", $referer)) { + // abort(403); + // } + + $fullpath = storage_path('app').'/'.$image->path; + if($image->approved && File::exists($fullpath)) + { + $file = File::get($fullpath); + $type = File::mimeType($fullpath); + + $response = Response::make($file, 200); + $response->header("Content-Type", $type); + return $response; + } + else + abort(403); + } + +} diff --git a/app/Http/Controllers/Statistics/AerodromeController.php b/app/Http/Controllers/Statistics/AerodromeController.php new file mode 100644 index 0000000..5428ce1 --- /dev/null +++ b/app/Http/Controllers/Statistics/AerodromeController.php @@ -0,0 +1,143 @@ +viewMake('frontend.statistics.aerodromes'); + } + + /** + * Display the specified resource. + * + * @param Aerodrome $aerodrome + * @return \Illuminate\Http\Response + */ + public function show(Request $request, Aerodrome $aerodrome) + { + $from = \Carbon\Carbon::createFromFormat('d.m.Y', $request->from, 'utc'); + $from->setHours(0); + $from->setMinutes(0); + $from->setSeconds(0); + + $till = \Carbon\Carbon::createFromFormat('d.m.Y', $request->till, 'utc'); + $till->setHours(23); + $till->setMinutes(59); + $till->setSeconds(59); + // Find data for an aerodrome + $atcHistory = \App\Models\Network\AtcClient::offline() + ->icao($aerodrome->icao) + ->whereBetween('connected_at', [$from, $till]) + ->orderBy('connected_at', 'DESC') + ->get(); + $atcCurrent = \App\Models\Network\AtcClient::online() + ->icao($aerodrome->icao) + // ->whereBetween('connected_at', [$from, $till]) + ->orderBy('connected_at', 'DESC') + ->get(); + + $departureHistory = \App\Models\Network\PilotClient::offline() + ->where('departure_airport', $aerodrome->icao) + ->whereBetween('departed_at', [$from, $till]) + ->orderBy('departed_at', 'DESC') + ->get(); + $departureStats = \App\Models\Statistic\FlightData::where('departure_airport', $aerodrome->icao) + ->whereBetween('departed_at', [$from, $till]) + ->orderBy('departed_at', 'DESC') + ->get(); + $departureHistory = $departureHistory->merge($departureStats); + + $departureCurrent = \App\Models\Network\PilotClient::online() + ->where('departure_airport', $aerodrome->icao) + // ->whereBetween('connected_at', [$from, $till]) + ->orderBy('connected_at', 'DESC') + ->get(); + + $arrivalHistory = \App\Models\Network\PilotClient::offline() + ->where('arrival_airport', $aerodrome->icao) + ->whereBetween('arrived_at', [$from, $till]) + ->orderBy('arrived_at', 'DESC') + ->get(); + $arrivalStats = \App\Models\Statistic\FlightData::where('arrival_airport', $aerodrome->icao) + ->whereBetween('arrived_at', [$from, $till]) + ->orderBy('arrived_at', 'DESC') + ->get(); + $arrivalHistory = $arrivalHistory->merge($arrivalStats); + + $arrivalCurrent = \App\Models\Network\PilotClient::online() + ->where('arrival_airport', $aerodrome->icao) + // ->whereBetween('connected_at', [$from, $till]) + ->orderBy('connected_at', 'DESC') + ->get(); + + // Find the statistics + $atcStatistics = \App\Models\Statistic\AtcData::callsign($aerodrome->icao) + ->whereBetween('connected_at', [$from, $till]) + ->orderBy('connected_at', 'DESC') + ->get(); + + + + // Merge atc offline and atc statistics + $atcHistory = $atcHistory->merge($atcStatistics); + + // Return the stats view + return $this->viewMake('frontend.statistics.aerodrome') + ->with('aerodrome', $aerodrome) + ->with('atcHistory', $atcHistory) + ->with('atcCurrent', $atcCurrent) + ->with('departureHistory', $departureHistory) + ->with('departureCurrent', $departureCurrent) + ->with('arrivalHistory', $arrivalHistory) + ->with('arrivalCurrent', $arrivalCurrent) + ->with('from', $from) + ->with('till', $till); + } + + /** + * Search an aerodrome by icao + * @param Request $request [description] + * @return [type] [description] + */ + public function search(Request $request) + { + $validated = $request->validate([ + 'searchString' => 'required|string' + ]); + + $aerodrome = null; + // Is it an icao? + if(preg_match('/[a-zA-Z]{4}/', $validated['searchString'])) { + $aerodrome = Aerodrome::icao($validated['searchString'])->first(); + } + // City or name? + if($aerodrome == null && preg_match('/[\w\s]+/', $validated['searchString'])) { + $aerodrome = Aerodrome::where('name', $validated['searchString'])->orWhere('city', $validated['searchString'])->first(); + } + + if($aerodrome != null) { + return redirect()->route('statistics.aerodrome.icao', $aerodrome->icao); + } else { + return redirect()->route('statistics.aerodrome.home'); + } + + abort(404); + } +} diff --git a/app/Http/Controllers/Statistics/AtcController.php b/app/Http/Controllers/Statistics/AtcController.php new file mode 100644 index 0000000..e8bc094 --- /dev/null +++ b/app/Http/Controllers/Statistics/AtcController.php @@ -0,0 +1,85 @@ +viewMake('frontend.statistics.atcSearch'); + } + + /** + * Search for a resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function search(Request $request) + { + + $validated = $request->validate( + [ + 'searchString' => 'string|required', + 'from' => 'required|date', + 'till' => 'required|date', + ] + ); + + // Do the search + $ad = null; + + try{ + $from = \Carbon\Carbon::createFromFormat('d.m.Y', $request->from, 'utc'); + } catch(\Carbon\Exceptions\InvalidFormatException $e) { + $from = \Carbon\Carbon::createFromFormat('Y-m-d', $request->from, 'utc'); + } + $from->setHours(0); + $from->setMinutes(0); + $from->setSeconds(0); + + try { + $till = \Carbon\Carbon::createFromFormat('d.m.Y', $request->till, 'utc'); + } catch(\Carbon\Exceptions\InvalidFormatException $e) { + $till = \Carbon\Carbon::createFromFormat('Y-m-d', $request->till, 'utc'); + } + $till->setHours(23); + $till->setMinutes(59); + $till->setSeconds(59); + + // Is it a callsign? + if(preg_match('/[A-Z]\w*/', $validated['searchString'])) { + $ad = AtcData::callsign($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + // Is it a cid? + if(preg_match('/\[0-9\]{7}/', $validated['searchString']) || preg_match('/[0-9]{6}/', $validated['searchString'])) { + $ad = AtcData::cid($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + // Is it an icao? + if(preg_match('/1[1-9]{2}[.][0-9]{3}/', $validated['searchString'])) { + $ad = AtcData::frequency($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + + // dd($ad); + + if($validated){ + return $this->viewMake('frontend.statistics.atcSessions')->with('searched', $validated['searchString'])->with('atcSessions', $ad); + } + return redirect()->back()->withInput(); + } +} diff --git a/app/Http/Controllers/Statistics/FlightController.php b/app/Http/Controllers/Statistics/FlightController.php new file mode 100644 index 0000000..5fe6e8d --- /dev/null +++ b/app/Http/Controllers/Statistics/FlightController.php @@ -0,0 +1,87 @@ +viewMake('frontend.statistics.flight'); + } + + /** + * Search stuff. + * + * @param Request $request + * @return \Illuminate\Http\Response + */ + public function search(Request $request) + { + $validated = $request->validate( + [ + 'searchString' => 'string|required', + 'from' => 'required|date', + 'till' => 'required|date', + ] + ); + + // Do the search + $fd = null; + + try{ + $from = \Carbon\Carbon::createFromFormat('d.m.Y', $request->from, 'utc'); + } catch(\Carbon\Exceptions\InvalidFormatException $e) { + $from = \Carbon\Carbon::createFromFormat('Y-m-d', $request->from, 'utc'); + } + $from->setHours(0); + $from->setMinutes(0); + $from->setSeconds(0); + + try { + $till = \Carbon\Carbon::createFromFormat('d.m.Y', $request->till, 'utc'); + } catch(\Carbon\Exceptions\InvalidFormatException $e) { + $till = \Carbon\Carbon::createFromFormat('Y-m-d', $request->till, 'utc'); + } + $till->setHours(23); + $till->setMinutes(59); + $till->setSeconds(59); + + // Is it a callsign? + if(preg_match('/[A-Z]{3}\w*/', $validated['searchString'])) { + $fd = FlightData::callsign($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + // Is it an icao? + if(preg_match('/[a-zA-Z]{4}/', $validated['searchString'])) { + $fd = FlightData::icao($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + // Is it a cid? + if(preg_match('/\[0-9\]{7}/', $validated['searchString']) || preg_match('/[0-9]{6}/', $validated['searchString'])) { + $fd = FlightData::cid($validated['searchString'])->whereBetween('connected_at', [$from, $till])->orderBy('connected_at', 'DESC')->get(); + } + + if($fd != null) + $filteredFd = $fd->filter(function($flight) { + if($flight->departure_airport != '' && $flight->arrival_airport != '') return true; + return false; + }); + else + $filteredFd = array(); + + return $this->viewMake('frontend.statistics.flights')->with('flightData', $filteredFd)->with('searchString', $validated['searchString']); + } +} diff --git a/app/Http/Controllers/Statistics/StatisticcenterController.php b/app/Http/Controllers/Statistics/StatisticcenterController.php new file mode 100644 index 0000000..28b4efc --- /dev/null +++ b/app/Http/Controllers/Statistics/StatisticcenterController.php @@ -0,0 +1,21 @@ +viewMake('frontend.statistics.index'); + } + +} diff --git a/app/Http/Controllers/Translation/TranslationController.php b/app/Http/Controllers/Translation/TranslationController.php new file mode 100644 index 0000000..d9cd481 --- /dev/null +++ b/app/Http/Controllers/Translation/TranslationController.php @@ -0,0 +1,37 @@ +firstOrFail(); + + //dd($aerodrome->toArray()); + + $result = ArrayToXml::convert( + $aerodrome->toArray(), + [ + 'rootElementName' => 'data', + ], + true, + 'UTF-8' + ); + + return response( + $result, + 200, + [ + 'Content-Type' => 'application/xml', + ] + ); + } + + /** + * Get runway information regarding an aerodrome indentified by it's ICAO + * + * This function will deliver a paired runway information. + * E.g. a runway that has setup an opposite one in the backend will be delivered not as two, but as one runway-pair + * + * @param Request $request [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function runways(Request $request, $icao) + { + $aerodrome = Aerodrome::icao($icao)->firstOrFail(); + $aerodrome->loadMissing('runways.opposite'); + + $runways = $aerodrome->runways; + + $rwyPairs = [ + ]; + + foreach ($runways as $runway) { + if(!array_key_exists('__custom:runway:'.$runway->id, $rwyPairs) && ($runway->opposite != null && !array_key_exists('__custom:runway:'.$runway->opposite->id, $rwyPairs))) + { + $rwyPairs['__custom:runway:'.$runway->id] = [ + 'ident' => $runway->ident, + 'heading' => $runway->heading, + 'dimensions' => $runway->length.'m x '.$runway->width.'m', + 'surface' => $runway->surfaceTypeString, + 'opposite_ident' => $runway->opposite->ident, + 'opposite_heading' => $runway->opposite->heading, + ]; + } elseif (!array_key_exists('__custom:runway:'.$runway->id, $rwyPairs) && $runway->opposite == null) { + $rwyPairs['__custom:runway:'.$runway->id] = [ + 'ident' => $runway->ident, + 'heading' => $runway->heading, + 'dimensions' => $runway->length.'m x '.$runway->width.'m', + 'surface' => $runway->surfaceTypeString + ]; + } elseif ($runway->opposite != null && array_key_exists('__custom:runway:'.$runway->opposite->id, $rwyPairs)) { + $rwyPairs['__custom:runway:'.$runway->opposite->id]['opposite_ident'] = $runway->ident; + $rwyPairs['__custom:runway:'.$runway->opposite->id]['opposite_heading'] = $runway->heading; + } + } + + $result = ArrayToXml::convert( + $rwyPairs, + [ + 'rootElementName' => 'runways', + ], + false, + 'UTF-8' + ); + + return response( + $result, + 200, + [ + 'Content-Type' => 'application/xml', + ] + ); + } + + public function stations(Request $request, $icao) + { + $aerodrome = Aerodrome::icao($icao)->firstOrFail(); + $aerodrome->loadMissing('stations'); + + $stations = []; + + foreach ($aerodrome->stations as $station) { + $stations['__custom:station:'.$station->id] = [ + 'name' => $station->name, + 'ident' => $station->ident, + 'frequency' => $station->fixedFrequency, + 'bookable' => $station->bookable ? 1 : 0, + 'atis' => $station->atis ? 1 : 0, + ]; + } + + $result = ArrayToXml::convert( + $stations, + [ + 'rootElementName' => 'stations', + ], + false, + 'UTF-8' + ); + + return response( + $result, + 200, + [ + 'Content-Type' => 'application/xml', + ] + ); + } + +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..50adcdc --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,81 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\LocaleMiddleware::class, + \Spatie\CookieConsent\CookieConsentMiddleware::class, + ], + 'languages' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + 'api' => [ + 'throttle:240,1', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + + 'optionalAuth' => \App\Http\Middleware\OptionalAuthentication::class, + 'setupCompleted' => \App\Http\Middleware\Membership\SetupCompletedMiddleware::class, + 'checkBanned' => \App\Http\Middleware\Membership\CheckBannedMiddleware::class, + 'checkInactive' => \App\Http\Middleware\Membership\CheckInactiveMiddleware::class, + ]; +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..55b0ba6 --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,21 @@ +expectsJson()) { + return route('vatauth.login'); + } + } +} diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php new file mode 100644 index 0000000..35b9824 --- /dev/null +++ b/app/Http/Middleware/CheckForMaintenanceMode.php @@ -0,0 +1,17 @@ +hasHeader('x-locale')) { + $locale = $request->header('x-locale'); + } + app()->setLocale($locale); + setlocale(LC_TIME, $locale); + Carbon::setLocale($locale); + + return $next($request); + } + if (!Session::has('language') && !Auth::check()) { + Session::put('language', app()->getLocale()); + } + if (!Session::has('language') && Auth::check()) { + Session::put('language', Auth::user()->setting->language); + } + + app()->setLocale(Session::get('language')); + setlocale(LC_TIME, Session::get('language')); + Carbon::setLocale(Session::get('language')); + + $response = $next($request); + $response->headers->set('x-locale', Session::get('language')); + + return $response; + } +} diff --git a/app/Http/Middleware/Membership/CheckBannedMiddleware.php b/app/Http/Middleware/Membership/CheckBannedMiddleware.php new file mode 100644 index 0000000..1e8b5a2 --- /dev/null +++ b/app/Http/Middleware/Membership/CheckBannedMiddleware.php @@ -0,0 +1,34 @@ +_authGuard = $guard; + } + + /** + * Handle an incoming request. + * + * @param Request $request + * @param Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if($this->_authGuard->user()->is_currently_homepage_banned) { + return redirect()->route('membership.banned'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Membership/CheckInactiveMiddleware.php b/app/Http/Middleware/Membership/CheckInactiveMiddleware.php new file mode 100644 index 0000000..270fc8e --- /dev/null +++ b/app/Http/Middleware/Membership/CheckInactiveMiddleware.php @@ -0,0 +1,33 @@ +_authGuard = $guard; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if($this->_authGuard->user()->isInactive) { + return redirect()->route('membership.inactive'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Membership/SetupCompletedMiddleware.php b/app/Http/Middleware/Membership/SetupCompletedMiddleware.php new file mode 100644 index 0000000..33ad574 --- /dev/null +++ b/app/Http/Middleware/Membership/SetupCompletedMiddleware.php @@ -0,0 +1,34 @@ +_authGuard = $guard; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + + if(!$this->_authGuard->user()->setup_completed) { + return redirect()->route('membership.setup'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/OptionalAuthentication.php b/app/Http/Middleware/OptionalAuthentication.php new file mode 100644 index 0000000..e55fbf8 --- /dev/null +++ b/app/Http/Middleware/OptionalAuthentication.php @@ -0,0 +1,23 @@ +authenticate($request, $guards); + } catch(AuthenticationException $e) { + // dont do anything + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..2395ddc --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,27 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..5a50e7b --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,18 @@ +timestamp = \Carbon\Carbon::now()->utc(); + $this->eventroute = $eventroute; + $this->eventroute->loadMissing('legs.arrivalAerodrome', 'legs.departureAerodrome', 'legs.accounts'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() : void + { + Log::channel('jobdaily')->info('[Events]::DatafeedLookupJob: Working on ' . $this->eventroute->name); + $this->handleLegs(); + $this->handleBadges(); + Log::channel('jobdaily')->info('[Events]::DatafeedLookupJob: Working on ' . $this->eventroute->name . ' finished'); + } + + private function handleBadges() : void + { + if ($this->eventroute->legs()->count() == 0) return; + if (is_null($this->eventroute->forum_badge_id)) return; + + $badge_account_ids = $this->eventroute->legs[0]->accounts + ->map(function ($acc){ return $acc->id; }); + + foreach($this->eventroute->legs as $leg) { + $accounts = $leg->accounts()->wherePivotNotNull('completed_at')->get() + ->map(function ($acc){ return $acc->id; });; + $badge_account_ids = $badge_account_ids->intersect($accounts); + } + + foreach ($badge_account_ids as $badge_account_id) + { + $account = Account::query()->where('id', $badge_account_id)->first(); + $badge_added = XenForoApi::add_user_badge($account, $this->eventroute->forum_badge_id); + if($badge_added) { + // Send Forum notification + $title = "Tour " . $this->eventroute->name . " abgeschlossen"; + $message = " Hallo " . $account->firstname .", \n + herzlichen Glückwunsch! Du hast die Tour " . $this->eventroute->name . " erfolgreich abgeschlossen und erhältst deshalb das Badge als Anerkennung für deine Leistung. + Wir hoffen, dass du die Tour genossen und dabei neue Erfahrungen gesammelt hast. Das Badge ist eine besondere Auszeichnung für dich, um deine Leistung zu feiern und zu zeigen, + dass du ein wahrer Abenteurer bist. + Wir freuen uns darauf, dich bei zukünftigen Touren wieder begrüßen zu dürfen.\n + Beste Grüße,\n + das VATGER Touren Team"; + + XenBridge::sendAccountNotification($account, $title, $message); + } + } + + } + + + private function handleLegs() : void + { + foreach($this->eventroute->legs as $leg) { + $this->handleLeg($leg); + } + } + + private function handleLeg(RouteLeg $leg) : void + { + $pilots = FlightData::completed() + ->where('departed_at', '>=', $this->eventroute->begins_at) + ->where('arrived_at', '<=', $this->eventroute->ends_at) + ->where('departure_airport', 'like', $leg->departureAerodrome->icao) + ->where('arrival_airport', 'like', $leg->arrivalAerodrome->icao) + ->whereIntegerInRaw('account_id', $this->getLegParticipants($leg)) + ->get(); + + foreach ($pilots as $pilot) { + $this->_checkCompletedLeg($pilot, $leg); + } + } + + + private function getLegParticipants(RouteLeg $leg): array + { + $participating_ids = []; + foreach ($leg->accounts as $acc) + $participating_ids[] = $acc->id; + return $participating_ids; + } + + + + private function _checkCompletedLeg(FlightData $flightData, RouteLeg $leg){ + $eventRoute = $this->eventroute; + $account = $leg->accounts()->wherePivot('account_id', $flightData->account_id)->first(); + if(is_null($account) || is_null($account->pivot)) return; //should not happen + $pivot = $account->pivot; + + if($flightData->arrived_at < $pivot->completed_at) return; + if($this->eventroute->flight_rules == 'V' && $flightData->flight_type != 'V') return; + if($this->eventroute->flight_rules == 'I' && $flightData->flight_type != 'I') return; + if(!empty($this->eventroute->aircrafts) && !in_array($flightData->aircraft, explode(",", $this->eventroute->aircrafts))) return; //we use the aircraft_short in vatsim json so no equipment + + if(!$this->eventroute->require_order) { + $leg->accounts()->updateExistingPivot($flightData->account_id, ['completed_at' => $flightData->arrived_at, 'fight_data_id' => $flightData->id]); + } else { + $last_leg_completed = \Carbon\Carbon::parse($this->eventroute->begins_at); + foreach ($this->eventroute->legs as $otherleg) { + $otherlegaccount = $otherleg->accounts()->wherePivot('account_id', $flightData->account_id)->first(); + if(is_null($otherlegaccount) || is_null($otherlegaccount->pivot)) return; //should not happen + if ($leg->id == $otherleg->id && \Carbon\Carbon::parse($flightData->arrived_at)->isAfter($last_leg_completed)){ + //the leg we want to check is after the previous leg + $leg->accounts()->updateExistingPivot($flightData->account_id, ['completed_at' => $flightData->arrived_at, 'fight_data_id' => $flightData->id]); + return; + } elseif (is_null($otherlegaccount->pivot->completed_at)){ + //one of the previous legs is not completed + return; + } else { + //until now all legs have been completed + $last_leg_completed = \Carbon\Carbon::parse($otherlegaccount->pivot->completed_at); + } + } + } + + } + +} diff --git a/app/Jobs/Forum/UpdateAccounts.php b/app/Jobs/Forum/UpdateAccounts.php new file mode 100644 index 0000000..1b00b6c --- /dev/null +++ b/app/Jobs/Forum/UpdateAccounts.php @@ -0,0 +1,107 @@ +_lastUpdatedAccount = Cache::get( + "forum.updater.lastUpdatedAccount" + ); + } else { + $this->_lastUpdatedAccount = 0; + } + + $this->_totalAccounts = Account::count(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + Log::channel("jobdaily")->info( + "[Forum]::Starting update. First CID " . $this->_lastUpdatedAccount + ); + + $chunkSize = + $this->_totalAccounts > 500 + ? ceil($this->_totalAccounts / 12) + : 100; + + $accountsToUpdate = Account::where( + "id", + ">=", + $this->_lastUpdatedAccount + 1 + ) + ->take($chunkSize) + ->get(); + foreach ($accountsToUpdate as $acc) { + if ($acc->setting == null) { + continue; + } + if ($acc->setting->forum_id == null) { + continue; + } + + if ( + $acc->is_currently_forum_banned || + $acc->data->rating_atc == 0 + ) { + XenBridge::banForumAccount($acc); + } else { + XenBridge::updateForumAccount($acc); + } + + //hacky for now, do better in V3 + //try { + // $gitlab = new Gitlab(); + // $gitlab->checkNAVAssignments($acc); + //} catch (Throwable $th) { + //} + } + if ($accountsToUpdate->count() < $chunkSize) { + Cache::forget("forum.updater.lastUpdatedAccount"); + Log::channel("jobdaily")->info( + "[Forum]::Finished update. Last CID " . + $accountsToUpdate->last()->id + ); + } else { + Cache::put( + "forum.updater.lastUpdatedAccount", + $accountsToUpdate->last()->id + ); + Log::channel("jobdaily")->info( + "[Forum]::Stopped update. Last CID " . + $accountsToUpdate->last()->id + ); + } + } +} diff --git a/app/Jobs/Membership/CleanIncompletedRegistrationsJob.php b/app/Jobs/Membership/CleanIncompletedRegistrationsJob.php new file mode 100644 index 0000000..122685e --- /dev/null +++ b/app/Jobs/Membership/CleanIncompletedRegistrationsJob.php @@ -0,0 +1,46 @@ +_accounts = $accounts; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (count($this->_accounts) == 0) return; + + foreach ($this->_accounts as $acc) { + $acc->loadMissing('data', 'setting'); + + if ($acc->setting != null) $acc->setting->forceDelete(); + if ($acc->data != null) $acc->data->forceDelete(); + $acc->forceDelete(); + } + + } +} diff --git a/app/Jobs/Membership/UpdateViaApiJob.php b/app/Jobs/Membership/UpdateViaApiJob.php new file mode 100644 index 0000000..b685826 --- /dev/null +++ b/app/Jobs/Membership/UpdateViaApiJob.php @@ -0,0 +1,62 @@ +_lastUpdatedAccount = Cache::get('membership.updater.api.lastUpdatedAccount'); + } else { + $this->_lastUpdatedAccount = 0; + } + + $this->_totalAccounts = Account::count(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + \Log::channel('jobdaily')->info('[Api]::Starting update Api. Starting CID ' . $this->_lastUpdatedAccount); + + $chunkSize = $this->_totalAccounts > 100 ? ceil($this->_totalAccounts / (7*24)) : 50; // all accounts in 1/2 week with jobs every half hour + + $accountsToUpdate = Account::where('id', '>=', $this->_lastUpdatedAccount + 1)->take($chunkSize)->get(); + foreach ($accountsToUpdate as $acc) { + ApiHelper::updateAccount($acc); + } + if ($accountsToUpdate->count() < $chunkSize) { + Cache::forget('membership.updater.api.lastUpdatedAccount'); + \Log::channel('jobdaily')->info('[Api]::Finished update. Last CID ' . $accountsToUpdate->last()->id); + } else { + Cache::put('membership.updater.api.lastUpdatedAccount', $accountsToUpdate->last()->id); + \Log::channel('jobdaily')->info('[Api]::Stopped update. Last CID ' . $accountsToUpdate->last()->id); + } + + } +} diff --git a/app/Jobs/Membership/UpdateViaConnectJob.php b/app/Jobs/Membership/UpdateViaConnectJob.php new file mode 100644 index 0000000..16365ef --- /dev/null +++ b/app/Jobs/Membership/UpdateViaConnectJob.php @@ -0,0 +1,62 @@ +_lastUpdatedAccount = Cache::get('membership.updater.connect.lastUpdatedAccount'); + } else { + $this->_lastUpdatedAccount = 0; + } + + $this->_totalAccounts = Account::count(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + \Log::channel('jobdaily')->info('[Connect]::Starting update. Starting CID ' . $this->_lastUpdatedAccount); + + $chunkSize = $this->_totalAccounts > 100 ? ceil($this->_totalAccounts / (24*7)) : 50; // all accounts in one week with jobs every hour + + $accountsToUpdate = Account::where('id', '>=', $this->_lastUpdatedAccount + 1)->where('refresh_token', '!=', null)->take($chunkSize)->get(); + foreach ($accountsToUpdate as $acc) { + ConnectHelper::updateAccount($acc); + } + if ($accountsToUpdate->count() < $chunkSize) { + Cache::forget('membership.updater.connect.lastUpdatedAccount'); + \Log::channel('jobdaily')->info('[Connect]::Finished update. Last CID ' . $accountsToUpdate->last()->id); + } else { + Cache::put('membership.updater.connect.lastUpdatedAccount', $accountsToUpdate->last()->id); + \Log::channel('jobdaily')->info('[Connect]::Stopped update. Last CID ' . $accountsToUpdate->last()->id); + } + + } +} diff --git a/app/Jobs/Navigation/ParseGoogleEarthKMZJob.php b/app/Jobs/Navigation/ParseGoogleEarthKMZJob.php new file mode 100644 index 0000000..c6a7bf6 --- /dev/null +++ b/app/Jobs/Navigation/ParseGoogleEarthKMZJob.php @@ -0,0 +1,64 @@ +_kmlContent = $kmlRead; + $this->_customer = $acc; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if($this->_kmlContent != null && strlen($this->_kmlContent) > 0) { + \Log::info('KMZ Parser: Initializing new run.'); + $xmlModel = new SimpleXMLElement($this->_kmlContent); + $sctModel = new \App\Models\Navigation\SCT($xmlModel); + \Log::info('KMZ Parser: Starting the parser'); + $sctModel->parse(); + \Log::info('KMZ Parser: Parsing completed. Starting renderer.'); + // $result = $sctModel->render(); + $result = $sctModel->build(); + + $outputFileName = 'temp/'.$this->_customer->id.'/'.\Carbon\Carbon::now()->timestamp.'.sct'; + Storage::put($outputFileName, $result); + \Log::info('KMZ Parser: Run completed.'); + $this->_customer->notify(new \App\Notifications\Navigation\GoogleEarthKMZParsed($outputFileName)); + } + } +} diff --git a/app/Jobs/Network/DownloadDataFeed.php b/app/Jobs/Network/DownloadDataFeed.php new file mode 100644 index 0000000..4e27451 --- /dev/null +++ b/app/Jobs/Network/DownloadDataFeed.php @@ -0,0 +1,215 @@ +toDateTimeString(), 59); + + $data = $this->_downloadDataFeedFile($this->dataFeedUrl); + if(is_null($data)){ + return; + } + try { + $this->_processData($data); + } catch (Exception $e){ + //handle + } + } + + /** + * Download the raw networkfeed file + * @param string $url + * @return string|null + */ + private function _downloadDataFeedFile(string $url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + $data = curl_exec($ch); + if(curl_errno($ch)){ + $data = null; + } + curl_close($ch); + return $data; + } + + + private function _processData($data) + { + $connectedClients = json_decode($data); + \Illuminate\Support\Facades\Cache::put('network.data.connectedClients', $connectedClients, 300); + \Log::channel('jobdaily')->info('[DownloadDataFeed]::Updating Pilots'); + + if (is_null($connectedClients) || is_null($connectedClients->general) || is_null($connectedClients->general->update_timestamp)) { + \Log::channel('jobdaily')->info('[DownloadDataFeed]::Datafeed invalid'); + return; + } + + \App\Models\Network\PilotClient::whereNull('disconnected_at') + ->update(['disconnected_at' => \Carbon\Carbon::parse($connectedClients->general->update_timestamp)->toDateTimeString()]); + foreach ($connectedClients->pilots as $pilot) { + $this->_updatePilot($pilot); + } + \Log::channel('jobdaily')->info('[DownloadDataFeed]::Pilots Updated'); + \App\Models\Network\AtcClient::whereNull('disconnected_at') + ->update(['disconnected_at' => \Carbon\Carbon::parse($connectedClients->general->update_timestamp)->toDateTimeString()]); + foreach ($connectedClients->controllers as $atc) { + $this->_updateATC($atc); + } + } + + /** + * Insert a pilot into table + * @param mixed $pilot + * @return void + */ + private function _updatePilot($pilot) + { + $p = $pilot; + if (\is_null($p) || \is_null($p->flight_plan)) { + return; + } + + // delete ONLY multiple flight, CAVE there may be multiple flights in one VATSIM connection, which is ok + $sameFlightCount = \App\Models\Network\PilotClient::where('callsign', $p->callsign) + ->where('departure_airport', 'LIKE', $p->flight_plan->departure) + ->where('arrival_airport', 'LIKE', $p->flight_plan->arrival) + ->count(); + $firstSameFlight = \App\Models\Network\PilotClient::where('callsign', $p->callsign) + ->where('departure_airport', 'LIKE', $p->flight_plan->departure) + ->where('arrival_airport', 'LIKE', $p->flight_plan->arrival) + ->first(); + if ($sameFlightCount > 1) { + $dbPilots = \App\Models\Network\PilotClient::where('callsign', $p->callsign) + ->where('departure_airport', 'LIKE', $p->flight_plan->departure) + ->where('arrival_airport', 'LIKE', $p->flight_plan->arrival) + ->where('id', '!=', $firstSameFlight->id) + ->delete(); + } + + $pilotClient = \App\Models\Network\PilotClient::firstOrNew( + [ + 'account_id' => $p->cid, + 'callsign' => $p->callsign, + 'departure_airport' => $p->flight_plan->departure, + 'arrival_airport' => $p->flight_plan->arrival, + 'alternative_airport' => $p->flight_plan->alternate, + ] + ); + if(empty($pilotClient->connected_at)) $pilotClient->connected_at = \Carbon\Carbon::parse($p->logon_time)->toDateTimeString(); + $pilotClient->minutes_online = \Carbon\Carbon::parse($pilotClient->connected_at)->diffInMinutes($p->last_updated); + $pilotClient->disconnected_at = null; + $pilotClient->aircraft = $p->flight_plan->aircraft_short; //just B738 not B738/L or B738/M-SDE2E3FGHIJ1RWY/LB1 + $pilotClient->cruise_altitude = $p->flight_plan->altitude; + $pilotClient->cruise_tas = $p->flight_plan->cruise_tas; + $pilotClient->flight_type = $p->flight_plan->flight_rules; + $pilotClient->route = $p->flight_plan->route; + $pilotClient->remarks = $p->flight_plan->remarks; + $pilotClient->current_latitude = $p->latitude; + $pilotClient->current_longitude = $p->longitude; + $pilotClient->current_altitude = $p->altitude; + $pilotClient->current_groundspeed = $p->groundspeed; + $pilotClient->current_heading = $p->heading; + // $pilotClient->save(); // Fill be saved in the subfunction. + $this->_postUpdatePilot($pilotClient, $p); + } + + private function _postUpdatePilot($pilotClient, $p) + { + // Find out if the pilot has departed and arrived + $depAirport = \App\Models\Navigation\Aerodrome::icao($pilotClient->departure_airport)->first(); + if ($depAirport != null && $pilotClient->departed_at == null) { + // we actually know the airport + // and the pilot is not departed by now + if (!$depAirport->containsCoordinates($pilotClient->current_latitude, $pilotClient->current_longitude)) { + // Pilot is no longer in the vicinity of the airport + if ($pilotClient->current_altitude > $depAirport->elevation + 300 && $pilotClient->current_groundspeed > 35) { + $pilotClient->departed_at = \Carbon\Carbon::parse($p->last_updated)->toDateTimeString(); + } + } + } else { + if ($pilotClient->departed_at == null) { + if ($pilotClient->current_groundspeed > 35) { + $pilotClient->departed_at = \Carbon\Carbon::parse($p->last_updated)->toDateTimeString(); + } + } + } + $arrAirport = \App\Models\Navigation\Aerodrome::icao($pilotClient->arrival_airport)->first(); + if ($arrAirport != null && $pilotClient->arrived_at == null) { + if ($arrAirport->containsCoordinates($pilotClient->current_latitude, $pilotClient->current_longitude)) { + // At least within airport boundary... + if ($pilotClient->current_altitude < $arrAirport->elevation + 100 && $pilotClient->current_groundspeed <= 35) { + $pilotClient->arrived_at = \Carbon\Carbon::parse($p->last_updated)->toDateTimeString(); + } + } + } else { + if ($pilotClient->arrived_at == null) { + if ($pilotClient->current_groundspeed <= 35) { + $pilotClient->arrived_at = \Carbon\Carbon::parse($p->last_updated)->toDateTimeString(); + } + } + } + $pilotClient->save(); + } + + /** + * Insert an ATC into table + * @param mixed $atc + * @return void + */ + private function _updateATC($atc) + { + $a = $atc; + if ($a->facility == 0 || $a->frequency == "199.998" || preg_match('/_OBS/s', $a->callsign) || preg_match('/_ATIS/s', $a->callsign)) { + return; + } + + $atcClient = \App\Models\Network\AtcClient::firstOrNew( + [ + 'account_id' => $a->cid, + 'callsign' => $a->callsign, + ] + ); + $atcClient->connected_at = \Carbon\Carbon::parse($a->logon_time)->toDateTimeString(); + $atcClient->minutes_online = \Carbon\Carbon::parse($a->logon_time)->diffInMinutes($a->last_updated); + + $atcClient->disconnected_at = null; + $atcClient->frequency = $a->frequency; + $atcClient->qualification_id = $a->rating; + $atcClient->facility_type = $a->facility; + $atcClient->save(); + } +} diff --git a/app/Jobs/Statistic/ClearStatistics.php b/app/Jobs/Statistic/ClearStatistics.php new file mode 100644 index 0000000..732bd54 --- /dev/null +++ b/app/Jobs/Statistic/ClearStatistics.php @@ -0,0 +1,59 @@ +info('[Statistics]::Clearing DB'); + //$this->_handleAtcClients(); + //$this->_handlePilotClients(); + //\Log::channel('jobdaily')->info('[Statistics]::Cleared DB'); + } + + private function _handleAtcClients() + { + // TODO: Find whats needed to be checked here and then clear stats accordingly + AtcData::query() + ->whereNotNull('disconnected_at') //the client has disconnected + ->where('minutes_online', '<', 5)->delete(); + } + + private function _handlePilotClients() + { + FlightData::query() + ->whereNotNull('disconnected_at') //the client has disconnected + ->where('minutes_online', '<', 5)->delete(); + + FlightData::query() + ->whereNotNull('disconnected_at') //the client has disconnected + ->where('arrival_airport', 'LIKE', '') + ->where('departure_airport', 'LIKE', '')->delete(); + } +} diff --git a/app/Jobs/Statistic/UpdateStatistics.php b/app/Jobs/Statistic/UpdateStatistics.php new file mode 100644 index 0000000..2a12693 --- /dev/null +++ b/app/Jobs/Statistic/UpdateStatistics.php @@ -0,0 +1,154 @@ +info('[Statistics]::Initializing updater'); + $this->_timestamp = \Carbon\Carbon::now()->utc(); + //$this->_atcClients = \App\Models\Network\AtcClient::all(); + //$this->_pilotClients = \App\Models\Network\PilotClient::all(); + } + + /** + * Update ATC statistics table and remove client from live table if session is completed + * @return void + */ + private function _handleAtcClients() + { + foreach (\App\Models\Network\AtcClient::query()->limit(500)->cursor() as $atc) { + // If a session is completed, move it to the statistics table + if(!is_null($atc->disconnected_at)) { + // Okay, session completed. + // Let's find any session in the statistics that might be related to this. + $related = \App\Models\Statistic\AtcData::where('account_id', $atc->account_id) + ->whereBetween('connected_at', [$atc->connected_at->subMinutes(5), $this->_timestamp]) + ->where('callsign', $atc->callsign) + ->first(); + if($related) { + // Might have a related session here. + // Let's just update the and disregard the new one + $related->disconnected_at = $atc->disconnected_at; + $related->minutes_online = \Carbon\Carbon::parse($related->connected_at)->diffInMinutes($related->disconnected_at, true); + $related->save(); + $atc->delete(); + continue; + } else { + // No related session found. + // Copy this one over + $atcStat = new \App\Models\Statistic\AtcData($atc->toArray()); + $atcStat->save(); + $atc->delete(); + continue; + } + } else { + // Clean up + if($this->_timestamp->diffInMinutes($atc->connected_at) > (14 * 60)) { + $atc->delete(); // 14hrs of atc? Clean that garbage + // If the client is still online it will be readded with the next cycle + } + } + } + } + + /** + * Update Pilot statistics table and remove completed flights from live table + * @return [type] [description] + */ + private function _handlePilotClients() + { + foreach (\App\Models\Network\PilotClient::query()->limit(5000)->cursor() as $pilot) { + if(!is_null($pilot->disconnected_at)) { + // Completed flight. + // Find a related flight by the account within a given timeframe with same route + $related = \App\Models\Statistic\FlightData::where('account_id', $pilot->account_id) + ->whereBetween('connected_at', [$pilot->connected_at->subMinutes(60), $this->_timestamp]) + ->where('callsign', $pilot->callsign) + ->where('departure_airport', $pilot->departure_airport) + ->where('arrival_airport', $pilot->arrival_airport) + ->first(); + if($related != null) { + $related->flight_type = $pilot->flight_type; + $related->route = $pilot->route; + $related->remarks = $pilot->remarks; + //$related->current_latitude = $pilot->current_latitude; + //$related->current_longitude = $pilot->current_longitude; + //$related->current_altitude = $pilot->current_altitude; + //$related->current_groundspeed = $pilot->current_groundspeed; + //$related->current_heading = $pilot->current_heading; + if (is_null($related->departed_at)){ + $related->departed_at = $pilot->departed_at; + } + $related->arrived_at = $pilot->arrived_at; + + $related->disconnected_at = $pilot->disconnected_at; + $related->minutes_online = \Carbon\Carbon::parse($related->connected_at)->diffInMinutes($related->disconnected_at, true); + $related->save(); + //\Log::info($pilot->callsign . ' deleted due to related flight.'); + $pilot->delete(); + continue; + } else { + // Finished flight + // Copy to stats + $pilotStat = new \App\Models\Statistic\FlightData($pilot->toArray()); + + $pilotStat->departed_at = $pilot->departed_at; + $pilotStat->arrived_at = $pilot->arrived_at; + + if (!is_null($pilotStat->departed_at) && !is_null($pilotStat->arrived_at)) { + //$pilotStat->minutes_inair = \Carbon\Carbon::parse($pilot->departed_at)->diffInMinutes($pilot->arrived_at, true); + } + $pilotStat->save(); + //\Log::info($pilot->callsign . ' deleted due to completed flight cycle.'); + $pilot->delete(); + continue; + } + } else { + // Clean up forgotten flights + if($this->_timestamp->diffInMinutes($pilot->connected_at) > (24 * 60)) { + //\Log::info($pilot->callsign . ' deleted due to exessive online period.'); + $pilot->delete(); // If the flight has not been completed within this timeframe, remove it + + // If the client is still online it will be readded with the next cycle + } + } + } + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + \Log::channel('jobdaily')->info('[Statistics]::Updating DB'); + $this->_handleAtcClients(); + + $this->_handlePilotClients(); + \Log::channel('jobdaily')->info('[Statistics]::Updated DB'); + + //\App\Jobs\Statistic\ClearStatistics::dispatch(); // disable for now + } +} diff --git a/app/Jobs/Teamspeak/UpdateViaWebQuery.php b/app/Jobs/Teamspeak/UpdateViaWebQuery.php new file mode 100644 index 0000000..9919766 --- /dev/null +++ b/app/Jobs/Teamspeak/UpdateViaWebQuery.php @@ -0,0 +1,60 @@ +_lastUpdatedListNr = Cache::get('teamspeak.updater.lastUpdatedListNr'); + } else { + $this->_lastUpdatedListNr = 0; + } + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + \Log::channel('jobdaily')->info('[TS]::Starting update. First ID ' . $this->_lastUpdatedListNr); + + for ($i = 0; $i < $this->_chunckNumber; $i++) { + $this->_clientDBlist = TeamSpeakWebquery::getClientDB($this->_lastUpdatedListNr); + if (count($this->_clientDBlist) == 0) { + Cache::put('teamspeak.updater.lastUpdatedListNr', 0); + \Log::info('[TS]::Finished update. Checked ' . $this->_lastUpdatedListNr); + $this->_lastUpdatedListNr = 0; + return; + } + foreach ($this->_clientDBlist as $client) { + TeamSpeakWebquery::checkClient($client); + $this->_lastUpdatedListNr++; + } + } + Cache::put('teamspeak.updater.lastUpdatedListNr', $this->_lastUpdatedListNr); + + \Log::channel('jobdaily')->info('[TS]::Stopped update. Last ID ' . ($this->_lastUpdatedListNr - 1)); + } +} diff --git a/app/Libraries/Gitlab.php b/app/Libraries/Gitlab.php new file mode 100644 index 0000000..a5a4ebf --- /dev/null +++ b/app/Libraries/Gitlab.php @@ -0,0 +1,188 @@ +client = new Client([ + 'base_uri' => $uri, + 'connect_timeout' => 30, + 'read_timeout' => 30, + 'timeout'=> 30, + 'headers' => [ + 'Authorization' => "Bearer {$access_token}", + ], + ]); + } + + + public function createAccount(Account $account) : bool + { + return false; + if($account->setting->gitlab_id != null) return false; //already has gitlab account + $res = $this->client->request('POST', 'users', ['form_params' => + [ + 'username' => $account->id, + 'name' => $account->username, + 'email' => $account->email, + 'reset_password' => true, + ], + ]); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299) return false; + $json = json_decode($res->getBody()->getContents()); + /*$createUserResponse = '{"id":75,"username":"10000001","name":"Web One","state":"active","avatar_url":"https://secure.gravatar.com/avatar/5bf979420dd45b7c9b1ad403fe051cae?s=80\u0026d=identicon","web_url":"https://git.vatsim-germany.org/10000001","created_at":"2022-03-16T12:55:35.202Z","bio":"","location":null,"public_email":null,"skype":"","linkedin":"","twitter":"","website_url":"","organization":null,"job_title":"","pronouns":null,"bot":false,"work_information":null,"followers":0,"following":0,"local_time":null,"last_sign_in_at":null,"confirmed_at":null,"last_activity_on":null,"email":"auth.dev1@vatsim.net","theme_id":1,"color_scheme_id":1,"projects_limit":100,"current_sign_in_at":null,"identities":[],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":false,"commit_email":"auth.dev1@vatsim.net","is_admin":false,"note":null}';*/ + $account->setting->gitlab_id = $json->id; + $account->setting->save(); + return true; + } + + public function checkNAVAssignments(Account $account) : void + { + return; + if($account->setting->gitlab_id == null) return; //user has no account + //now this will not be good, but it will work until V3 :) + $rgs = Regionalgroup::all(); + $mapping = [ + 2 => 119, //edbb + 1 => 116, //edww + 3 => 115, //edll + 4 => 114, //edff + 5 => 117, //edmm + // non-existing rg id to remove members + 102 => 92, //edbb old group + 101 => 93, //edww old group + 103 => 94, //edll old group + 104 => 95, //edff old group + 105 => 96, //edmm old group + ]; + foreach ($rgs as $rg) { + $rg_gitlab_group = $mapping[$rg->id]; + if($rg->navigators()->wherePivot('account_id', $account->id)->count()){ + try { + $this->assignToGroup($account, $rg_gitlab_group); + } catch (\Exception $e){} + } else { + try { + $this->removeFromGroup($account, $rg_gitlab_group); + } catch (\Exception $e){} + } + } + } + + //$gitlab_access_level: No access (0), Minimal access (5), Guest (10), Reporter (20), Developer (30), Maintainer (40), Owner (50) + public function assignToGroup(Account $account, int $gitlab_group_id, int $gitlab_access_level = 30) : bool + { + return false; + if($account->setting->gitlab_id == null) return false; //has no gitlab account + $res = $this->client->request('POST', "groups/{$gitlab_group_id}/members", ['form_params' => + [ + 'user_id' => $account->setting->gitlab_id, + 'access_level' => $gitlab_access_level, + ], + ]); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299) return false; + return true; + } + + public function removeFromGroup(Account $account, int $gitlab_group_id) : bool + { + return false; + if($account->setting->gitlab_id == null) return false; //has no gitlab account + $res = $this->client->request('DELETE', "groups/{$gitlab_group_id}/members/{$account->setting->gitlab_id}"); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299) return false; + return true; + } + + public function getDocumentInfo(int $repository_id, string $document_path, string $branch_name="main") + { + return false; + $document_path = urlencode($document_path); + $branch_name = urlencode($branch_name); + $body_contents = \Cache::remember("gitlab.fileinfo.repo_{$repository_id}.{$document_path}.branch_{$branch_name}", 60, function () use ($repository_id, $document_path, $branch_name ){ + $res = $this->client->request('GET', "projects/{$repository_id}/repository/files/{$document_path}", ['form_params' => + [ + 'ref' => $branch_name, + ], + ]); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299) return false; + return $res->getBody()->getContents(); + }); + if($body_contents == false) abort(404); + return json_decode($body_contents); + } + + public function saveDocumentRaw(int $repository_id, string $document_path, string $branch_name="main") + { + return false; + $document_path = urlencode($document_path); + $branch_name = urlencode($branch_name); + $path = \Cache::remember("gitlab.fileurl.repo_{$repository_id}.{$document_path}.branch_{$branch_name}", 60, function () use ($repository_id, $document_path, $branch_name ){ + $res = $this->client->request('GET', "projects/{$repository_id}/repository/files/{$document_path}/raw", ['form_params' => + [ + 'ref' => $branch_name, + ], + ]); + $documentInfo = $this->getDocumentInfo($repository_id, $document_path, $branch_name); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299 || $documentInfo == false) return false; + + $contents = $res->getBody()->getContents(); + $storage_path = "public/gitlab/repo_{$repository_id}/{$documentInfo->file_path}"; + if(!Storage::put($storage_path, $contents)) return false; + return $storage_path; + }); + return $path; + } + + public function generateChartLink(Chart $chart) + { + return false; + if(!$chart->getIsGitlabAttribute()) return false; + if(!$chart->public_available) return false; //non-public access is not implemented yet + try { + $string = $chart->href; //gitlab:edff_public/path/to/file.pdf + $pos = strpos($string, 'gitlab:'); if($pos===false) return false; + $string = substr_replace($string, '', $pos, strlen('gitlab:')); + $pos = strpos($string, '/'); if($pos===false) return false; + $prefix = substr($string,0,$pos); //edff_public + $pos = strpos($string, $prefix); if($pos===false) return false; + $path_to_file = substr_replace($string, '', $pos, strlen($prefix)+1); // path/to/file.pdf + //this again will be shady: prefix -> gitlab repo + $mappings=[ + 'edbb_public' => 57, + 'edww_public' => 58, + 'edll_public' => 59, + 'edff_public' => 60, + 'edmm_public' => 61, + ]; + $repo_id = $mappings[$prefix]; + $storage_path = $this->saveDocumentRaw($repo_id, $path_to_file); + return Storage::url($storage_path); + } catch (\Exception $e) {} + return false; + } + + + + + + /*public function ddProjects() + { + $res = $this->client->request('GET', 'projects'); + if ($res->getStatusCode()< 200 || $res->getStatusCode() > 299) return false; + $json = json_decode($res->getBody()->getContents()); + dd($json); + }*/ +} diff --git a/app/Libraries/TeamSpeak.php b/app/Libraries/TeamSpeak.php new file mode 100644 index 0000000..df53c81 --- /dev/null +++ b/app/Libraries/TeamSpeak.php @@ -0,0 +1,246 @@ +_instance = TeamSpeak3::factory($tsUrl); + + } + + /** + * Gets an instance of the serverquery connection + * + * @return [type] [description] + */ + public function getInstance() + { + return $this->_instance; + } + + /** + * Is a given virtual server available? + * + * @return [type] [description] + */ + public function getStatus() + { + return $this->_instance->getProperty('virtualserver_status') == 'online'; + } + + /** + * Does a client has been registered properly? + * + * @param TeamSpeak3_Node_Client $client [description] + * @param integer $attempt [description] + * @return [type] [description] + */ + public static function checkClient(TeamSpeak3_Node_Client $client, $attempt = 0) + { + $registration = self::getActiveRegistration($client); + if(!is_null($registration)) { + self::updateClientInfo($client, $registration); + + return $registration->account; + } + $registration = self::getNewRegistration($client); + if(is_null($registration)) { + if($attempt > 3) { + self::kickClient($client, 'Bitte auf der Website registrieren!'); + return false; + } else { + self::pokeClient($client, 'Bitte auf der Website registrieren!'); + self::messageClient($client, 'Keine gültige Registrierung gefunden. Bitte auf der Homepage registrieren. Warnung '.($attempt + 1).'/3'); + // awaiting manual ID input from Membership Settings + sleep(30); + + return self::checkRegistration($client, ++$attempt); + } + } + self::completeNewRegistration($client, $registration); + return $registration->account; + } + + /** + * Grab the current registration from the database. + * + * @param TeamSpeak3_Node_Client $client The current client node + * + * @return Registration + */ + protected static function getActiveRegistration(TeamSpeak3_Node_Client $client) + { + return Registration::where('uid', $client['client_unique_identifier']) + ->where('dbid', $client['client_database_id']) + ->first(); + } + + /** + * Try to find a not completed registration process + * Then find out if it matches to the client. + * + * @param TeamSpeak3_Node_Client $client [description] + * + * @return mixed|Registration|null [description] + */ + protected static function getNewRegistration(TeamSpeak3_Node_Client $client) + { + // Try some magic + try { + $customInfo = $client->customInfo(); + } catch (TeamSpeak3_Adapter_ServerQuery_Exception $e) { + if (1281 !== $e->getCode()) { // Database Empty Result Set + throw $e; + } else { + return; + } + } + // Find the set registration_id in the custom info + // and if found grab the registration + foreach ($customInfo as $info) { + if ('registration_id' == $info['ident']) { + $reg = Registration::where('id', $info['value'])->whereNull('dbid')->first(); + + return $reg; + } + } + } + + /** + * Update an incomplete registration. + * + * @param TeamSpeak3_Node_Client $client [description] + * @param Registration $registration [description] + */ + protected static function completeNewRegistration(TeamSpeak3_Node_Client $client, Registration $registration) + { + if ($registration->confirmation) { + $registration->confirmation->delete(); + } + $registration->uid = $client['client_unique_identifier']; + $registration->dbid = $client['client_database_id']; + $registration->save(); + + event(new \App\Events\TeamSpeak\RegistrationCompletedEvent($registration->account, true)); + + self::updateClientInfo($client, $registration); + } + + /** + * Update client information. + * + * @param TeamSpeak3_Node_Client $client [description] + * @param Registration $registration [description] + */ + protected static function updateClientInfo(TeamSpeak3_Node_Client $client, Registration $registration) + { + $registration->last_login = \Carbon\Carbon::now(); + $registration->last_ip = $client['connection_client_ip']; + $registration->last_os = $client['client_platform']; + $registration->save(); + } + + /** + * Does the description of a client match the attributes from cert? + * + * @param TeamSpeak3_Node_Client $client [description] + * @param Account $account [description] + * + * @return TeamSpeak3_Node_Client + */ + public static function clientDescription(TeamSpeak3_Node_Client $client, Account $account) + { + $description = $account->username.' ('.$account->id.')'; + if ($client->infoDb()['client_description'] !== $description) { + $client->modify(['client_description' => $description]); + $client->getParent()->clientListReset(); + $client = $client->getParent()->clientGetById($client->getId()); + } + + return $client; + } + + /** + * Send a private text message to the client. + * + * @param TeamSpeak3_Node_Client $client [description] + * @param string $message [description] + * + * @return [type] [description] + */ + public static function messageClient(TeamSpeak3_Node_Client $client, $message) + { + $client->message($message); + } + + /** + * Send a poke message to a client. + * + * @param TeamSpeak3_Node_Client $client [description] + * @param string $message [description] + * + * @return [type] [description] + */ + public static function pokeClient(TeamSpeak3_Node_Client $client, $message) + { + $client->poke($message); + } + + /** + * Kicks a client from the server. + * + * @param TeamSpeak3_Node_Client $client [description] + * @param string $message [description] + * + * @return [type] [description] + */ + public static function kickClient(TeamSpeak3_Node_Client $client, $message) + { + $client->kick(TeamSpeak3::KICK_SERVER, $message); + } + + /** + * Bans the client. + * + * @param TeamSpeak3_Node_Client $client + * @param string $reason + * @param int $duration Duration in seconds. + */ + public static function banClient(TeamSpeak3_Node_Client $client, $reason, $duration) + { + self::pokeClient($client, $reason); + $client->ban($duration, $reason); + } + +} diff --git a/app/Libraries/TeamSpeakWebquery.php b/app/Libraries/TeamSpeakWebquery.php new file mode 100644 index 0000000..91cc8ca --- /dev/null +++ b/app/Libraries/TeamSpeakWebquery.php @@ -0,0 +1,504 @@ +cldbid; + + $registration = new Registration(); + $registration->account_id = $account->id; + $registration->registration_ip = $registration_ip; + $registration->uid = $uid; + $registration->dbid = $clientDBid; + + $servergroupId = self::getServergroupId( + config("teamspeak.default_group") + ); + + $description = $account->username . " (" . $account->id . ")"; + if (self::_clientdbedit($clientDBid, $description) == false) { + return false; + } + if (self::_servergroupaddclient($clientDBid, $servergroupId) == false) { + return false; + } + $registration->save(); + return true; + } + + /** + * removeTSRegistation + * + * @param Registration $registration + * @return boolean + */ + public static function removeTSRegistation(Registration $registration) + { + $servergroupId = self::getServergroupId( + config("teamspeak.default_group") + ); + $clientDBid = $registration->dbid; + + if (self::_servergroupdelclient($clientDBid, $servergroupId) == false) { + return false; + } + + self::_clientdbedit($clientDBid, ""); + + $registration->delete(); + return true; + } + + /** + * checkClientDB + * + * @return void + */ + /* + public static function checkClientDB() + { + $clients = self::_clientdblist(); + if ($clients == false) { + return; + } + + $servergroupId = self::getServergroupId(config('teamspeak.default_group')); + foreach ($clients as $client) { + + } + } + */ + + public static function checkClient($client) + { + $servergroupId = self::getServergroupId( + config("teamspeak.default_group") + ); + $registration = Registration::where( + "uid", + $client->client_unique_identifier + ) + ->where("dbid", $client->cldbid) + ->first(); + if ($registration == null) { + // client has no Registration + self::_servergroupdelclient($client->cldbid, $servergroupId); + return; + } + self::checkRegistration($registration, $client); + } + + public static function getClientDB($startNr = 0) + { + $clients = self::_clientdblist($startNr); + if ($clients == false) { + return []; + } + return $clients; + } + + /** + * checkAccount + * + * @param Account $account + * @return void + */ + public static function checkAccount(Account $account) + { + $account->loadMissing("data"); + $has_active_ban_vatger = $account->getIsCurrentlyTSBannedAttribute(); + $has_active_ban_vatsim = $account->data->rating_atc == 0; + + $has_active_ban = $has_active_ban_vatger || $has_active_ban_vatsim; + + $registrations = Registration::where("account_id", $account->id)->get(); + foreach ($registrations as $registration) { + $existingTSBans = self::getBansFromRegistration($registration); + if ($has_active_ban_vatger && empty($existingTSBans)) { + $ban = $account->currentBan; + self::_banadd( + $registration->uid, + Carbon::now()->diffInSeconds($ban->banned_till), + "[Account " . $account->id . "]" . $ban->reason + ); + } else { + if ($has_active_ban_vatsim && empty($existingTSBans)) { + self::_banadd( + $registration->uid, + 0, + "[Account " . $account->id . "]" . " Account suspended" + ); + } + } + + + + if ($has_active_ban == false && !empty($existingTSBans)) { + foreach ($existingTSBans as $ban) { + self::_bandel($ban->banid); + } + } + } + } + + /** + * getBansFromRegistration + * + * @param Registration $registration + * @return mixed + */ + public static function getBansFromRegistration(Registration $registration) + { + $allbans = self::_banlist(); + $registrationbans = []; + if ($allbans == false) { + return $registrationbans; + } + + foreach ($allbans as $ban) { + if (strcmp($ban->uid, $registration->uid) == 0) { + $registrationbans[] = $ban; + } + } + return $registrationbans; + } + + /** + * checkRegistration + * + * @param Registration $registration + * @param stdClass $client + * @return void + */ + public static function checkRegistration( + Registration $registration, + $client + ) { + $registration->last_login = Carbon::createFromTimestamp( + $client->client_lastconnected + ); + $registration->last_ip = $client->client_lastip; + $registration->save(); + $account = $registration->account; + if ($account == null) { + return; + } + $description = $account->username . " (" . $account->id . ")"; + if (strcmp($client->client_description, $description) != 0) { + self::_clientdbedit($client->cldbid, $description); + } + self::checkAccount($account); + } + + /** + * getServergroupId + * + * @param string $name + * @return mixed + */ + public static function getServergroupId($name) + { + return Cache::remember( + "teamspeak.servergroupid." . $name, + 60, + function () use ($name) { + $list = self::_servergrouplist(); + if ($list == false) { + return false; + } + foreach ($list as $group) { + if ( + strcmp($group->name, $name) == 0 && + strcmp($group->type, 1) == 0 + ) { + return $group->sgid; + } + } + return false; + } + ); + } + + /** + * _sendWebQuery + * + * @param string $command + * @param array $queryparams + * @return mixed + */ + public static function _sendWebQuery($command, $queryparams = []) + { + $uri = + "http://" . + config("teamspeak.host") . + ":" . + config("teamspeak.webquery_port") . + "/" . + config("teamspeak.server_number") . + "/"; + + $_httpClient = new Client([ + "base_uri" => $uri, + "connect_timeout" => 10, + "read_timeout" => 15, + "timeout" => 30, + ]); + + $queryparams["api-key"] = config("teamspeak.apikey"); + $params = [ + "query" => $queryparams, + ]; + $response = null; + try { + $response = $_httpClient->get($command, $params); + } catch (GuzzleException $e) { + // code 4xx-5xx + Log::channel("joberror")->error( + "[TS] Webquery GuzzleException (Code " . + $e->getCode() . + "): " . + $e->getMessage() + ); + return false; + } + if ($response == null || $response->getStatusCode() != 200) { + return false; + } + $body = json_decode($response->getBody()); + + if (empty($body->status) || $body->status->message != "ok") { + return false; + } + if (empty($body->body)) { + return true; + } + + return $body->body; + } + + //------------------ DIRECT API FUNCTIONS ------------------ + + /** + * _servergroupaddclient + * + * @param int $clientDBid + * @param int $servergroupId + * @return boolean + */ + private static function _servergroupaddclient($clientDBid, $servergroupId) + { + return self::_sendWebQuery("servergroupaddclient", [ + "cldbid" => $clientDBid, + "sgid" => $servergroupId, + ]); + } + + /** + * _servergroupdelclient + * + * @param int $clientDBid + * @param int $servergroupId + * @return boolean + */ + private static function _servergroupdelclient($clientDBid, $servergroupId) + { + return self::_sendWebQuery("servergroupdelclient", [ + "cldbid" => $clientDBid, + "sgid" => $servergroupId, + ]); + } + + /** + * _servergrouplist + * + * @return mixed + */ + private static function _servergrouplist() + { + return Cache::remember("teamspeak.servergrouplist", 120, function () { + return self::_sendWebQuery("servergrouplist"); + }); + } + + /** + * _clientdbedit + * + * @param int $clientDBid + * @param string $client_description + * @return void + */ + private static function _clientdbedit($clientDBid, $client_description = "") + { + return self::_sendWebQuery("clientdbedit", [ + "cldbid" => $clientDBid, + "client_description" => $client_description, + ]); + } + + /** + * _clientdblist + * + * @return mixed + */ + private static function _clientdblist($start = 0) + { + return Cache::remember( + "teamspeak.clientdblist." . $start, + 30, + function () use ($start) { + return self::_sendWebQuery("clientdblist", ["start" => $start]); + } + ); + } + + /** + * _clientgetdbidfromuid + * + * @param string $uid + * @return mixed + */ + private static function _clientgetdbidfromuid($uid) + { + return self::_sendWebQuery("clientgetdbidfromuid", ["cluid" => $uid]); + } + + /** + * _banlist + * + * @return mixed + */ + private static function _banlist() + { + return Cache::remember("teamspeak.banlist", 120, function () { + return self::_sendWebQuery("banlist"); + }); + } + + /** + * _banadd + * + * @param string $uid + * @param int $time + * @param string $text + * @return boolean + */ + private static function _banadd($uid, $time, $text = "Banned!") + { + Cache::forget("teamspeak.banlist"); + return self::_sendWebQuery("banadd", [ + "uid" => $uid, + "time" => $time, + "text" => $text, + ]); + } + + /** + * _bandel + * + * @param int $banID + * @return boolean + */ + private static function _bandel($banID) + { + Cache::forget("teamspeak.banlist"); + return self::_sendWebQuery("bandel", [ + "banid" => $banID, + ]); + } + + /** + * _privilegekeylist + * + * @return mixed + */ + private static function _privilegekeylist() + { + return Cache::remember("teamspeak.privilegekeylist", 120, function () { + return self::_sendWebQuery("privilegekeylist"); + }); + } + + /** + * _privilegekeyadd + * + * @param int $tokentype + * @param int $groupID + * @param int $channelID + * @param string $description + * @param array $tokencustomset + * @return mixed + */ + private static function _privilegekeyadd( + $tokentype, + $groupID, + $channelID, + $description = "", + $tokencustomset = [] + ) { + Cache::forget("teamspeak.privilegekeylist"); + return self::_sendWebQuery( + "privilegekeyadd", + ["tokentype" => $tokentype], + ["tokenid1" => $groupID], + ["tokenid2" => $channelID], + ["tokendescription" => $description], + ["tokencustomset" => $tokencustomset] + ); + } + + /** + * _privilegekeydelete + * + * @param string $token + * @return boolean + */ + private static function _privilegekeydelete($token) + { + Cache::forget("teamspeak.privilegekeylist"); + return self::_sendWebQuery("privilegekeydelete", ["token" => $token]); + } +} diff --git a/app/Libraries/XenBridge.php b/app/Libraries/XenBridge.php new file mode 100644 index 0000000..a8deee5 --- /dev/null +++ b/app/Libraries/XenBridge.php @@ -0,0 +1,585 @@ + "application/json", + "XF-Api-Key" => config("forum.apikey"), + "XP-Api-User" => 1, + ]; + $data["api_bypass_permissions"] = 1; + $client = new Client(); + try { + $response = $client->get( + config("forum.url") . "/api/" . $endpoint . '?api_bypass_permissions=1' , + $data + ); + + return $response; + } catch (GuzzleException $e) { + Log::debug($e->getMessage()); + return false; + } + } + + /** + * Send a post request to a XenForo Api. + * + * @param [type] $endpoint [description] + * @param [type] $formData [description] + * + * @return [type] [description] + */ + private static function _sendAPIPostCommand($endpoint, $formData) + { + $data["headers"] = [ + "Accept" => "application/json", + "XF-Api-Key" => config("forum.apikey"), + "XF-Api-User" => 1, + ]; + $data["api_bypass_permissions"] = 1; + $data["form_params"] = $formData; + $client = new Client(); + try { + $response = $client->post( + config("forum.url") . "/api/" . $endpoint, + $data + ); + + return $response; + } catch (ClientException $e) { + Log::debug($e->getMessage()); + return false; + } + } + + /** + * Send an update request to a XenForo API. + * + * @param [type] $endpoint [description] + * @param [type] $formData [description] + * + * @return [type] [description] + */ + private static function _sendAPIPatchCommand($endpoint, $formData) + { + $data["headers"] = [ + "Accept" => "application/json", + "XF-Api-Key" => config("forum.apikey"), + "XF-Api-User" => 1, + ]; + $data["api_bypass_permissions"] = 1; + $data["form_params"] = $formData; + $client = new Client(); + try { + $response = $client->put( + config("forum.url") . "/api/" . $endpoint, + $data + ); + + return $response; + } catch (ClientException $e) { + Log::debug($e->getMessage()); + return false; + } + } + + /** + * Send a delete request to XenForo Api. + * + * @param [type] $endpoint [description] + * @param [type] $formData [description] + * + * @return [type] [description] + */ + private static function _sendAPIDeleteCommand($endpoint, $formData) + { + $data["headers"] = [ + "Accept" => "application/json", + "XF-Api-Key" => config("forum.apikey"), + "XF-Api-User" => 1, + ]; + $data["api_bypass_permissions"] = 1; + $data["form_params"] = $formData; + $client = new Client(); + try { + $response = $client->delete( + config("forum.url") . "/api/" . $endpoint, + $data + ); + + return $response; + } catch (ClientException $e) { + Log::debug($e->getMessage()); + return false; + } + } + + /** + * A function to create an Account for the XenForo Application via API call. + * + * @param Account $account [description] + * @param string $password [description] + * + * @return bool + */ + public static function createForumAccount( + Account $account, + $password, + $try = 0 + ) + { + if (null != $account->setting->forum_id) { + return false; + } + // Build the data we need from the existing user object + $dataArray = []; + // Set all options we need as defaults + $dataArray["option"] = [ + "content_show_signature" => true, + "email_on_conversation" => true, + "push_on_conversation" => true, + "receive_admin_email" => true, + "show_dob_year" => false, + "show_dob_date" => false, + "is_discouraged" => false, + ]; + // Profile specific Account-Data + $dataArray["profile"] = [ + "location" => "", + "website" => "", + "about" => "", + "signature" => "", + ]; + $dataArray["visible"] = true; + $dataArray["activity_visible"] = true; + $dataArray["timezome"] = "Europe/Berlin"; + $dataArray["custom_title"] = $account->id; + + // Is nothing else than $account->firstname.' '.$account->lastname + if (0 == $try) { + $dataArray["username"] = $account->username; + } else { + $dataArray["username"] = $account->username . " " . $try; + } + $dataArray["email"] = $account->email; + // Set Default Usergroup + $dataArray["user_group_id"] = config("forum.defaultGroup"); // Default Registered User Group + $dataArray["is_staff"] = false; + + $dataArray["password"] = $password; + + $dataArray["custom_fields"] = [ + "vatsimid" => $account->id, + ]; + + $result = self::_sendAPIPostCommand("users", $dataArray); + if ($result && 200 == $result->getStatusCode()) { + $body = $result->getBody()->getContents(); + $forumUserObject = json_decode($body); + $accSetting = $account->setting; + $accSetting->forum_id = $forumUserObject->user->user_id; + $accSetting->save(); + + return true; + } else { + ++$try; + if ($try <= 99) { + return self::createForumAccount($account, $password, $try); + } + } + + return false; + } + + /** + * Update an account on the forum. + * + * @param Account $account [description] + * + * @return [type] [description] + */ + public static function updateForumAccount(Account $account) + { + $account->loadMissing("data", "groups", "regionalgroups"); + + if (null == $account->setting) { + return false; + } + + if (null == $account->setting->forum_id) { + return false; + } + + if ( + $account->data->rating_atc == 0 || + $account->is_currently_forum_banned + ) { + self::banForumAccount($account); + } + + $dataArray = []; + /** + * Issue 133. + * Update username when cert data was updated. + * + * 1. Get current user object from the board + * 2. Compare the names + * 3. If mismatch: set new name + */ + $result = self::_sendAPICommand( + "users/" . $account->setting->forum_id, + [] + ); + + if (!$result) { + return false; + } + + $response = json_decode($result->getBody()->getContents()); + $forumUserObject = $response->user; + + if ( + $forumUserObject && + trim(preg_replace("/[0-9]+/", "", $forumUserObject->username)) != + $account->username + ) { + $dataArray["username"] = $account->username; + } + + /** + * Array to store forum groups that must be assigned to the user. + * @var array + */ + $secondaryGroups = []; + + // Get all forumgroups the user has through assigned groups + foreach ($account->groups as $grp) { + $grp->loadMissing("forumgroups"); + foreach ($grp->forumgroups as $fg) { + if (!array_key_exists($fg->forum_id, $secondaryGroups)) { + $secondaryGroups[] = $fg->forum_id; + } + } + } + + /** + * Assign forum groups based upon regionalgroup status + * + */ + $fgrps = ForumGroup::all(); + foreach ($account->regionalgroups as $rg) { + if ( + $rg->chief_id == $account->id || + $rg->deputy_id == $account->id + ) { + // Account is staff team + if ($fgrps->contains($rg->staff_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->staff_group_id + )->forum_id; + } + } + + if ($account->isMentorOfRegionalgroup($rg)) { + // Mentoring group + if ($fgrps->contains($rg->mentor_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->mentor_group_id + )->forum_id; + } + } + + if ($account->isNavigatorOfRegionalgroup($rg)) { + // Nav group + if ($fgrps->contains($rg->navler_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->navler_group_id + )->forum_id; + } + } + + if ($account->isEventlerOfRegionalgroup($rg)) { + // Eventteam group + if ($fgrps->contains($rg->eventler_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->eventler_group_id + )->forum_id; + } + } + + if ($account->isMemberOfRegionalgroup($rg)) { + // Fullmemeber, give acces to membership group + if ($fgrps->contains($rg->member_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->member_group_id + )->forum_id; + } + // Fullmember, give access to the voting group + // But only if at least 2 month non stop membership + if ( + $rg->pivot->created_at <= Carbon::now()->subMonth(2) && + $fgrps->contains($rg->voting_group_id) + ) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->voting_group_id + )->forum_id; + } + // If you are assigend to a regionalgroup you are eligable to vote as a vacc member + // But only if you are a member for at least 2 month + if ($rg->pivot->created_at <= Carbon::now()->subMonth(2)) { + $secondaryGroups[] = 44; + } // vACC-Forum voting group id => 44; + } + + if ($account->isGuestOfRegionalgroup($rg)) { + if ($fgrps->contains($rg->guest_group_id)) { + $secondaryGroups[] = $fgrps->firstWhere( + "id", + $rg->guest_group_id + )->forum_id; + } + } + } + + // OBSOLETE: NOT THE RIGHT WAY OF DOING THIS + // Determine if the user is allowed to participate in vACC wide votings. + // vACC-Forum voting group id => 44; + // if ($account->created_at <= \Carbon\Carbon::now()->subMonth(2)) { + // if ('GER' == $account->data->subdivision_code) { + // // Definitly member + // $secondaryGroups[] = 44; + // } elseif ('EUD' == $account->data->division_code && 'EUR' == $account->region_code && null == $account->data->subdivision_code) { + // // They might be pilots that do not have set their subdivision + // // TODO: find a better way of checking this. Maybe force any real vACC member to set their subdivision to GER + // // The current way does also include non-vACC members that are within the european region. + // $secondaryGroups[] = 44; + // } + // } + + $dataArray["secondary_group_ids"] = []; + if (!empty($secondaryGroups)) { + $dataArray["secondary_group_ids"] = $secondaryGroups; + } else { + $dataArray["secondary_group_ids"][] = config("forum.guestGroup"); + } + + $result = self::_sendAPIPostCommand( + "users/" . $account->setting->forum_id, + $dataArray + ); + if (!$result) { + return false; + } + $response = json_decode($result->getBody()->getContents()); + if ($response->success) { + // $account->notify(new \App\Notifications\Forum\AccountUpdatedNotification($secondaryGroups)); + + // activity() + // ->causedBy($account) + // ->performedOn($account) + // ->log("Der Forenaccount wurde entsprechend der neusten Daten aktualisiert!"); + + return true; + } + + return false; + } + + /** + * Sets a user discouraged and assigns the suspended group. + * + * @param Account $account [description] + * @return [type] [description] + */ + public static function banForumAccount(Account $account) + { + if (null == $account->setting) { + return false; + } + + if (null == $account->setting->forum_id) { + return false; + } + + + $suspendedGroup = config("forum.suspendedGroup"); + + // Build the data we need from the existing user object + $dataArray = []; + // Set all options we need as defaults + $dataArray["option"] = [ + "is_discouraged" => false, + ]; + $dataArray["secondary_group_ids"] = []; + $dataArray["secondary_group_ids"][] = $suspendedGroup; + + $result = self::_sendAPIPostCommand( + "users/" . $account->setting->forum_id, + $dataArray + ); + if (!$result) { + return false; + } + $response = json_decode($result->getBody()->getContents()); + if ($response->success) { + // $account->notify(new \App\Notifications\Forum\AccountUpdatedNotification($secondaryGroups)); + + // activity() + // ->causedBy($account) + // ->performedOn($account) + // ->log("Der Forenaccount wurde entsprechend der neusten Daten gesperrt!"); + + return true; + } + + return false; + } + + /** + * Sends a private message to a user in the forum + * @param [type] $account [description] + * @param [type] $title [description] + * @param [type] $message [description] + * @return [type] [description] + */ + public static function sendAccountNotification(Account $account, $title, $message) + { + if (null == $account->setting->forum_id) { + return false; + } + + $dataArray["recipient_ids"] = [$account->setting->forum_id]; + + $dataArray["title"] = $title; + $dataArray["message"] = $message; + $dataArray["open_invite"] = false; + + $result = self::_sendAPIPostCommand("conversations", $dataArray); + if ($result && 200 == $result->getStatusCode()) { + return true; + } + return false; + } + + /** + * Delete an forum account. + * + * @param [type] $forumId [description] + * @param [type] $vid [description] + * + * @return [type] [description] + */ + public static function deleteForumAccount($forumId, $vid) + { + $result = self::_sendAPIDeleteCommand("users/" . $forumId, [ + "rename_to" => $vid, + ]); + if ($result && 200 == $result->getStatusCode()) { + return true; + } + + return false; + } + + /** + * Get the actual username used for this account on the forums + * + * @param Account $account + * + * @return String The actual username + */ + public static function getForumUsername(Account $account) + { + if(!$account->setting->forum_id) return false; + $result = self::_sendAPICommand( + "users/" . $account->setting->forum_id, + [] + ); + if (!$result) { + return false; + } + + $response = json_decode($result->getBody()->getContents()); + $forumUserObject = $response->user; + return $forumUserObject->username; + } + + + public static function getForumEmail(Account $account) + { + if(!$account->setting->forum_id) return false; + $result = self::_sendAPICommand( + "users/" . $account->setting->forum_id, + [] + ); + if (!$result) { + return false; + } + + $response = json_decode($result->getBody()->getContents()); + $forumUserObject = $response->user; + return $forumUserObject->email; + } + + /** + * Grab the threads from the news forum + * @return [type] [description] + */ + public static function getNewsThreads() + { + $result = self::_sendAPICommand( + "forums/" . config("forum.newsId") . "/threads", + [] + ); + if ($result && 200 == $result->getStatusCode()) { + return json_decode($result->getBody()->getContents()); + } + + return false; + } + + /** + * Grab the posts in the news threads + * @param [type] $postId [description] + * @return [type] [description] + */ + public static function getPost($postId) + { + $result = self::_sendAPICommand("posts/" . $postId, []); + if ($result && 200 == $result->getStatusCode()) { + return json_decode($result->getBody()->getContents()); + } + + return false; + } +} diff --git a/app/Libraries/XenForoApi.php b/app/Libraries/XenForoApi.php new file mode 100644 index 0000000..5c3445d --- /dev/null +++ b/app/Libraries/XenForoApi.php @@ -0,0 +1,49 @@ +setting->forum_id) return false; + $user_forum_id = $user->setting->forum_id; + $res = self::send_request('POST', 'badge/add', [ + 'user_id' => $user_forum_id, + 'badge_id' => $badge_id, + ]); + return $res != false; + } + + + protected static function send_request(string $method, string $endpoint, $data = []) + { + $baseUrl = config('forum.extraurl'); + $accessToken = config("forum.extratoken"); + $client = new Client([ + 'base_uri' => $baseUrl, + 'connect_timeout' => self::TIMEOUT, + 'read_timeout' => self::TIMEOUT, + 'timeout' => self::TIMEOUT, + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ] + ]); + $res = false; + try { + $res = $client->request($method, $endpoint, [ + 'json' => $data, + ]); + } catch (GuzzleException $e) { + return false; + } + return json_decode($res->getBody()->getContents(), true); + } +} diff --git a/app/Models/ATD/SoloEndorsement.php b/app/Models/ATD/SoloEndorsement.php new file mode 100644 index 0000000..8467509 --- /dev/null +++ b/app/Models/ATD/SoloEndorsement.php @@ -0,0 +1,76 @@ +belongsTo(\App\Models\Membership\Account::class, 'candidate_id', 'id'); + } + + /** + * The solo phase this endorsement is bound to. + * + * @return [type] [description] + */ + public function phase() + { + return $this->belongsTo(\App\Models\ATD\SoloPhase::class, 'solophase_id', 'id'); + } + + /** + * The atc station this endorsement is limited to. + * + * @return [type] [description] + */ + public function station() + { + return $this->belongsTo(\App\Models\Navigation\Station::class, 'station_id', 'id'); + } + + /** + * Calculates the date this endorsement ends. + * + * @param \Carbon\Carbon|null $startDate [description] + * @param [type] $isExtension [description] + * + * @return [type] [description] + */ + public function calculateEndDate(\Carbon\Carbon $startDate = null, $isExtension = false) + { + if (null == $startDate) { + $startDate = \Carbon\Carbon::now()->utc(); + } + + if ($isExtension) { + // Extensions are always 30 days + return $startDate->addDays(30); + } + + if (null != $this->phase) { + $endDate = $startDate->addDays($this->phase->days); + + return $endDate; + } else { + return false; + } + } + +} diff --git a/app/Models/ATD/SoloPhase.php b/app/Models/ATD/SoloPhase.php new file mode 100644 index 0000000..6123644 --- /dev/null +++ b/app/Models/ATD/SoloPhase.php @@ -0,0 +1,20 @@ +hasMany(\App\Models\ATD\SoloEndorsements::class, 'solophase_id', 'id'); + } +} diff --git a/app/Models/Booking/AtcSessionBooking.php b/app/Models/Booking/AtcSessionBooking.php new file mode 100644 index 0000000..97d6a68 --- /dev/null +++ b/app/Models/Booking/AtcSessionBooking.php @@ -0,0 +1,92 @@ +belongsTo( + \App\Models\Navigation\Station::class, + 'station_id', + 'id' + ); + } + + /** + * The user that made the booking. + * + * @return [type] [description] + */ + public function controller() + { + return $this->belongsTo( + \App\Models\Membership\Account::class, + 'controller_id', + 'id' + )->select(['id', 'firstname', 'lastname']); + } + + /** + * Get all bookings for a given event + * + * @param [type] $query [description] + * @param [type] $event_id [description] + * @return [type] [description] + */ + public function scopeForEvent($query, $event_id) + { + return $query->where('event', true) + ->where('event_id', $event_id); + } + + /** + * All bookings an account has made + * + * @param [type] $query [description] + * @param [type] $id [description] + * @return [type] [description] + */ + public function scopeForAccountId($query, $id) + { + return $query->where('controller_id', $id); + } + + /** + * All bookings for a given station id + * + * @param [type] $query [description] + * @param [type] $id [description] + * @return [type] [description] + */ + public function scopeForStation($query, $id) + { + return $query->where('station_id', $id); + } +} diff --git a/app/Models/Events/Event.php b/app/Models/Events/Event.php new file mode 100644 index 0000000..3b7f3db --- /dev/null +++ b/app/Models/Events/Event.php @@ -0,0 +1,18 @@ +check()) return false; + if ($this->legs()->count() == 0) return false; + return $this->legs()->first()->accounts()->wherePivot('account_id', auth()->id())->exists(); // a little hacky + } + + public function legs() + { + return $this->hasMany( + \App\Models\Events\RouteLeg::class, + 'route_id', + 'id' + ); + } +} diff --git a/app/Models/Events/RouteLeg.php b/app/Models/Events/RouteLeg.php new file mode 100644 index 0000000..371139d --- /dev/null +++ b/app/Models/Events/RouteLeg.php @@ -0,0 +1,49 @@ +check()) return null; + $data = $this->accounts()->wherePivot('account_id', auth()->id())->first(); + if(is_null($data)) return null; + return $data->pivot; + } + + + public function route(){ + return $this->belongsTo(EventRoute::class, 'route_id', 'id'); + } + + public function accounts() { + return $this->belongsToMany(Account::class, 'event_account_routelegs','routeleg_id','account_id')->withPivot('completed_at', 'fight_data_id'); + } + + public function departureAerodrome(){ + return $this->hasOne(Aerodrome::class, 'id', 'departureaerodrome_id'); + } + + public function arrivalAerodrome() + { + return $this->hasOne(Aerodrome::class, 'id', 'arrivalaerodrome_id'); + } + + public function flightData() + { + return $this->hasOne(FlightData::class, 'id', 'flight_data_id'); + } +} diff --git a/app/Models/Filebase/Image.php b/app/Models/Filebase/Image.php new file mode 100644 index 0000000..794223a --- /dev/null +++ b/app/Models/Filebase/Image.php @@ -0,0 +1,30 @@ +where('account_id', $id); + } + + public function getHrefAttribute() + { + return config('app.url').'/resources/image/'.$this->id; + } + + public function account() { + return $this->belongsTo(\App\Models\Membership\Account::class, 'account_id', 'id'); + } + +} diff --git a/app/Models/Forum/ForumGroup.php b/app/Models/Forum/ForumGroup.php new file mode 100644 index 0000000..d1ca1de --- /dev/null +++ b/app/Models/Forum/ForumGroup.php @@ -0,0 +1,16 @@ +belongsToMany(config('acl.models.group'), 'forumgroup_group', 'group_id', 'forumgroup_id'); + } + +} \ No newline at end of file diff --git a/app/Models/Membership/Account.php b/app/Models/Membership/Account.php new file mode 100644 index 0000000..adb17f1 --- /dev/null +++ b/app/Models/Membership/Account.php @@ -0,0 +1,138 @@ +] + */ + public function renewApiToken() + { + $this->api_token = Str::random(80); + $this->save(); + } + + /** + * Concatinate the first- and lastname of an account. + * + * @return string + */ + public function getUsernameAttribute() + { + return $this->firstname.' '.$this->lastname; + } + + // since this doesn't work in jobs + public function getIsCurrentlyForumBannedAttribute() + { + $now = Carbon::now()->utc(); + return $this->bans()->where(function($query) use ($now) { + $query->where('permanent', true)->orWhere('banned_till', '>=', $now); + })->where('forum', 1)->exists(); + } + + /** + * Is this user allowed to impersonate others + * @return boolean + */ + public function canImpersonate() + { + return $this->hasPermission('administration.impersonate'); + } + + /** + * Returns a string representing a link to VATSIM Stats. + * + * @return string + */ + public function getStatsLinkAttribute() + { + return 'https://stats.vatsim.net/search_id.php?id='.$this->id; + } + + public function getDescriptionForEvent(string $eventName): string + { + if($eventName == "created") { + return "Erstmalige Anmeldung auf der Website von vACC Germany!"; + } + if($eventName == "updated") { + return "Die Nutzerdaten wurden aktualisiert!"; + } + if($eventName == "deleted") { + return "Der Nutzer wurde gelöscht!"; + } + } +} diff --git a/app/Models/Membership/Account/Ban.php b/app/Models/Membership/Account/Ban.php new file mode 100644 index 0000000..2bb7f44 --- /dev/null +++ b/app/Models/Membership/Account/Ban.php @@ -0,0 +1,67 @@ +belongsTo(\App\Models\Membership\Account::class); + } + + /** + * The author of the ban + * @return [type] [description] + */ + public function author() + { + return $this->belongsTo(\App\Models\Membership\Account::class, 'author_id', 'id'); + } + + public function getDescriptionForEvent(string $eventName): string + { + if($eventName == "created") { + return "Der Nutzer mit der vID {$this->account_id} wurde gesperrt!"; + } + if($eventName == "updated") { + return "Die Sperre des Nutzer mit der vID {$this->account_id} wurde geändert!"; + } + if($eventName == "deleted") { + return "Der Nutzer mit der vID {$this->account_id} wurde entsperrt!"; + } + } + +} diff --git a/app/Models/Membership/Account/Data.php b/app/Models/Membership/Account/Data.php new file mode 100644 index 0000000..6bb1384 --- /dev/null +++ b/app/Models/Membership/Account/Data.php @@ -0,0 +1,186 @@ +belongsTo(Account::class, 'account_id', 'id'); + } + + public function getControllerRatingAttribute() + { + $atdRating = $this->rating_atc; + return Cache::remember( + 'rating_atc_'.$this->rating_atc, + 60*60*48, + function () use ($atdRating) { + $ratings_atd = [ + -1 => [ + 'short' => 'Inactive', + 'long' => 'Inactive', + 'grp' => 'Inactive', + ], + 0 => [ + 'short' => 'Suspended', + 'long' => 'Suspended', + 'grp' => 'Suspended', + ], + 1 => [ + 'short' => 'OBS', + 'long' => 'Observer', + 'grp' => 'Observer', + ], + 2 => [ + 'short' => 'S1', + 'long' => 'Student', + 'grp' => 'Student', + ], + 3 => [ + 'short' => 'S2', + 'long' => 'Student 2', + 'grp' => 'Student', + ], + 4 => [ + 'short' => 'S3', + 'long' => 'Senior Student', + 'grp' => 'Student', + ], + 5 => [ + 'short' => 'C1', + 'long' => 'Controller', + 'grp' => 'Controller', + ], + 6 => [ + 'short' => 'C2', + 'long' => 'Controller 2', + 'grp' => 'Controller', + 'active' => false, + ], + 7 => [ + 'short' => 'C3', + 'long' => 'Senior Controller', + 'grp' => 'Controller', + ], + 8 => [ + 'short' => 'I1', + 'long' => 'Instructor', + 'grp' => 'Instructor', + ], + 9 => [ + 'short' => 'I2', + 'long' => 'Instructor 2', + 'grp' => 'Instructor', + 'active' => false, + ], + 10 => [ + 'short' => 'I3', + 'long' => 'Senior Instructor', + 'grp' => 'Instructor', + ], + 11 => [ + 'short' => 'SUP', + 'long' => 'Supervisor', + 'grp' => 'Supervisor', + ], + 12 => [ + 'short' => 'ADM', + 'long' => 'Administrator', + 'grp' => 'Administrator', + ], + ]; + + if(array_key_exists($atdRating, $ratings_atd)) { + return $ratings_atd[$atdRating]; + } + } + ); + } + + public function getPilotRatingAttribute() + { + $ptdRating = $this->rating_pilot; + return Cache::remember( + 'rating_ptd_'.$this->rating_pilot, + 60*60*48, + function () use ($ptdRating) { + $rating = ''; + switch ($ptdRating) { + case 0: + $rating = 'P0'; + break; + case 1: + $rating = 'P1'; + break; + case 3: + $rating = 'P1 & P2'; + break; + case 7: + $rating = 'P1 & P2 & P3'; + break; + case 13: + $rating = 'No P0'; //why vatsim connect? + break; + case 15: + $rating = 'P1 & P2 & P3 & P4'; + break; + default: + $rating = 'Unkown'; + break; + } + + return $rating; + } + ); + } +} diff --git a/app/Models/Membership/Account/Note.php b/app/Models/Membership/Account/Note.php new file mode 100644 index 0000000..09315d1 --- /dev/null +++ b/app/Models/Membership/Account/Note.php @@ -0,0 +1,58 @@ +belongsTo(\App\Models\Membership\Account::class); + } + + /** + * The author of the note + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function author() + { + return $this->belongsTo(\App\Models\Membership\Account::class, 'author_id', 'id'); + } + + public function getDescriptionForEvent(string $eventName): string + { + if($eventName == "created") { + return "Eine Notiz über den Nutzer mit der vID {$this->account_id} wurde hinterlegt!"; + } + if($eventName == "updated") { + return "Eine Notiz über den Nutzer mit der vID {$this->account_id} wurde geändert!"; + } + if($eventName == "deleted") { + return "Eine Notiz über den Nutzer mit der vID {$this->account_id} wurde gelöscht!"; + } + } + +} diff --git a/app/Models/Membership/Account/Setting.php b/app/Models/Membership/Account/Setting.php new file mode 100644 index 0000000..d82e12c --- /dev/null +++ b/app/Models/Membership/Account/Setting.php @@ -0,0 +1,50 @@ +belongsTo(Account::class, 'account_id', 'id'); + } +} diff --git a/app/Models/Membership/Concerns/HasBanConcern.php b/app/Models/Membership/Concerns/HasBanConcern.php new file mode 100644 index 0000000..18fbb36 --- /dev/null +++ b/app/Models/Membership/Concerns/HasBanConcern.php @@ -0,0 +1,53 @@ +id)->exists(); + } + + public function bans() + { + return $this->hasMany(Ban::class)->orderBy('created_at', 'DESC'); + } + + public function getIsCurrentlyBannedAttribute() + { + $now = Carbon::now()->utc(); + return $this->bans()->where(function($query) use ($now) { + $query->where('permanent', true)->orWhere('banned_till', '>=', $now); + })->exists(); + } + + public function getIsCurrentlyTSBannedAttribute() + { + $now = Carbon::now()->utc(); + return $this->bans()->where(function($query) use ($now) { + $query->where('permanent', true)->orWhere('banned_till', '>=', $now); + })->where('teamspeak', 1)->exists(); + } + + public function getIsCurrentlyHomepageBannedAttribute() + { + $now = Carbon::now()->utc(); + return $this->bans()->where(function($query) use ($now) { + $query->where('permanent', true)->orWhere('banned_till', '>=', $now); + })->where('homepage', 1)->exists(); + } + + public function getCurrentBanAttribute() + { + $now = Carbon::now()->utc(); + + return $this->bans()->where(function($query) use ($now) { + $query->where('permanent', true)->orWhere('banned_till', '>=', $now); + })->first(); + } + +} diff --git a/app/Models/Membership/Concerns/HasDataConcern.php b/app/Models/Membership/Concerns/HasDataConcern.php new file mode 100644 index 0000000..69389fe --- /dev/null +++ b/app/Models/Membership/Concerns/HasDataConcern.php @@ -0,0 +1,37 @@ +id)->exists(); + } + + /** + * The data object that belongs to the account. + * + * @return Data | null + */ + public function data() + { + return $this->hasOne(Data::class, "account_id", "id"); + } + + /** + * Is the account currently inactive in the global VATSIM scope? + * @return bool [True if account is inactive] + */ + public function getIsInactiveAttribute() + { + return $this->data->rating_atc == -1; + } +} diff --git a/app/Models/Membership/Concerns/HasNoteConcern.php b/app/Models/Membership/Concerns/HasNoteConcern.php new file mode 100644 index 0000000..f866a79 --- /dev/null +++ b/app/Models/Membership/Concerns/HasNoteConcern.php @@ -0,0 +1,19 @@ +id)->exists(); + } + + public function notes() + { + return $this->hasMany(Note::class)->orderBy('created_at', 'DESC'); + } + +} diff --git a/app/Models/Membership/Concerns/HasRegionalgroupConcern.php b/app/Models/Membership/Concerns/HasRegionalgroupConcern.php new file mode 100644 index 0000000..b923205 --- /dev/null +++ b/app/Models/Membership/Concerns/HasRegionalgroupConcern.php @@ -0,0 +1,109 @@ +belongsToMany(Regionalgroup::class, 'regionalgroups_account_regionalgroup', 'account_id', 'regionalgroup_id') + ->withPivot('pilot', 'controller', 'guest') + ->with('fir') + ->withTimestamps(); + } + + /** + * All requests to regionalgroups from this account + * + * @return [type] [description] + */ + public function regionalgroupRequests() + { + return $this->hasMany(RegionalgroupRequest::class, 'account_id', 'id'); + } + + /** + * Fullmember of regionalgroup? + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isMemberOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->members->contains($this); + } + + /** + * Assigned as guest? + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isGuestOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->guests->contains($this); + } + + /** + * Is the account a controller at the regionalgroup + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isControllerOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->controllers->contains($this); + } + + /** + * Is the account assigned as a pilot + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isPilotOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->pilots->contains($this); + } + + /** + * Is the account assigend as a mentor to the given regionalgroup + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isMentorOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->mentors->contains($this); + } + + /** + * Is the account assigned as a navigator to the regionalgroup + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isNavigatorOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->navigators->contains($this); + } + + /** + * Does the account belong to the event team of a given regionalgroup + * + * @param Regionalgroup $regionalgroup [description] + * @return boolean [description] + */ + public function isEventlerOfRegionalgroup(Regionalgroup $regionalgroup) + { + return $regionalgroup->eventler->contains($this); + } + +} \ No newline at end of file diff --git a/app/Models/Membership/Concerns/HasSettingConcern.php b/app/Models/Membership/Concerns/HasSettingConcern.php new file mode 100644 index 0000000..570bf3a --- /dev/null +++ b/app/Models/Membership/Concerns/HasSettingConcern.php @@ -0,0 +1,28 @@ +id)->exists(); + } + + /** + * The data object that belongs to the account. + * + * @return \App\Models\Membership\Account\Setting | null + */ + public function setting() + { + return $this->hasOne(Setting::class, 'account_id', 'id'); + } +} diff --git a/app/Models/Membership/Concerns/HasTeamSpeakConcern.php b/app/Models/Membership/Concerns/HasTeamSpeakConcern.php new file mode 100644 index 0000000..ff882bf --- /dev/null +++ b/app/Models/Membership/Concerns/HasTeamSpeakConcern.php @@ -0,0 +1,32 @@ +hasMany(\App\Models\TeamSpeak\Registration::class, 'account_id'); + } + + /** + * Get the first unused registration an account has. + * + * @return [type] [description] + */ + public function getNewTeamspeakRegistrationAttribute() + { + return $this->teamspeakRegistrations + ->filter( + function ($reg) { + return is_null($reg->dbid); + } + ) + ->first(); + } +} diff --git a/app/Models/Membership/Group.php b/app/Models/Membership/Group.php new file mode 100644 index 0000000..fc2003a --- /dev/null +++ b/app/Models/Membership/Group.php @@ -0,0 +1,48 @@ + GroupSaving::class, + ]; + + /** + * Group constructor. + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + parent::__construct($attributes); + $this->setTable(config('acl.tables.groups')); + } + + public function getRouteKeyName() + { + return config('acl.route_model_binding_keys.group_model', 'slug'); + } + + public function forumgroups() { + return $this->belongsToMany(\App\Models\Forum\ForumGroup::class, 'forumgroup_group', 'forumgroup_id', 'group_id'); + } +} diff --git a/app/Models/Navigation/AIP.php b/app/Models/Navigation/AIP.php new file mode 100644 index 0000000..54edff2 --- /dev/null +++ b/app/Models/Navigation/AIP.php @@ -0,0 +1,243 @@ +_aipFile = $file; + } + + public function parse() : void + { + $parser = new \Smalot\PdfParser\Parser(); + $pdf = $parser->parseFile(storage_path('app').'/'.$this->_aipFile); + $pages = $pdf->getPages(); + + $currentSector = array(); + + $processingSector = false; + + for ($i = 0; $i < sizeof($pages); $i++) { + + $pageData = $pages[$i]->getTextArray(); + + $lastCoordinateFoundAt = 0; + + + for($j = 0; $j < sizeof($pageData); $j++) { + + if(preg_match("/N{1}\s?(\d{2}\s\d{2}\s\d{2}|\d{6})\s?E{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})/", $pageData[$j])) { + + if(!$processingSector) { + if(isset($currentSector['name']) && isset($currentSector['coordinates'])) { + $this->_sectors[] = $currentSector; + } + + $currentSector['name'] = ''; + $currentSector['coordinates'] = ''; + + $matches; + preg_match("/N{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})\s?E{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})/", $pageData[$j], $matches, PREG_OFFSET_CAPTURE); + + if($matches[0][1] > 0) { + $currentSector['name'] = substr($pageData[$j], 0, $matches[0][1] - 1); + $currentSector['coordinates'] = substr($pageData[$j], $matches[0][1]); + } else { + if($lastCoordinateFoundAt != 0) { + for($k = $j - 1; $k > $lastCoordinateFoundAt; $k--) { + if( + !preg_match("/N{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})\s?E{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})/", $pageData[$k]) + && preg_match("/(EDD|ETN|ETS|EDUU|EDWW|EDYY|EDGG|EDMM|FIR|UIR|\(HX\)|CTR|Sector)/", $pageData[$k]) + && Str::length($pageData[$k]) > 1 + ) { + $currentSector['name'] = $pageData[$k]; + if(!Str::contains($currentSector['name'], ['part', 'gebiet', 'territory', 'area'])) + break; + } + } + $lastCoordinateFoundAt = 0; + } else { + $currentSector['name'] = $pageData[$j - 1]; + } + $currentSector['coordinates'] = $pageData[$j]; + } + // var_dump($currentSector['name']); + $crawlBack = ($j >= 2) ? $j - 1 : $j; + while($crawlBack > 1 + && (Str::length($currentSector['name']) == 0 + || Str::contains($currentSector['name'], $this->_skipOnWords) + || preg_match("/N{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})\s?E{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})/", $currentSector['name']) + ) + ) + { + $currentSector['name'] = $pageData[$crawlBack]; + $crawlBack--; + } + + if( + $crawlBack == 0 && Str::length($currentSector['name']) == 0 + || Str::contains($currentSector['name'], $this->_skipOnWords) + || preg_match("/N{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})\s?E{1}\s?(\d{2,3}\s\d{2}\s\d{2}|\d{6,7})/", $currentSector['name']) + ) { + $currentSector['name'] = $pageData[$j - 1]; + } + + // var_dump($currentSector['name']); + + if(Str::endsWith($pageData[$j], ['.', ';'])) { + $lastCoordinateFoundAt = $j; + $this->_sectors[] = $currentSector; + $processingSector = false; + } else { + $processingSector = true; + } + } else { + + // Don't parse remarks + if(preg_match("/(^\d{1}\.\s?|\d{1}\.\s\w+\sTWR)/", $pageData[$j])) { + $lastCoordinateFoundAt = $j; + $processingSector = false; + } + + if(isset($currentSector['name']) && isset($currentSector['coordinates'])) { + $currentSector['coordinates'] .= ' '.$pageData[$j]; + } + + if(Str::endsWith($pageData[$j], ['.', ';']) && $processingSector) { + $lastCoordinateFoundAt = $j; + $processingSector = false; + } + } + + } else { + + // Don't parse remarks + if(preg_match("/(^\d{1}\.\s?|\d{1}\.\s\w+\sTWR)/", $pageData[$j])) { + $lastCoordinateFoundAt = $j; + $processingSector = false; + } + + if(isset($currentSector['name']) && isset($currentSector['coordinates']) && $processingSector) { + $currentSector['coordinates'] .= ' '.$pageData[$j]; + } + + if(Str::endsWith($pageData[$j], ['.', ';']) && $processingSector) { + $lastCoordinateFoundAt = $j; + $processingSector = false; + } + } + + + } + + } + } + + public function buildSct() : string { + $sctOutput = "[ARTCC]\n"; + $lastSensfulSectorname = ''; + foreach ($this->_sectors as $sector) { + + if((preg_match("/((a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z){1}\)?|\/)/", $sector['name']) && Str::length($sector['name']) < 3) && $lastSensfulSectorname != '') { + $sector['name'] = $lastSensfulSectorname.' '.$sector['name']; + } else { + $lastSensfulSectorname = $sector['name']; + } + + $sctOutput .= "; ================================\n"; + $sctOutput .= "; ".$sector['name']."\n"; + $sctOutput .= "; ================================\n"; + $convertedCoordinates = $this->convertCoordinates($sector['coordinates'], 'sct'); + $coords = explode("\n", $convertedCoordinates); + foreach ($coords as $c) { + $cSplit = explode(" ", $c); + $warning = false; + foreach ($cSplit as $cs) { + if(!preg_match("/(N{1}0{1}(4|5){1}|E{1}0{1}(05|06|07|08|09|10|11|12|13|14){1})/", $cs)) + $warning = true; + } + if($warning) + $sctOutput .= $sector['name'].' '.$c." ; WARNING!!!\n"; + else + $sctOutput .= $sector['name'].' '.$c."\n"; + } + } + return $sctOutput; + } + + public function buildEse() : string { + $eseOutput = ""; + foreach ($this->_sectors as $sector) { + if((preg_match("/((a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z){1}\)?|\/)/", $sector['name']) && Str::length($sector['name']) < 3) && $lastSensfulSectorname != '') { + $sector['name'] = $lastSensfulSectorname.' '.$sector['name']; + } else { + $lastSensfulSectorname = $sector['name']; + } + + $eseOutput .= "; ================================\n"; + $eseOutput .= "; ".$sector['name']."\n"; + $eseOutput .= "; ================================\n"; + $eseOutput .= "SECTORLINE:".$sector['name']."\n"; + $convertedCoordinates = $this->convertCoordinates($sector['coordinates'], 'ese'); + $coords = explode("\n", $convertedCoordinates); + foreach ($coords as $c) { + $cSplit = explode(" ", $c); + $warning = false; + foreach ($cSplit as $cs) { + if(!preg_match("/(N{1}0{1}(4|5){1}|E{1}0{1}(05|06|07|08|09|10|11|12|13|14){1})/", $cs)) + $warning = true; + } + if($warning) + $eseOutput .= $c." ; WARNING!!!\n"; + else + $eseOutput .= $c."\n"; + } + } + return $eseOutput; + } + + private function convertCoordinates($coords, $format = 'sct') + { + + $coords = str_replace(",", ".", $coords); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL,"https://webtools.kusternet.ch/coordinatesimporter"); + curl_setopt($ch, CURLOPT_POST, 1); + + // In real life you should use something like: + curl_setopt($ch, CURLOPT_POSTFIELDS, + http_build_query(array( + 'text1' => $coords, + 'analyse' => 'Analyse', + 'format' => $format, + 'colorname' => '' + ))); + + // Receive server response ... + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $server_output = curl_exec($ch); + + curl_close ($ch); + + $coords = preg_split("/")); + + return $coords; + } + +} diff --git a/app/Models/Navigation/Aerodrome.php b/app/Models/Navigation/Aerodrome.php new file mode 100644 index 0000000..c9c6f49 --- /dev/null +++ b/app/Models/Navigation/Aerodrome.php @@ -0,0 +1,130 @@ +belongsToMany(Regionalgroup::class, 'navigation_aerodrome_regionalgroup', 'regionalgroup_id', 'aerodrome_id'); + } + + /** + * Get all assigned stations + * @return [type] [description] + */ + public function stations() + { + return $this->belongsToMany(Station::class, 'navigation_aerodrome_station', 'aerodrome_id', 'station_id') + ->withPivot('order') + ->orderBy('navigation_aerodrome_station.order', 'ASC'); + } + + /** + * All runways this aerodrome has + * @return [type] [description] + */ + public function runways() + { + return $this->hasMany(Runway::class); + } + + /** + * All associated navaids + * @return [type] [description] + */ + public function navaids() + { + return $this->belongsToMany(Navaid::class, 'navigation_aerodrome_navaid', 'navaid_id', 'aerodrome_id'); + } + + /** + * The charts associated with this aerodrome + * + * @return [type] [description] + */ + public function charts() + { + return $this->belongsToMany(Chart::class, 'navigation_aerodrome_chart', 'aerodrome_id', 'chart_id'); + } + + /** + * Get an aerodrome by it's icao + * @param [type] $query [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function scopeIcao($query, $icao) + { + return $query->where('icao', $icao); + } + + /** + * Get only aerodromes that are assigned to Germany + * @param [type] $query [description] + * @return [type] [description] + */ + public function scopeIsDe($query) + { + return $query->where('country', 'DE'); + } + + /** + * The country the aerodrome is situated at + * @return [type] [description] + */ + public function countryDetail() + { + return $this->belongsTo(Country::class, 'country', 'alpha_2'); + } + + /** + * Load the current atc activity at the aerodrome + * + * @return [type] [description] + */ + public function getControllerActivityAttribute() + { + if($this->stations->count() > 0) { + return \App\Models\Network\AtcClient::withCallsignIn( + $this->stations->pluck('ident')->push('%'.$this->icao.'%')->all() + )->online()->get(); + } + return \App\Models\Network\AtcClient::icao('%'.$this->icao.'%')->online()->get(); + } + + /** + * Is something in the vicinity of the airport? + * @param [type] $latitude [description] + * @param [type] $longitude [description] + * @return [type] [description] + */ + public function containsCoordinates($latitude, $longitude) + { + return $latitude < $this->latitude + 0.06 && $latitude > $this->latitude - 0.06 + && $longitude < $this->longitude + 0.06 && $longitude > $this->longitude - 0.06; + } + +} diff --git a/app/Models/Navigation/Chart.php b/app/Models/Navigation/Chart.php new file mode 100644 index 0000000..8b06a7e --- /dev/null +++ b/app/Models/Navigation/Chart.php @@ -0,0 +1,46 @@ +belongsToMany(Aerodrome::class, 'navigation_aerodrome_chart', 'chart_id', 'aerodrome_id'); + } + + public function scopeAirac($query, $airac = '') + { + if($airac === '') { + $airac = \Carbon\Carbon::now()->utc()->format('ym'); + } + return $query->where('airac', $airac); + } + + public function scopePublished($query, $published = true) + { + return $query->where('published', $published); + } + + public function getIsGitlabAttribute() : bool + { + return str_starts_with($this->href, 'gitlab:'); + } + + public function getGitlabLinkAttribute() + { + $gitlab = new Gitlab(); + return $gitlab->generateChartLink($this); + } + + + +} diff --git a/app/Models/Navigation/Country.php b/app/Models/Navigation/Country.php new file mode 100644 index 0000000..7734e33 --- /dev/null +++ b/app/Models/Navigation/Country.php @@ -0,0 +1,18 @@ +belongsToMany(Aerodrome::class, 'navigation_aerodrome_navaid', 'aerodrome_id', 'navaid_id'); + } + + public function getTypeStringAttribute() + { + $ts = ''; + switch ($this->type) { + case self::TYPE_NDB: + $ts = 'NDB'; + break; + case self::TYPE_VOR: + $ts = 'VOR'; + break; + case self::TYPE_VORDME: + $ts = 'VOR / DME'; + break; + case self::TYPE_DME: + $ts = 'DME'; + break; + case self::TYPE_ILS: + $ts = 'ILS'; + break; + case self::TYPE_TACAN: + $ts = 'TACAN'; + break; + default: + $ts = 'Unknown'; + break; + } + + return $ts; + } + + public function getFrequencyBandStringAttribute() + { + $fbs = ''; + switch ($this->frequency_band) { + case self::FREQUENCY_BAND_MHZ: + $fbs = 'MHz'; + break; + case self::FREQUENCY_BAND_KHZ: + $fbs = 'KHz'; + break; + default: + $fbs = 'Unknown'; + break; + } + + return $fbs; + } + +} \ No newline at end of file diff --git a/app/Models/Navigation/Runway.php b/app/Models/Navigation/Runway.php new file mode 100644 index 0000000..a05db91 --- /dev/null +++ b/app/Models/Navigation/Runway.php @@ -0,0 +1,55 @@ +belongsTo(\App\Models\Navigation\Aerodrome::class); + } + + public function opposite() + { + return $this->belongsTo(self::class, 'opposite_id', 'id'); + } + + public function getSurfaceTypeStringAttribute() { + switch ($this->surface_type) { + case self::SURFACE_TYPE_ASPHALT: + return trans('pilots.aerodromes.navigation.surface.asphalt'); + case self::SURFACE_TYPE_CONCRETE: + return trans('pilots.aerodromes.navigation.surface.concrete'); + case self::SURFACE_TYPE_GRASS: + return trans('pilots.aerodromes.navigation.surface.grass'); + case self::SURFACE_TYPE_WATER: + return trans('pilots.aerodromes.navigation.surface.water'); + case self::SURFACE_TYPE_SAND: + return trans('pilots.aerodromes.navigation.surface.sand'); + case self::SURFACE_TYPE_GRE: + return trans('pilots.aerodromes.navigation.surface.graded'); + default: + return trans('pilots.aerodromes.navigation.surface.unkown'); + } + } + +} \ No newline at end of file diff --git a/app/Models/Navigation/SCT.php b/app/Models/Navigation/SCT.php new file mode 100644 index 0000000..9f8c066 --- /dev/null +++ b/app/Models/Navigation/SCT.php @@ -0,0 +1,375 @@ +raw = $r->Document; + // $this->parse(); + } + + private function parseGeo($geo, $defaultStyle = 'COLOR_GEO') + { + // GEO FOUND + if (isset($geo->LineString)) { + $coords = explode(' ', $geo->LineString->coordinates); + foreach ($coords as $key => $value) { + $v = trim($value); + if ('' != $v && !empty($v) && !ctype_space($v)) { + $coords[$key] = substr($v, 0, strlen($v) - 2); + } else { + unset($coords[$key]); + } + } + $g = []; + $g['name'] = trim($geo->name); + if (isset($geo->description)) { + $g['style'] = ''.$geo->description; + } else { + $g['style'] = ''.$defaultStyle; + } + $g['coords'] = $coords; + + return $g; + } + } + + private function parseMultiGeo($geo, $defaultStyle = 'COLOR_GEO', $fname = 'Germany') + { + // MULTIGEO FOUND + if (isset($geo->MultiGeometry)) { + $returnGeo = []; + $polygons = $geo->MultiGeometry; + foreach ($polygons as $poly) { + foreach ($poly->Polygon as $p) { + $coords = explode(' ', $p->outerBoundaryIs->LinearRing->coordinates); + foreach ($coords as $key => $value) { + $v = trim($value); + if ('' != $v && !empty($v) && !ctype_space($v)) { + $coords[$key] = substr($v, 0, strlen($v) - 2); + } else { + unset($coords[$key]); + } + } + $g = []; + $g['name'] = trim($fname); + $g['style'] = (isset($geo->description)) ? $geo->description : $defaultStyle; + $g['coords'] = $coords; + + $returnGeo[] = $g; + } + } + return $returnGeo; + } + } + + private function parseLabel($label, $defaultStyle = 'COLOR_LABEL') + { + if (isset($label->Point)) { + $coords = substr($label->Point->coordinates, 0, strlen($label->Point->coordinates) - 2); + $l = []; + $l['name'] = trim($label->name); + if (isset($label->description)) { + $l['style'] = ''.$label->description; + } else { + $l['style'] = ''.$defaultStyle; + } + $l['coords'] = $coords; + + return $l; + } + } + + private function parseRegion($region, $defaultStyle = 'COLOR_REGION') + { + if (isset($region->Polygon)) { + $coords = explode(' ', $region->Polygon->outerBoundaryIs->LinearRing->coordinates); + foreach ($coords as $key => $value) { + $v = trim($value); + if ('' != $v && !empty($v) && !ctype_space($v)) { + $coords[$key] = substr($v, 0, strlen($v) - 2); + } else { + unset($coords[$key]); + } + } + $r = []; + $r['name'] = trim($region->name); + if (isset($region->description)) { + $r['style'] = ''.$region->description; + } else { + $r['style'] = ''.$defaultStyle; + } + $r['coords'] = $coords; + + return $r; + } + } + + public function parse() + { + if (isset($this->raw->Folder)) { + $folder = $this->raw->Folder; + if(isset($folder->description)) { + // Parse Color codes from description + $desc = trim(''.$folder->description); + $styles = explode(',', trim($desc)); + for ($i = 0; $i < count($styles); ++$i) { + $s = explode(' ', trim($styles[$i])); + if (2 == count($s)) { + // (BLUE * 65536) + (GREEN * 256) + RED + $colorRGB = $this->hexToRgb($s[1]); + $colorCode = ($colorRGB['b'] * 65536) + ($colorRGB['g'] * 256) + $colorRGB['r']; + $this->colors[] = $s[0]."\t".$colorCode; + } + } + } + foreach ($folder->Folder as $glf) { + $this->groundlayouts[] = $this->parseGroundlayout($glf); + } + } + } + + private function parseGroundlayout($folder) + { + $layoutName = $folder->name; + $regions = []; + $geos = []; + $labels = []; + + foreach ($folder->Folder as $f) { + switch ($f->name) { + case 'Perimiter': + foreach ($f->Placemark as $region) { + $regions[] = $this->parseRegion($region, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_PERIMITER'); + } + break; + + case 'Runways': + foreach ($f->Placemark as $region) { + $regions[] = $this->parseRegion($region, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_RUNWAY'); + } + break; + + case 'Taxiways': + foreach ($f->Placemark as $region) { + $regions[] = $this->parseRegion($region, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_TAXIWAY'); + } + break; + + case 'Aprons': + foreach ($f->Placemark as $region) { + $regions[] = $this->parseRegion($region, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_APRON'); + } + break; + + case 'Buildings': + foreach ($f->Placemark as $geo) { + $geos[] = $this->parseGeo($geo, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_BUILDING'); + } + break; + + case 'Markings': + foreach ($f->Placemark as $geo) { + if(isset($geo->LineString)) + $geos[] = $this->parseGeo($geo, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_MARKING'); + if(isset($geo->Polygon)) + $regions[] = $this->parseRegion($geo, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_MARKING'); + if(isset($geo->MultiGeometry)) + $geos[] = $this->parseMultiGeo($geo, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_BUILDING', $f->name); + } + break; + + case 'Labels': + foreach ($f->Placemark as $label) { + $labels[] = $this->parseLabel($label, (isset($f->description)) ? $f->description : 'COLOR_AIRPORT_LABELS'); + } + break; + + default: + // Do nothing!!!!!!!!! + break; + } + } + + $layout = [ + 'name' => $layoutName, + 'geo' => $geos, + 'regions' => $regions, + 'labels' => $labels, + ]; + + return $layout; + } + + private function hexToRgb($hex) + { + $hex = str_replace('#', '', $hex); + $length = strlen($hex); + $rgb['a'] = hexdec(8 == $length ? substr($hex, 0, 2) : 0); + $rgb['r'] = hexdec(8 == $length ? substr($hex, 2, 2) : (6 == $length ? substr($hex, 0, 2) : 0)); + $rgb['g'] = hexdec(8 == $length ? substr($hex, 4, 2) : (6 == $length ? substr($hex, 2, 2) : 0)); + $rgb['b'] = hexdec(8 == $length ? substr($hex, 6, 2) : (6 == $length ? substr($hex, 4, 2) : 0)); + + return $rgb; + } + + private function DECtoDMS($coord, $isLon = false) + { + $val = abs($coord); + $deg = floor($val); + $min = floor(($val - $deg) * 60); + $sec = round(($val - $deg - $min / 60) * 3600 * 1000) / 1000; + $dirInd = 'N'; + if (!$isLon && $coord < 0) { + $dirInd = 'S'; + } + if ($isLon && $coord < 0) { + $dirInd = 'W'; + } + if ($isLon && $coord > 0) { + $dirInd = 'E'; + } + + $str = $dirInd.(($deg < 10 && $deg > -10) ? '00'.$deg : '0'.$deg).'.'.(($min < 10 && $min > -10) ? '0'.$min : $min).'.'.(($sec < 10 && $sec > -10) ? '0'.$sec : $sec); + if (strlen($str) < 14) { + while (strlen($str) < 14) { + if (10 == strlen($str)) { + $str .= '.0'; + } else { + $str .= '0'; + } + } + } + + return $str; + } + + private function convertCoordinates($cString) + { + $cPair = explode(',', $cString); + $lat = (float) $cPair[1]; + $lon = (float) $cPair[0]; + // Convert now + $lat = $this->DECtoDMS($lat); + $lon = $this->DECtoDMS($lon, true); + + return $lat.' '.$lon; + } + + private function convertCoordinatesSeparated($cString) + { + $cPair = explode(',', $cString); + $lat = (float) $cPair[1]; + $lon = (float) $cPair[0]; + // Convert now + $lat = $this->DECtoDMS($lat); + $lon = $this->DECtoDMS($lon, true); + + return $lat.':'.$lon; + } + + public function build() + { + $output = ""; + + // General Information + $output = "[INFO]\nGlG Version 1.1\nVATSim Germany\nZZZZ\nN053.08.35.000\nE009.25.08.000\n60\n38\n-0\n1\n\n"; + // Render The Colors + $output .= "; -- Define Colors\n"; + for ($i = 0; $i < count($this->colors); ++$i) { + $output .= '#define '.$this->colors[$i]."\n"; + } + + $geoOutput = "\n[GEO]\n";; + $regionOutput = "\n[REGIONS]\n"; + $labelOutput = "\n\n\n;================== BELOW CONTENT MUST BE WRITTEN TO .ESE FILE ============================\n\n\n"; + + foreach ($this->groundlayouts as $gl) { + if(isset($gl['geo'])) { + $geoOutput .= "\n\n\n;===================================================================================\n"; + $geoOutput .= "; ".$gl['name']."\n"; + $geoOutput .= ";===================================================================================\n"; + $geoOutput .= $this->buildGeo($gl['name'], $gl['geo']); + } + + if(isset($gl['regions'])) { + $regionOutput .= "\n\n\n;===================================================================================\n"; + $regionOutput .= "; ".$gl['name']."\n"; + $regionOutput .= ";===================================================================================\n"; + $regionOutput .= $this->buildRegion($gl['name'], $gl['regions']); + } + + if(isset($gl['labels'])) { + $labelOutput .= "\n\n\n;===================================================================================\n"; + $labelOutput .= "; ".$gl['name']."\n"; + $labelOutput .= ";===================================================================================\n"; + $labelOutput .= $this->buildFreetext($gl['name'], $gl['labels']); + } + } + + return $output . $geoOutput . $regionOutput . $labelOutput; + + } + + + private function buildGeo($name, $geo) { + $geoOutput = ''; + + foreach ($geo as $g) { + if(is_array($g) && !isset($g['name']) && !isset($g['coords'])) { + $geoOutput .= $this->buildGeo($name, $g); + } else { + $geoOutput.= ";========== ".$g['name']."\n"; + for($i = 1; $i < count($g['coords']); ++$i) { + $geoOutput .= $name."\t\t".$this->convertCoordinates($g['coords'][$i-1])."\t\t".$this->convertCoordinates($g['coords'][$i])."\t".$g['style']."\n"; + } + } + } + + return $geoOutput; + } + + private function buildRegion($name, $region) { + $regionOutput = ''; + + foreach ($region as $r) { + if(is_array($r) && !isset($r['name']) && !isset($r['coords'])) { + $regionOutput.= $this->buildRegion($name, $r); + } else { + $regionOutput .= 'REGIONNAME '.$name."\n"; + for ($i = 0; $i < count($r['coords']) - 1; ++$i) { + if (0 == $i) { + $regionOutput .= $r['style']."\t".$this->convertCoordinates($r['coords'][$i])."\n"; + } else { + $regionOutput .= "\t\t".$this->convertCoordinates($r['coords'][$i])."\n"; + } + } + } + } + + return $regionOutput; + } + + private function buildFreetext($name, $labels) + { + $ftxtOutput = ""; + + // Render Labels & Freetext + for ($i = 0; $i < count($labels); ++$i) { + // $labelSection .= '; --- '.$gl['name'].' -- '.$gl['labels'][$i]['name']."\n"; + // $labelSection .= $gl['labels'][$i]['name']."\t".$this->convertCoordinates($gl['labels'][$i]['coords'])."\t".$gl['labels'][$i]['style']."\n"; + $ftxtOutput .= '; --- '.$name.' -- '.$labels[$i]['name']."\n"; + $ftxtOutput .= $this->convertCoordinatesSeparated($labels[$i]['coords']).':'.$name.':'.$labels[$i]['name']."\n"; + } + + return $ftxtOutput; + } +} \ No newline at end of file diff --git a/app/Models/Navigation/StandStatus.php b/app/Models/Navigation/StandStatus.php new file mode 100644 index 0000000..1d07d85 --- /dev/null +++ b/app/Models/Navigation/StandStatus.php @@ -0,0 +1,213 @@ +airportStandsFile, 'r'); + if ($handle) { + while (false !== ($row = fgetcsv($handle, 4096))) { + if (empty($fields)) { + $fields = $row; + continue; + } + $y = 0; + foreach ($row as $k => $value) { + if (1 == $y) { // Convert LAT coordinate + // $array[$row[0]][$fields[$k]] = $this->convertCAALatCoord($value); + $array[$row[0]][$fields[$k]] = $value; + } elseif (2 == $y) { // Convert LONG coordinate + // $array[$row[0]][$fields[$k]] = $this->convertCAALongCoord($value); + $array[$row[0]][$fields[$k]] = $value; + } else { + $array[$row[0]][$fields[$k]] = $value; + } + ++$y; + } + ++$i; + } + if (!feof($handle)) { + echo "Error: unexpected fgets() fail\n"; + + return false; + } + fclose($handle); + } else { + return false; + } + $this->stands = $array; + + return true; + } + + /** + * Does a stand has sidestands? + * + * @param [type] $standId [description] + * @return [type] [description] + */ + public function standSides($standID) + { + $standSides = []; + $stands = $this->stands; + + // Consider only those sidestands that are stands with appendix + // R, L, A, B, C + // but start in the same way as a normal stand + // so we will check if a stand ends on one of those letter and remove it + $standBase = ''; + if (Str::endsWith($standID, 'R')) { + $standBase = Str::replaceLast('R', '', $standID); + } + if (Str::endsWith($standID, 'L')) { + $standBase = Str::replaceLast('L', '', $standID); + } + if (Str::endsWith($standID, 'A')) { + $standBase = Str::replaceLast('A', '', $standID); + } + if (Str::endsWith($standID, 'B')) { + $standBase = Str::replaceLast('B', '', $standID); + } + if (Str::endsWith($standID, 'C')) { + $standBase = Str::replaceLast('C', '', $standID); + } + + // Check if stand has a side already + if (Str::endsWith($standID, 'R') || Str::endsWith($standID, 'L')) { + // Our stand is already L/R + if (Str::endsWith($standID, 'R')) { + if (isset($stands[$standBase.'L'])) { + $standSides[] = $standBase.'L'; + } + if (isset($stands[$standBase.'C'])) { + $standSides[] = $standBase.'C'; + } + if (isset($stands[$standBase])) { + $standSides[] = $standBase; + } + } + if (Str::endsWith($standID, 'L')) { + if (isset($stands[$standBase.'R'])) { + $standSides[] = $standBase.'R'; + } + if (isset($stands[$standBase.'C'])) { + $standSides[] = $standBase.'C'; + } + if (isset($stands[$standBase])) { + $standSides[] = $standBase; + } + } + } elseif (strstr($standID, 'A') || strstr($standID, 'B')) { + // Our stand already is A / B + if (Str::endsWith($standID, 'A')) { + if (isset($stands[$standBase.'B'])) { + $standSides[] = $standBase.'B'; + } + if (isset($stands[$standBase.'C'])) { + $standSides[] = $standBase.'C'; + } + if (isset($stands[$standBase])) { + $standSides[] = $standBase; + } + } + if (Str::endsWith($standID, 'B')) { + if (isset($stands[$standBase.'A'])) { + $standSides[] = $standBase.'A'; + } + if (isset($stands[$standBase.'C'])) { + $standSides[] = $standBase.'C'; + } + if (isset($stands[$standBase])) { + $standSides[] = $standBase; + } + } + } else { + // Stand itself has no side, but may have L / R / A / B sides + if (isset($stands[$standBase.'L'])) { + $standSides[] = $standBase.'L'; + } + if (isset($stands[$standBase.'R'])) { + $standSides[] = $standBase.'R'; + } + if (isset($stands[$standBase.'C'])) { + $standSides[] = $standBase.'C'; + } + if (isset($stands[$standBase.'A'])) { + $standSides[] = $standBase.'A'; + } + if (isset($stands[$standBase.'B'])) { + $standSides[] = $standBase.'B'; + } + } + + if (0 == count($standSides)) { + return false; + } else { + return $standSides; + } + } + + public function getAircraftWithinParameters() + { + + $pilots = \App\Models\Network\PilotClient::online()->withinAirport($this->airportICAO)->get(); + + $filteredResults = []; + foreach ($pilots as $pilot) { + $d = $this->getCoordDistance($pilot->current_latitude, $pilot->current_longitude, $this->airportCoordinates['lat'], $this->airportCoordinates['long']); + // \Log::info('Flight: '.$pilot->callsign.' has distance '.$d.' to icao '.$this->airportICAO); + if($d <= $this->getMaxDistanceFromAirport()) { + //$pilots[] = array('callsign' => "TEST", "latitude" => 55.949228, "longitude" => -3.364303, "altitude" => 0, "groundspeed" => 0, "planned_destairport" => "TEST", "planned_depairport" => "TEST"); + $filteredResults[] = array( + 'callsign' => $pilot->callsign, + 'latitude' => $pilot->current_latitude, + 'longitude' => $pilot->current_longitude, + 'altitude' => $pilot->current_altitude, + 'groundspeed' => $pilot->current_groundspeed, + 'planned_destairport' => $pilot->arrival_airport, + 'planned_depairport' => $pilot->departure_airport, + ); + } + } + $this->aircraftSearchResults = $filteredResults; + return true; + } +} \ No newline at end of file diff --git a/app/Models/Navigation/Station.php b/app/Models/Navigation/Station.php new file mode 100644 index 0000000..5ea3005 --- /dev/null +++ b/app/Models/Navigation/Station.php @@ -0,0 +1,43 @@ +belongsToMany(Aerodrome::class, 'navigation_aerodrome_station', 'station_id', 'aerodrome_id') + ->withPivot('order'); + } + + public function getFixedFrequencyAttribute() + { + return number_format($this->frequency, 3); + } + + public function scopeBookable($query) + { + return $query->where('bookable', true); + } + + public function scopeAtis($query) + { + return $query->where('atis', true); + } + +} diff --git a/app/Models/Network/AtcClient.php b/app/Models/Network/AtcClient.php new file mode 100644 index 0000000..acbd7f7 --- /dev/null +++ b/app/Models/Network/AtcClient.php @@ -0,0 +1,113 @@ +belongsTo(\App\Models\Membership\Account::class, 'account_id', 'id'); + } + + /** + * Station is offline + * @param [type] $query [description] + * @return [type] [description] + */ + public function scopeOffline($query) + { + return $query->whereNotNull('disconnected_at'); + } + + /** + * Is the station considered online? + * @param [type] $query [description] + * @return [type] [description] + */ + public function scopeOnline($query) + { + return $query->whereNull('disconnected_at'); + } + + /** + * Find station by icao. + * THIS ONLY CHECKS IF THE ICAO OF AN AERODROME IS CONTAINED + * @param [type] $query [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public function scopeIcao($query, $icao) + { + return $query->where('callsign', 'LIKE', '%'.$icao.'%'); + } + + /** + * Get all clients that match one of the given callsigns + * + * @param [type] $query [description] + * @param array $callsigns [description] + * @return [type] [description] + */ + public function scopeWithCallsignIn($query, array $callsigns) + { + return $query->where(function($query) use ($callsigns) { + foreach ($callsigns as $callsign) { + $query->orWhere('callsign', 'LIKE', $callsign); + } + }); + } + + /** + * Is the station within the german airspace + * @param [type] $query [description] + * @return [type] [description] + */ + public function scopeIsDe($query) + { + return $query->where(function ($subQuery) { + return $subQuery->where('callsign', 'LIKE', 'ED__%') + ->orWhere('callsign', 'LIKE', "ETA_%") + ->orWhere('callsign', 'LIKE', "ETH_%") + ->orWhere('callsign', 'LIKE', "ETI_%") + ->orWhere('callsign', 'LIKE', 'ETM_%') + ->orWhere('callsign', 'LIKE', "ETN_%") + ->orWhere('callsign', 'LIKE', "ETS_%"); + }); + } + + /** + * Format the frequency to always have 3 digits after the divider + * + * @return [type] [description] + */ + public function getFixedFrequencyAttribute() + { + return number_format($this->frequency, 3); + } + +} diff --git a/app/Models/Network/PilotClient.php b/app/Models/Network/PilotClient.php new file mode 100644 index 0000000..d8231c8 --- /dev/null +++ b/app/Models/Network/PilotClient.php @@ -0,0 +1,138 @@ +whereNotNull('disconnected_at'); + } + + /** + * Still connected to the network. + * Flight is therefore not completed + * @param [type] $query [description] + * @return [type] [description] + */ + public function scopeOnline($query) + { + return $query->whereNull('disconnected_at'); + } + + /** + * Only flights from or to an aerodrome with the given icao. + * + * @param [type] $query [description] + * @param [type] $icao [description] + * @return [type] [description] + */ + public static function scopeWithinAirport($query, $icao) + { + return $query->where(function ($subQuery) use ($icao) { + return $subQuery->where('departure_airport', $icao) + ->orWhere('arrival_airport', $icao); + }); + } + + /** + * Get the expected time remaining. + * + * @return float -1 if plane at origin, -2 if plane at destination, -3 if unknown + * Else a positive value representing the remaining time in hours + */ + public function getExpectedArrivalTimeAttribute() + { + $destination = Cache::remember( + 'navigation.airport.flight.arrival.'.$this->id, 24 * 60 * 60, + function () { + return $this->_loadAirport($this->arrival_airport); + } + ); + $origin = Cache::remember( + 'navigation.airport.flight.origin.'.$this->id, 24 * 60 * 60, + function () { + return $this->_loadAirport($this->departure_airport); + } + ); + // Determine if the flight is at it's origin or destination + $atOrigin = false; + $atDestination = false; + try { + if ($origin) { + $atOrigin = $origin->containsCoordinates($this->current_latitude, $this->current_longitude); + } + if ($destination) { + $atDestination = $destination->containsCoordinates($this->current_latitude, $this->current_longitude); + } + } catch (Exception $e) { + } + if ($atOrigin) { + return -1; + } + if ($atDestination) { + return -2; + } + if ($destination == null) { + return -3; + } + $dlong = ($destination->longitude - $this->current_longitude) * (M_PI / 180); + $dlat = ($destination->latitude - $this->current_latitude) * (M_PI / 180); + $a = pow(sin($dlat / 2.0), 2) + cos($this->current_latitude * (M_PI / 180)) * cos($destination->latitude * (M_PI / 180)) * pow(sin($dlong / 2.0), 2); + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + $d = 3956 * $c; + + // return the remaining flight time to destination in hours + // Avoid division by Zero for planes at parking position + if ($this->current_groundspeed <= 1) { + $this->current_groundspeed = 1; + } + + return $d / ($this->current_groundspeed * 1.15077945); + } + + /** + * Get an aerodrome from the database if needed for calculations + * @param [type] $icao [description] + * @return [type] [description] + */ + protected function _loadAirport($icao) + { + return \App\Models\Navigation\Aerodrome::icao($icao)->first(); + } + +} diff --git a/app/Models/Partner.php b/app/Models/Partner.php new file mode 100644 index 0000000..dddb800 --- /dev/null +++ b/app/Models/Partner.php @@ -0,0 +1,42 @@ +belongsTo(Account::class, 'id', 'created_by'); + } + + public function getDescriptionAttribute() + { + if ( + (Auth::check() && Auth::user()->settings->language == 'de') || + ((Session::has('language')) && Session::get('language') == 'de') + ) + { + return $this->description_de; + } else + { + return $this->description_en; + } + } +} diff --git a/app/Models/Regionalgroups/FlightInformationRegion.php b/app/Models/Regionalgroups/FlightInformationRegion.php new file mode 100644 index 0000000..9262722 --- /dev/null +++ b/app/Models/Regionalgroups/FlightInformationRegion.php @@ -0,0 +1,22 @@ +hasMany(\App\Models\Regionalgroups\Regionalgroup::class, 'fir_id', 'id'); + } + +} diff --git a/app/Models/Regionalgroups/Regionalgroup.php b/app/Models/Regionalgroups/Regionalgroup.php new file mode 100644 index 0000000..76390f8 --- /dev/null +++ b/app/Models/Regionalgroups/Regionalgroup.php @@ -0,0 +1,189 @@ +belongsTo(FlightInformationRegion::class, 'fir_id', 'id'); + } + + /** + * All associated accounts + * Regardless of guest status + * + * @return [type] [description] + */ + public function accounts() + { + return $this->belongsToMany(Account::class, 'regionalgroups_account_regionalgroup', 'regionalgroup_id', 'account_id') + ->withPivot('pilot', 'controller', 'guest') + ->withTimestamps(); + } + + /** + * Only full regionalgroup members + * + * @return [type] [description] + */ + public function getMembersAttribute() + { + return $this->accounts->reject(function ($acc) { + return $acc->pivot->guest; + }); + } + public function getMembersCountAttribute() + { + return count($this->getMembersAttribute()); + } + + /** + * Only guest members of the regionalgroup + * + * @return [type] [description] + */ + public function getGuestsAttribute() + { + return $this->accounts->filter(function ($acc) { + return $acc->pivot->guest; + }); + } + public function getGuestsCountAttribute() + { + return count($this->getGuestsAttribute()); + } + + /** + * Only members (guest or full) that are assigned as controllers + * + * @return [type] [description] + */ + public function getControllersAttribute() + { + return $this->accounts->filter(function ($acc) { + return $acc->pivot->controller; + }); + } + + /** + * Members or guests that are assigned as pilots + * + * @return [type] [description] + */ + public function getPilotsAttribute() + { + return $this->accounts->filter(function ($acc) { + return $acc->pivot->pilot; + }); + } + + /** + * The current regionalgroup chief + * + * @return [type] [description] + */ + public function chief() + { + return $this->belongsTo(Account::class, 'chief_id', 'id'); + } + + /** + * The deputy of the regionalgroup + * + * @return [type] [description] + */ + public function deputy() + { + return $this->belongsTo(Account::class, 'deputy_id', 'id'); + } + + /** + * All atc/atd mentors of the regionalgroup + * + * @return [type] [description] + */ + public function mentors() + { + return $this->belongsToMany(Account::class, 'regionalgroups_mentors', 'regionalgroup_id', 'account_id') + ->withPivot('chief', 'senior'); + } + + /** + * All members that are participating in the navigation team of the regionalgroup + * + * @return [type] [description] + */ + public function navigators() + { + return $this->belongsToMany(Account::class, 'regionalgroups_navigators', 'regionalgroup_id', 'account_id') + ->withPivot('chief', 'deputy'); + } + + /** + * The event team of the regionalgroup + * + * @return [type] [description] + */ + public function eventler() + { + return $this->belongsToMany(Account::class, 'regionalgroups_eventler', 'regionalgroup_id', 'account_id') + ->withPivot('chief', 'deputy'); + } + + /** + * All requests that have been stated to the regionalgroup + * + * @return [type] [description] + */ + public function requests() + { + return $this->hasMany(RegionalgroupRequest::class, 'regionalgroup_id', 'id'); + } + + /** + * All aerodromes that are assigned to this regionalgroup + * + * @return [type] [description] + */ + public function aerodromes() + { + return $this->belongsToMany(Aerodrome::class, 'navigation_aerodrome_regionalgroup', 'aerodrome_id', 'regionalgroup_id'); + } + + /** + * All templates that are assigned to this regionalgroup + * + * @return [type] [description] + */ + public function templates() + { + return $this->hasMany(RegionalgroupTemplate::class, 'regionalgroup_id', 'id'); + } + +} diff --git a/app/Models/Regionalgroups/RegionalgroupRequest.php b/app/Models/Regionalgroups/RegionalgroupRequest.php new file mode 100644 index 0000000..6d850fd --- /dev/null +++ b/app/Models/Regionalgroups/RegionalgroupRequest.php @@ -0,0 +1,43 @@ +belongsTo(Regionalgroup::class, 'regionalgroup_id', 'id')->select(['id', 'name']); + } + + /** + * The account that made the request + * + * @return [type] [description] + */ + public function account() + { + return $this->belongsTo(Account::class, 'account_id', 'id'); + } + + /** + * The targeting regionalgroup in case of change requests + * @return [type] [description] + */ + public function destination() + { + return $this->belongsTo(Regionalgroup::class, 'destination_id', 'id'); + } + +} diff --git a/app/Models/Regionalgroups/RegionalgroupTemplate.php b/app/Models/Regionalgroups/RegionalgroupTemplate.php new file mode 100644 index 0000000..71b4e1f --- /dev/null +++ b/app/Models/Regionalgroups/RegionalgroupTemplate.php @@ -0,0 +1,23 @@ +belongsTo(Regionalgroup::class, 'regionalgroup_id', 'id')->select(['id', 'name']); + } + +} diff --git a/app/Models/Statistic/AtcData.php b/app/Models/Statistic/AtcData.php new file mode 100644 index 0000000..886a1f5 --- /dev/null +++ b/app/Models/Statistic/AtcData.php @@ -0,0 +1,45 @@ +where('callsign', 'LIKE', '%'.$callsign.'%'); + } + + public function scopeCid($query, $cid) + { + return $query->where('account_id', $cid); + } + + public function scopeFrequency($query, $fq) + { + return $query->where('frequency', $fq); + } + +} diff --git a/app/Models/Statistic/FlightData.php b/app/Models/Statistic/FlightData.php new file mode 100644 index 0000000..d174aef --- /dev/null +++ b/app/Models/Statistic/FlightData.php @@ -0,0 +1,58 @@ +where('callsign', 'LIKE', $callsign.'%'); + } + + public function scopeCid($query, $cid) + { + return $query->where('account_id', $cid); + } + + public function scopeIcao($query, $icao) + { + return $query->where('arrival_airport', $icao)->orWhere('departure_airport', $icao); + } + + public function scopeCompleted($query) + { + return $query->whereNotNull('departed_at')->whereNotNull('arrived_at'); + } + +} diff --git a/app/Models/SurveyKey.php b/app/Models/SurveyKey.php new file mode 100644 index 0000000..5f36ab7 --- /dev/null +++ b/app/Models/SurveyKey.php @@ -0,0 +1,19 @@ + 'datetime', + ]; + + public function account() + { + return $this->belongsTo(Account::class, 'id', 'user_id'); + } +} diff --git a/app/Models/TeamSpeak/Confirmation.php b/app/Models/TeamSpeak/Confirmation.php new file mode 100644 index 0000000..c0830c7 --- /dev/null +++ b/app/Models/TeamSpeak/Confirmation.php @@ -0,0 +1,19 @@ +belongsTo(\App\Models\Membership\TeamSpeak\Registration::class, 'registration_id', 'id'); + } +} diff --git a/app/Models/TeamSpeak/Registration.php b/app/Models/TeamSpeak/Registration.php new file mode 100644 index 0000000..4d66dae --- /dev/null +++ b/app/Models/TeamSpeak/Registration.php @@ -0,0 +1,56 @@ + '0.0.0.0', 'last_ip' => '0.0.0.0']; + + protected $dates = ['created_at', 'updated_at']; + + /** + * Delete a registration while cascading the confirmation. + * + * @return [type] [description] + */ + public function delete() + { + if ($this->confirmation) { + $this->confirmation->delete(); + } + parent::delete(); + } + + /** + * The confirmation to this registration. + * + * @return [type] [description] + */ + public function confirmation() + { + return $this->hasOne(\App\Models\TeamSpeak\Confirmation::class, 'registration_id', 'id'); + } + + /** + * The associated account. + * + * @return [type] [description] + */ + public function account() + { + return $this->belongsTo(\App\Models\Membership\Account::class, 'account_id'); + } +} diff --git a/app/Models/UTS/ATD/Training.php b/app/Models/UTS/ATD/Training.php new file mode 100644 index 0000000..e63c441 --- /dev/null +++ b/app/Models/UTS/ATD/Training.php @@ -0,0 +1,32 @@ +belongsTo(\App\Models\Membership\Account::class, 'trainee_id', 'id'); + } + + public function regionalgroup() + { + return $this->belongsTo(\App\Models\Regionalgroups\Regionalgroup::class, 'regionalgroup_id', 'id'); + } + + public function sessions() + { + return $this->hasMany(\App\Models\UTS\ATD\TrainingSession::class, 'training_id', 'id'); + } + + public function scopeForTrainee($query, $cid) + { + return $query->where('trainee_id', $cid); + } + +} diff --git a/app/Models/UTS/ATD/TrainingSession.php b/app/Models/UTS/ATD/TrainingSession.php new file mode 100644 index 0000000..83e8169 --- /dev/null +++ b/app/Models/UTS/ATD/TrainingSession.php @@ -0,0 +1,64 @@ +belongsTo(\App\Models\UTS\ATD\Training::class, 'training_id', 'id'); + } + + public function mentor() + { + return $this->belongsTo(\App\Models\Membership\Account::class, 'mentor_id', 'id'); + } + + public function secondMentor() + { + return $this->belongsTo(\App\Models\Membership\Account::class, 'second_mentor_id', 'id'); + } + + public function station() + { + return $this->belongsTo(\App\Models\Navigation\Station::class, 'station_id', 'id'); + } + + public function getTypeStringAttribute() + { + $tts = ''; + switch ($this->type) { + case self::TYPE_THEORY: + $tts = 'Theory Session'; + break; + case self::TYPE_SIM: + $tts = 'Sim Session'; + break; + case self::TYPE_ONLINE: + $tts = 'Online Session'; + break; + default: + break; + } + return $tts; + } + +} diff --git a/app/Notifications/ATD/NoMemberOfRegionalgroupNotification.php b/app/Notifications/ATD/NoMemberOfRegionalgroupNotification.php new file mode 100644 index 0000000..22b28eb --- /dev/null +++ b/app/Notifications/ATD/NoMemberOfRegionalgroupNotification.php @@ -0,0 +1,64 @@ +_rgName = $rgName; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['broadcast']; + } + + /** + * Get the broadcast representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Du bist kein Mitglied oder Gastmitglied der Regionalgruppe '.$this->_rgName.'.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Du bist kein Mitglied oder Gastmitglied der Regionalgruppe '.$this->_rgName.'.', + ]; + } +} diff --git a/app/Notifications/ATD/SeeForumNotification.php b/app/Notifications/ATD/SeeForumNotification.php new file mode 100644 index 0000000..da8b3a9 --- /dev/null +++ b/app/Notifications/ATD/SeeForumNotification.php @@ -0,0 +1,56 @@ + 'ATD Verwaltung', + 'message' => 'Um ein Training anzufordern, schau bitte in unserem Forum vorbei.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Um ein Training anzufordern, schau bitte in unserem Forum vorbei.', + ]; + } +} diff --git a/app/Notifications/ATD/SessionAlreadyRequestedNotification.php b/app/Notifications/ATD/SessionAlreadyRequestedNotification.php new file mode 100644 index 0000000..4396bcd --- /dev/null +++ b/app/Notifications/ATD/SessionAlreadyRequestedNotification.php @@ -0,0 +1,56 @@ + 'ATD Verwaltung', + 'message' => 'Du hast bereits ein Trainingssitzung in dieser Regionalgruppe angefragt.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Du hast bereits ein Trainingssitzung in dieser Regionalgruppe angefragt.', + ]; + } +} diff --git a/app/Notifications/ATD/SoloApprovedNotification.php b/app/Notifications/ATD/SoloApprovedNotification.php new file mode 100644 index 0000000..878c4af --- /dev/null +++ b/app/Notifications/ATD/SoloApprovedNotification.php @@ -0,0 +1,58 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + public function toBroadcast($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde freigegeben.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde freigegeben.', + ]; + } +} diff --git a/app/Notifications/ATD/SoloDeniedNotification.php b/app/Notifications/ATD/SoloDeniedNotification.php new file mode 100644 index 0000000..4d9c007 --- /dev/null +++ b/app/Notifications/ATD/SoloDeniedNotification.php @@ -0,0 +1,58 @@ +station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + public function toBroadcast($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->station.' wurde abgelehnt.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->station.' wurde abgelehnt.', + ]; + } +} diff --git a/app/Notifications/ATD/SoloExtendedNotification.php b/app/Notifications/ATD/SoloExtendedNotification.php new file mode 100644 index 0000000..475cad4 --- /dev/null +++ b/app/Notifications/ATD/SoloExtendedNotification.php @@ -0,0 +1,58 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + public function toBroadcast($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde verlängert.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde verlängert.', + ]; + } +} diff --git a/app/Notifications/ATD/SoloModifiedNotification.php b/app/Notifications/ATD/SoloModifiedNotification.php new file mode 100644 index 0000000..450d332 --- /dev/null +++ b/app/Notifications/ATD/SoloModifiedNotification.php @@ -0,0 +1,58 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + public function toBroadcast($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde verändert. Bitte informiere dich über die Änderungen.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Deine Solofreigabe für '.$this->_station.' wurde verändert. Bitte informiere dich über die Änderungen.', + ]; + } +} diff --git a/app/Notifications/ATD/TrainingAlreadyRequestedNotification.php b/app/Notifications/ATD/TrainingAlreadyRequestedNotification.php new file mode 100644 index 0000000..f0dc1e7 --- /dev/null +++ b/app/Notifications/ATD/TrainingAlreadyRequestedNotification.php @@ -0,0 +1,56 @@ + 'ATD Verwaltung', + 'message' => 'Du hast bereits ein Training in dieser Regionalgruppe.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'ATD Verwaltung', + 'message' => 'Du hast bereits ein Training in dieser Regionalgruppe.', + ]; + } +} diff --git a/app/Notifications/Administration/Forum/GroupAlreadyExistsNotification.php b/app/Notifications/Administration/Forum/GroupAlreadyExistsNotification.php new file mode 100644 index 0000000..55433f7 --- /dev/null +++ b/app/Notifications/Administration/Forum/GroupAlreadyExistsNotification.php @@ -0,0 +1,82 @@ +_forumgroupId = $id; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Forumgruppen Manager', + 'message' => 'Die Forengruppe mit der ID '.$this->_forumgroupId.' existiert bereits. Sie kann kein weiteres mal angelegt werden.', + 'station' => $this->_forumgroupId, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Forumgruppen Manager', + 'message' => 'Die Forengruppe mit der ID '.$this->_forumgroupId.' existiert bereits. Sie kann kein weiteres mal angelegt werden.', + 'station' => $this->_forumgroupId, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Forumgruppen Manager', + 'message' => 'Die Forengruppe mit der ID '.$this->_forumgroupId.' existiert bereits. Sie kann kein weiteres mal angelegt werden.', + 'station' => $this->_forumgroupId, + ]; + } +} \ No newline at end of file diff --git a/app/Notifications/Administration/Navigation/AerodromeUpdatedNotification.php b/app/Notifications/Administration/Navigation/AerodromeUpdatedNotification.php new file mode 100644 index 0000000..6a8b409 --- /dev/null +++ b/app/Notifications/Administration/Navigation/AerodromeUpdatedNotification.php @@ -0,0 +1,87 @@ +_aerodrome = $aerodrome; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Flugplatzdaten aktualisiert', + 'message' => 'Die Daten des Flugplatzes '.$this->_aerodrome->name.' wurden überarbeitet.', + 'aerodrome' => $this->_aerodrome, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Flugplatzdaten aktualisiert', + 'message' => 'Die Daten des Flugplatzes '.$this->_aerodrome->name.' wurden überarbeitet.', + 'aerodrome' => $this->_aerodrome, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Flugplatzdaten aktualisiert', + 'message' => 'Die Daten des Flugplatzes '.$this->_aerodrome->name.' wurden überarbeitet.', + 'aerodrome' => $this->_aerodrome, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/ChartCreatedNotification.php b/app/Notifications/Administration/Navigation/ChartCreatedNotification.php new file mode 100644 index 0000000..00ea402 --- /dev/null +++ b/app/Notifications/Administration/Navigation/ChartCreatedNotification.php @@ -0,0 +1,83 @@ +_chart = $chart; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Chart angelegt', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich angelegt.', + 'station' => $this->_chart, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Chart angelegt', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich angelegt.', + 'station' => $this->_chart, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Chart angelegt', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich angelegt.', + 'station' => $this->_chart, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/ChartRemovedNotification.php b/app/Notifications/Administration/Navigation/ChartRemovedNotification.php new file mode 100644 index 0000000..48c04cf --- /dev/null +++ b/app/Notifications/Administration/Navigation/ChartRemovedNotification.php @@ -0,0 +1,82 @@ +_chart = $chart; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Chart gelöscht', + 'message' => 'Die Chart '.$this->_chart.' wurde erfolgreich gelöscht.', + 'station' => $this->_chart, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Chart gelöscht', + 'message' => 'Die Chart '.$this->_chart.' wurde erfolgreich gelöscht.', + 'station' => $this->_chart, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Chart gelöscht', + 'message' => 'Die Chart '.$this->_chart.' wurde erfolgreich gelöscht.', + 'station' => $this->_chart, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/ChartUpdatedNotification.php b/app/Notifications/Administration/Navigation/ChartUpdatedNotification.php new file mode 100644 index 0000000..aa0b8f5 --- /dev/null +++ b/app/Notifications/Administration/Navigation/ChartUpdatedNotification.php @@ -0,0 +1,83 @@ +_chart = $chart; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Chart aktualisiert', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich aktualisiert.', + 'station' => $this->_chart, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Chart aktualisiert', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich aktualisiert.', + 'station' => $this->_chart, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Chart aktualisiert', + 'message' => 'Die Chart '.$this->_chart->name.' wurde erfolgreich aktualisiert.', + 'station' => $this->_chart, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/NavaidCreatedNotification.php b/app/Notifications/Administration/Navigation/NavaidCreatedNotification.php new file mode 100644 index 0000000..fc0c7de --- /dev/null +++ b/app/Notifications/Administration/Navigation/NavaidCreatedNotification.php @@ -0,0 +1,82 @@ +_navaid = $navaid; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Navaid angelegt', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich angelegt.', + 'navaid' => $this->_navaid, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Navaid angelegt', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich angelegt.', + 'navaid' => $this->_navaid, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Navaid angelegt', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich angelegt.', + 'navaid' => $this->_navaid, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/NavaidDeletedNotification.php b/app/Notifications/Administration/Navigation/NavaidDeletedNotification.php new file mode 100644 index 0000000..48c5bf1 --- /dev/null +++ b/app/Notifications/Administration/Navigation/NavaidDeletedNotification.php @@ -0,0 +1,82 @@ +_navaid = $navaid; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Navaid gelöscht', + 'message' => 'Das Navaid '.$this->_navaid.' wurde erfolgreich gelöscht.', + 'navaid' => $this->_navaid, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Navaid gelöscht', + 'message' => 'Das Navaid '.$this->_navaid.' wurde erfolgreich gelöscht.', + 'navaid' => $this->_navaid, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Navaid gelöscht', + 'message' => 'Das Navaid '.$this->_navaid.' wurde erfolgreich gelöscht.', + 'navaid' => $this->_navaid, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/NavaidUpdatedNotification.php b/app/Notifications/Administration/Navigation/NavaidUpdatedNotification.php new file mode 100644 index 0000000..a77b3b4 --- /dev/null +++ b/app/Notifications/Administration/Navigation/NavaidUpdatedNotification.php @@ -0,0 +1,82 @@ +_navaid = $navaid; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Navaid geändert', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich geändert.', + 'navaid' => $this->_navaid, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Navaid geändert', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich geändert.', + 'navaid' => $this->_navaid, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Navaid geändert', + 'message' => 'Das Navaid '.$this->_navaid->name.' wurde erfolgreich geändert.', + 'navaid' => $this->_navaid, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/StationCreatedNotification.php b/app/Notifications/Administration/Navigation/StationCreatedNotification.php new file mode 100644 index 0000000..2d02eff --- /dev/null +++ b/app/Notifications/Administration/Navigation/StationCreatedNotification.php @@ -0,0 +1,83 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Station angelegt', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich angelegt.', + 'station' => $this->_station, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Station angelegt', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich angelegt.', + 'station' => $this->_station, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Station angelegt', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich angelegt.', + 'station' => $this->_station, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/StationRemovedNotification.php b/app/Notifications/Administration/Navigation/StationRemovedNotification.php new file mode 100644 index 0000000..09b5f29 --- /dev/null +++ b/app/Notifications/Administration/Navigation/StationRemovedNotification.php @@ -0,0 +1,82 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Station gelöscht', + 'message' => 'Die Station '.$this->_station.' wurde erfolgreich gelöscht.', + 'station' => $this->_station, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Station gelöscht', + 'message' => 'Die Station '.$this->_station.' wurde erfolgreich gelöscht.', + 'station' => $this->_station, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Station gelöscht', + 'message' => 'Die Station '.$this->_station.' wurde erfolgreich gelöscht.', + 'station' => $this->_station, + ]; + } +} diff --git a/app/Notifications/Administration/Navigation/StationUpdatedNotification.php b/app/Notifications/Administration/Navigation/StationUpdatedNotification.php new file mode 100644 index 0000000..a1bd6d6 --- /dev/null +++ b/app/Notifications/Administration/Navigation/StationUpdatedNotification.php @@ -0,0 +1,83 @@ +_station = $station; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // return ['database', 'broadcast']; + return ['broadcast']; // Only broadcast to the causer of the event + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Station aktualisiert', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich aktualisiert.', + 'station' => $this->_station, + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Station aktualisiert', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich aktualisiert.', + 'station' => $this->_station, + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Station aktualisiert', + 'message' => 'Die Station '.$this->_station->ident.' wurde erfolgreich aktualisiert.', + 'station' => $this->_station, + ]; + } +} diff --git a/app/Notifications/Administration/Regionalgroups/RequestAcceptedNotification.php b/app/Notifications/Administration/Regionalgroups/RequestAcceptedNotification.php new file mode 100644 index 0000000..a5b52ca --- /dev/null +++ b/app/Notifications/Administration/Regionalgroups/RequestAcceptedNotification.php @@ -0,0 +1,86 @@ +_regionalgroup = $regionalgroup; + $this->_details = $details; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde angenommen.', + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde angenommen. Bitte erlaube bis zu 12h bis alle Systeme die Änderungen übernommen haben.', + 'details' => $this->_details, + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde angenommen. Bitte erlaube bis zu 12h bis alle Systeme die Änderungen übernommen haben.', + 'details' => $this->_details, + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]; + } +} diff --git a/app/Notifications/Administration/Regionalgroups/RequestDeniedNotification.php b/app/Notifications/Administration/Regionalgroups/RequestDeniedNotification.php new file mode 100644 index 0000000..675cc56 --- /dev/null +++ b/app/Notifications/Administration/Regionalgroups/RequestDeniedNotification.php @@ -0,0 +1,87 @@ +_regionalgroup = $regionalgroup; + $this->_details = $details; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'broadcast']; + } + + /** + * Get the broadcastable representation of the notification. + * + * @param mixed $notifiable + * @return BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde abgelehnt.', + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]); + } + + /** + * Get the database format of the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde abgelehnt.', + 'details' => $this->_details, + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Regionalgruppenanfrage', + 'message' => 'Die Anfrage an die Regionalgruppe '.$this->_regionalgroup->name.' wurde abgelehnt.', + 'details' => $this->_details, + 'regionalgroup' => $this->_regionalgroup->only(['id', 'name']), + ]; + } +} diff --git a/app/Notifications/Booking/AtcBookingCreatedNotification.php b/app/Notifications/Booking/AtcBookingCreatedNotification.php new file mode 100644 index 0000000..3c6fab2 --- /dev/null +++ b/app/Notifications/Booking/AtcBookingCreatedNotification.php @@ -0,0 +1,61 @@ + 'ATC Session Booking', + 'message' => 'Buchung wurde erfolgreich gespeichert.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'message' => 'Buchung wurde erfolgreich gespeichert.', + ]; + } +} diff --git a/app/Notifications/Booking/AtcBookingDeletedNotification.php b/app/Notifications/Booking/AtcBookingDeletedNotification.php new file mode 100644 index 0000000..c8d045d --- /dev/null +++ b/app/Notifications/Booking/AtcBookingDeletedNotification.php @@ -0,0 +1,61 @@ + 'ATC Session Booking', + 'message' => 'Buchung wurde erfolgreich gelöscht.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'message' => 'Buchung wurde erfolgreich gelöscht.', + ]; + } +} diff --git a/app/Notifications/Booking/AtcBookingUpdateNotification.php b/app/Notifications/Booking/AtcBookingUpdateNotification.php new file mode 100644 index 0000000..3ec938c --- /dev/null +++ b/app/Notifications/Booking/AtcBookingUpdateNotification.php @@ -0,0 +1,61 @@ + 'ATC Session Booking', + 'message' => 'Buchung wurde erfolgreich aktualisiert.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'message' => 'Buchung wurde erfolgreich aktualisiert.', + ]; + } +} diff --git a/app/Notifications/Forum/AccountUpdatedNotification.php b/app/Notifications/Forum/AccountUpdatedNotification.php new file mode 100644 index 0000000..a74bdd2 --- /dev/null +++ b/app/Notifications/Forum/AccountUpdatedNotification.php @@ -0,0 +1,81 @@ +_forum_groups = $forumGroups; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database']; + } + + /** + * Get the database representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'Forum', + 'message' => 'Forenaccount wurde synchronisiert. Folgende Gruppen wurden zugeordnet: '.$this->forumGroupsToString(), + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'message' => 'Forenaccount wurde synchronisiert. Folgende Gruppen wurden zugeordnet: '.$this->forumGroupsToString(), + ]; + } + + /** + * Convert the forum groups array to a readable string + * + * @return [type] [description] + */ + private function forumGroupsToString() { + $fgrps = \App\Models\Forum\ForumGroup::whereIn('forum_id', $this->_forum_groups)->select('name')->get(); + + $result = ''; + foreach ($fgrps as $fg) { + if($result == '') $result.= $fg->name; + else $result.= ', '.$fg->name; + } + return $result; + } +} diff --git a/app/Notifications/Membership/PasswordUpdatedNotification.php b/app/Notifications/Membership/PasswordUpdatedNotification.php new file mode 100644 index 0000000..169f23d --- /dev/null +++ b/app/Notifications/Membership/PasswordUpdatedNotification.php @@ -0,0 +1,63 @@ + 'Account Manager', + 'message' => 'Dein Passwort wurde erfolgreich geändert.' + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Dein Passwort wurde erfolgreich geändert.' + ]; + } +} diff --git a/app/Notifications/Membership/TeamSpeakIdentityCreated.php b/app/Notifications/Membership/TeamSpeakIdentityCreated.php new file mode 100644 index 0000000..83ef141 --- /dev/null +++ b/app/Notifications/Membership/TeamSpeakIdentityCreated.php @@ -0,0 +1,64 @@ +_tsid = $tsid; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['broadcast']; + } + + /** + * Get the broadcast representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Die ID ' . $this->_tsid . ' wurde mit deinem Account verbunden.' + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Die ID ' . $this->_tsid . ' wurde mit deinem Account verbunden.' + ]; + } +} diff --git a/app/Notifications/Membership/TeamSpeakIdentityDeleted.php b/app/Notifications/Membership/TeamSpeakIdentityDeleted.php new file mode 100644 index 0000000..adb5362 --- /dev/null +++ b/app/Notifications/Membership/TeamSpeakIdentityDeleted.php @@ -0,0 +1,64 @@ +_tsid = $tsid; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['broadcast']; + } + + /** + * Get the broadcast representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Die ID ' . $this->_tsid . ' wurde von deinem Account gelöst.' + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Die ID ' . $this->_tsid . ' wurde von deinem Account gelöst.' + ]; + } +} diff --git a/app/Notifications/Membership/TeamSpeakRegistrationCreated.php b/app/Notifications/Membership/TeamSpeakRegistrationCreated.php new file mode 100644 index 0000000..afbd7d3 --- /dev/null +++ b/app/Notifications/Membership/TeamSpeakRegistrationCreated.php @@ -0,0 +1,64 @@ +_ip = $ip; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['broadcast']; + } + + /** + * Get the broadcast representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\BroadcastMessage + */ + public function toBroadcast($notifiable) + { + return new BroadcastMessage([ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Eine neue TeamSpeak Registrierung wurde für deinen Account angefragt. Absender: ' . $this->_ip + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'TeamSpeak Identity Manager', + 'message' => 'Eine neue TeamSpeak Registrierung wurde für deinen Account angefragt. Absender: ' . $this->_ip + ]; + } +} diff --git a/app/Notifications/Navigation/GoogleEarthKMZParsed.php b/app/Notifications/Navigation/GoogleEarthKMZParsed.php new file mode 100644 index 0000000..ee696cb --- /dev/null +++ b/app/Notifications/Navigation/GoogleEarthKMZParsed.php @@ -0,0 +1,78 @@ +_sectorFileLocation = $fileName; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['broadcast', 'database']; + } + + /** + * Get the broadcast representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toBroadcast($notifiable) + { + return [ + 'title' => 'GoogleEarth Parsed', + 'message' => 'The parsing of the GE file is completed. See notifications details for further information.', + ]; + } + + /** + * Get the database format for the notification + * + * @param [type] $notifiable [description] + * @return [type] [description] + */ + public function toDatabase($notifiable) + { + return [ + 'title' => 'GoogleEarth Parsed', + 'message' => 'The parsing of the GE file is completed. See notifications details for further information.', + 'details' => 'Download results from here: Download' + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/Regionalgroups/RegionalgroupRequestExistsNotification.php b/app/Notifications/Regionalgroups/RegionalgroupRequestExistsNotification.php new file mode 100644 index 0000000..f8e83d4 --- /dev/null +++ b/app/Notifications/Regionalgroups/RegionalgroupRequestExistsNotification.php @@ -0,0 +1,62 @@ + 'Regionalgroup Request Manager', + 'message' => 'Du hast bereits eine Anfrage an diese Regionalgruppe gestellt. Bitte warte auf deren Bearbeitung!', + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'title' => 'Regionalgroup Request Manager', + 'message' => 'Du hast bereits eine Anfrage an diese Regionalgruppe gestellt. Bitte warte auf deren Bearbeitung!', + ]; + } +} diff --git a/app/Notifications/System/GdprUpdatedNotification.php b/app/Notifications/System/GdprUpdatedNotification.php new file mode 100644 index 0000000..2457372 --- /dev/null +++ b/app/Notifications/System/GdprUpdatedNotification.php @@ -0,0 +1,61 @@ + 'GDPR', + 'message' => 'Datenschutzerklärung wurde überarbeitet.', + ]; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'message' => 'Datenschutzerklärung wurde überarbeitet.', + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..8e60df2 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,46 @@ +register([ + // \App\Charts\Statistics\Aerodrome\TrafficChart::class, + // \App\Charts\Statistics\Flightdata\ActivityChart::class, + //]); + + Queue::failing(function (JobFailed $event) { + \Log::channel('joberror')->error($event->connectionName); + \Log::channel('joberror')->error($event->job->getName()); + \Log::channel('joberror')->error($event->exception->getMessage()); + }); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..3049068 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,30 @@ + 'App\Policies\ModelPolicy', + ]; + + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + $this->registerPolicies(); + + // + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..395c518 --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,21 @@ + [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + parent::boot(); + + // + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..ee54c92 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,94 @@ +registerCustomRouteBindings(); + } + + /** + * Define the routes for the application. + * + * @return void + */ + public function map() + { + $this->mapApiRoutes(); + + $this->mapWebRoutes(); + + // + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + * + * @return void + */ + protected function mapWebRoutes() + { + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + * + * @return void + */ + protected function mapApiRoutes() + { + Route::prefix('api') + ->middleware('api') + ->namespace($this->namespace) + ->group(base_path('routes/api.php')); + } + + /** + * Place to register our custom route model bindings + * + * @return void + */ + private function registerCustomRouteBindings() + { + Route::bind('aerodromeByIcao', function ($value) { + return \App\Models\Navigation\Aerodrome::icao($value)->first() ?? abort(404); + }); + } +} diff --git a/app/Providers/Vatsim/VatauthProvider.php b/app/Providers/Vatsim/VatauthProvider.php new file mode 100644 index 0000000..acf6779 --- /dev/null +++ b/app/Providers/Vatsim/VatauthProvider.php @@ -0,0 +1,51 @@ + config('vatsim_auth.id'), // The client ID assigned to you by the provider + 'clientSecret' => config('vatsim_auth.secret'), // The client password assigned to you by the provider + 'redirectUri' => route($this->_redirectAfterAuth), + 'urlAuthorize' => config('vatsim_auth.base').'/oauth/authorize', + 'urlAccessToken' => config('vatsim_auth.base').'/oauth/token', + 'urlResourceOwnerDetails' => config('vatsim_auth.base').'/api/user', + 'scopes' => config('vatsim_auth.scopes'), + 'scopeSeparator' => ' ' + ] + ); + } + + public static function updateToken($token) + { + $controller = new VatauthProvider; + + try { + return $controller->getAccessToken( + 'refresh_token', + [ + 'refresh_token' => $token->getRefreshToken() + ] + ); + } catch (IdentityProviderException $e) { + return null; + } + } + +} diff --git a/app/Rules/Navigation/FrequencyRule.php b/app/Rules/Navigation/FrequencyRule.php new file mode 100644 index 0000000..8d1acba --- /dev/null +++ b/app/Rules/Navigation/FrequencyRule.php @@ -0,0 +1,40 @@ += 0 && $hdg <= 360) return true; + return false; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return trans('administration.validation.navigation.heading.limits'); + } +} diff --git a/app/Rules/StrongPasswordRule.php b/app/Rules/StrongPasswordRule.php new file mode 100644 index 0000000..fcb6744 --- /dev/null +++ b/app/Rules/StrongPasswordRule.php @@ -0,0 +1,40 @@ +,.\/~`±§+-äöü]).{10,256}$/u", $value) === 1; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return trans('settings.validation.password.complexity'); + } +} diff --git a/artisan b/artisan new file mode 100644 index 0000000..5c23e2e --- /dev/null +++ b/artisan @@ -0,0 +1,53 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a166608 --- /dev/null +++ b/composer.json @@ -0,0 +1,82 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The Laravel Framework.", + "keywords": [ + "framework", + "laravel" + ], + "license": "MIT", + "require": { + "php": "^7.4", + "ext-curl": "*", + "ext-json": "*", + "cobaltgrid/vatsim-stand-status": "^1.0", + "consoletvs/charts": "6.*", + "doctrine/dbal": "^2.10", + "fideloper/proxy": "^4.2", + "fruitcake/laravel-cors": "^1.0", + "genert/bbcode": "^1.1", + "guzzlehttp/guzzle": "^6.3", + "joedixon/laravel-translation": "^1.1", + "lab404/laravel-impersonate": "^1.7", + "laravel/framework": "^7.0", + "laravel/tinker": "^2.0", + "laravel/ui": "^2.0", + "league/oauth2-client": "^2.4", + "mateusjunges/laravel-acl": "^2.4", + "paragonie/paseto": "^1.0.3", + "planetteamspeak/ts3-php-framework": "dev-master", + "predis/predis": "^1.1", + "smalot/pdfparser": "^0.16.2", + "spatie/array-to-xml": "^2.13", + "spatie/laravel-activitylog": "^3.14", + "spatie/laravel-cookie-consent": "^2.12" + }, + "require-dev": { + "barryvdh/laravel-debugbar": "^3.3", + "facade/ignition": "^2.0", + "fzaninotto/faker": "^1.9.1", + "mockery/mockery": "^1.3.1", + "nunomaduro/collision": "^4.1", + "phpunit/phpunit": "^8.5" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "autoload": { + "psr-4": { + "App\\": "app/" + }, + "classmap": [ + "database/seeds", + "database/factories" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi" + ] + } +} diff --git a/composer.phar b/composer.phar new file mode 100644 index 0000000..decd087 Binary files /dev/null and b/composer.phar differ diff --git a/config/acl.php b/config/acl.php new file mode 100644 index 0000000..a9fd5e7 --- /dev/null +++ b/config/acl.php @@ -0,0 +1,95 @@ + [ + /* + | The model you want to use as User Model must use MateusJunges\ACL\Traits\UsersTrait + */ + 'user' => App\Models\Membership\Account::class, + + /* + | The model you want to use as Permission model must use the MateusJunges\ACL\Traits\PermissionsTrait + */ + 'permission' => Junges\ACL\Http\Models\Permission::class, + + /* + | The model you want to use as Group model must use the MateusJunges\ACL\Traits\GroupsTrait + */ + 'group' => App\Models\Membership\Group::class, + ], + + /* + |-------------------------------------------------------------------------- + | Route Model Binding + |-------------------------------------------------------------------------- + | + | If you would like model binding to use a database column other than id when + | retrieving a given model class, you may override the getRouteKeyName method + | on the Eloquent model with yours. The default key used for route model binding + | in this package is the `slug` database column. You can modify it by changing the + | following configuration: + | + */ + 'route_model_binding_keys' => [ + 'group_model' => 'slug', + 'permission_model' => 'slug', + ], + + /* + |-------------------------------------------------------------------------- + | Tables + |-------------------------------------------------------------------------- + | Specify the basics authentication tables that you are using. + | Once you required this package, the following tables are + | created by default when you run the command + | + | php artisan migrate + | + | If you want to change this tables, please keep the basic structure unchanged. + | + */ + 'tables' => [ + 'groups' => 'membership_groups', + 'permissions' => 'membership_permissions', + 'users' => 'membership_accounts', + 'group_has_permissions' => 'membership_group_has_permissions', + 'user_has_permissions' => 'membership_account_has_permissions', + 'user_has_groups' => 'membership_account_has_groups', + ], + + /* + | + |If you want to customize your tables, set this flag to "true" + | */ + 'custom_migrations' => true, + + /* + | + | If you want to customize the admin-permission, you can change it here. + | By default, it is set to 'admin'. + */ + 'admin_permission' => 'superman', + + /* + |-------------------------------------------------------------------------- + | Ignition Solution Suggestions + |-------------------------------------------------------------------------- + | + | To enable the ignition solutions for laravel-acl, set this flag to true. + | + | The solutions will then be automatically registered with ignition if its installed. + | + */ + 'offer_solutions' => false, +]; diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000..a6558ec --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,52 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the default Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should be implements the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \Spatie\Activitylog\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => 'activity_log', + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..e50ab1d --- /dev/null +++ b/config/app.php @@ -0,0 +1,248 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Version + |-------------------------------------------------------------------------- + | + | This value is the version of your application. This value is used when the + | framework needs to place the application's version in a notification or + | any other location as required by the application or its packages. + | + */ + 'version' => env('APP_VERSION', '1.0'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL', null), + + + 'forcehttps' => env('APP_FORCE_HTTPS', false), + + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'de', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + + /* + * Package Service Providers... + */ + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => Illuminate\Support\Facades\App::class, + 'Arr' => Illuminate\Support\Arr::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Http' => Illuminate\Support\Facades\Http::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Str' => Illuminate\Support\Str::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..08b988d --- /dev/null +++ b/config/auth.php @@ -0,0 +1,118 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + 'remember' => 4320, // 3 days + ], + + 'api' => [ + 'driver' => 'token', + 'provider' => 'users', + 'hash' => false, + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\Membership\Account::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_resets', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + +]; diff --git a/config/booking.php b/config/booking.php new file mode 100644 index 0000000..df558dc --- /dev/null +++ b/config/booking.php @@ -0,0 +1,7 @@ + env('BOOKING_TESTING', true), + 'vatbookBaseUrl' => env('BOOKING_BASE_URL', 'http://vatbook.euroutepro.com/atc/'), + 'eventBaseUrl' => env('BOOKING_EVENT_BASE_URL', 'http://vatsim-germany.org'), +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..3bba110 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,59 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'useTLS' => true, + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..4f41fdf --- /dev/null +++ b/config/cache.php @@ -0,0 +1,104 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'cache', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + +]; diff --git a/config/charts.php b/config/charts.php new file mode 100644 index 0000000..84f2ccc --- /dev/null +++ b/config/charts.php @@ -0,0 +1,44 @@ + 'api/chart', + + /* + |-------------------------------------------------------------------------- + | Global Middlewares. + |-------------------------------------------------------------------------- + | + | This option allows to apply a list of middlewares to each and every + | chart created. This is commonly used if all your charts share some + | logic. For example, you might have all your charts under authentication + | middleware. If that's the case, applying a global middleware is a good + | choice rather than applying it individually to each chart. + | + */ + 'global_middlewares' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Global Route Name Prefix + |-------------------------------------------------------------------------- + | + | This option allows to modify the prefix used by all the chart route names. + | This is mostly used if there's the need to modify the route names that are + | binded to the charts. + | + */ + 'global_route_name_prefix' => 'charts', +]; diff --git a/config/cookie-consent.php b/config/cookie-consent.php new file mode 100644 index 0000000..2e707a2 --- /dev/null +++ b/config/cookie-consent.php @@ -0,0 +1,20 @@ + env('COOKIE_CONSENT_ENABLED', true), + + /* + * The name of the cookie in which we store if the user + * has agreed to accept the conditions. + */ + 'cookie_name' => 'vatger_cookie_consent', + + /* + * Set the cookie duration in days. Default is 365 * 20. + */ + 'cookie_lifetime' => 365 * 20, +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..558369d --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..4a162fc --- /dev/null +++ b/config/database.php @@ -0,0 +1,147 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + // 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..94c8112 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,85 @@ + env('FILESYSTEM_DRIVER', 'local'), + + /* + |-------------------------------------------------------------------------- + | Default Cloud Filesystem Disk + |-------------------------------------------------------------------------- + | + | Many applications store files both locally and in the cloud. For this + | reason, you may specify a default "cloud" driver here. This driver + | will be bound as the Cloud disk implementation in the container. + | + */ + + 'cloud' => env('FILESYSTEM_CLOUD', 's3'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + | Supported Drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/forum.php b/config/forum.php new file mode 100644 index 0000000..9acd36a --- /dev/null +++ b/config/forum.php @@ -0,0 +1,15 @@ + env('FORUM_URL', 'https://board.vatsim-germany.org'), + 'apikey' => env('FORUM_API_KEY', ''), + 'newsId' => env('FORUM_NEWS_THREAD', 97), + 'defaultGroup' => env('FORUM_DEFAULT_GROUP', 2), // 2 is the standard registered group id by default + 'suspendedGroup' => env('FORUM_SUSPENDED_GROUP', 56), // is the suspended group + 'guestGroup' => env('FORUM_GUEST_GROUP', 55), // if a user has no secondary group he gets this group + 'extraurl' => env('FORUM_EXTRA_API_URL', ''), + 'extratoken' => env('FORUM_EXTRA_API_KEY', ''), +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..8425770 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/laravel-impersonate.php b/config/laravel-impersonate.php new file mode 100644 index 0000000..b96a93a --- /dev/null +++ b/config/laravel-impersonate.php @@ -0,0 +1,41 @@ + 'impersonated_by', + + /** + * The session key used to stored the original user guard. + */ + 'session_guard' => 'impersonator_guard', + + /** + * The session key used to stored what guard is impersonator using. + */ + 'session_guard_using' => 'impersonator_guard_using', + + /** + * The default impersonator guard used. + */ + 'default_impersonator_guard' => 'web', + + /** + * The URI to redirect after taking an impersonation. + * + * Only used in the built-in controller. + * * Use 'back' to redirect to the previous page + */ + 'take_redirect_to' => '/membership', + + /** + * The URI to redirect after leaving an impersonation. + * + * Only used in the built-in controller. + * Use 'back' to redirect to the previous page + */ + 'leave_redirect_to' => '/administration/membership', + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..a69a934 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,116 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + 'days' => 14, + ], + + 'jobdaily' =>[ + 'driver' => 'daily', + 'path' => storage_path('logs/job.log'), + 'level' => 'debug', + 'days' => 7, + ], + 'joberror' =>[ + 'driver' => 'daily', + 'path' => storage_path('logs/joberror.log'), + 'level' => 'debug', + 'days' => 14, + ], + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => 'critical', + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => 'debug', + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => 'debug', + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => 'debug', + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..54299aa --- /dev/null +++ b/config/mail.php @@ -0,0 +1,110 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", + | "postmark", "log", "array" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'auth_mode' => null, + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + ], + + 'postmark' => [ + 'transport' => 'postmark', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => '/usr/sbin/sendmail -bs', + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/paseto.php b/config/paseto.php new file mode 100644 index 0000000..cf86a40 --- /dev/null +++ b/config/paseto.php @@ -0,0 +1,8 @@ + env('PASETO_KEY', null), +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..00b76d6 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,89 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..2a1d616 --- /dev/null +++ b/config/services.php @@ -0,0 +1,33 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..da692f3 --- /dev/null +++ b/config/session.php @@ -0,0 +1,199 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using the "apc", "memcached", or "dynamodb" session drivers you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => 'lax', + +]; diff --git a/config/teamspeak.php b/config/teamspeak.php new file mode 100644 index 0000000..3eccaeb --- /dev/null +++ b/config/teamspeak.php @@ -0,0 +1,14 @@ + env('TS_USER', 'serveradmin'), + 'pass' => env('TS_PASS', ''), + 'host' => env('TS_HOST', '127.0.0.1'), + 'query_port' => env('TS_QUERY_PORT', 10011), + 'port' => env('TS_PORT', 9987), + 'default_group' => env('TS_NEW_GROUP', 'Normal'), + 'apikey' => env('TS_APIKEY',''), + 'webquery_port' => env('TS_WEBQUERY_PORT', 10080), + 'server_number' => env('TS_SERVER_NR', 1), + 'homepage_api_key' => env('TS_HP_WEBAPIKEY'), +]; diff --git a/config/translation.php b/config/translation.php new file mode 100644 index 0000000..927960f --- /dev/null +++ b/config/translation.php @@ -0,0 +1,77 @@ + 'file', + + /* + |-------------------------------------------------------------------------- + | Route group configuration + |-------------------------------------------------------------------------- + | + | The package ships with routes to handle language management. Update the + | configuration here to configure the routes with your preferred group options. + | + */ + 'route_group_config' => [ + 'middleware' => ['languages', 'auth', 'setupCompleted', 'checkBanned'], + ], + + /* + |-------------------------------------------------------------------------- + | Translation methods + |-------------------------------------------------------------------------- + | + | Update this array to tell the package which methods it should look for + | when finding missing translations. + | + */ + 'translation_methods' => ['trans', '__', '@lang'], + + /* + |-------------------------------------------------------------------------- + | Scan paths + |-------------------------------------------------------------------------- + | + | Update this array to tell the package which directories to scan when + | looking for missing translations. + | + */ + 'scan_paths' => [app_path(), resource_path()], + + /* + |-------------------------------------------------------------------------- + | UI URL + |-------------------------------------------------------------------------- + | + | Define the URL used to access the language management too. + | + */ + 'ui_url' => 'administration/languages', + + /* + |-------------------------------------------------------------------------- + | Database settings + |-------------------------------------------------------------------------- + | + | Define the settings for the database driver here. + | + */ + 'database' => [ + + 'connection' => '', + + 'languages_table' => 'languages', + + 'translations_table' => 'translations', + ], +]; diff --git a/config/vatsim_api.php b/config/vatsim_api.php new file mode 100644 index 0000000..6604376 --- /dev/null +++ b/config/vatsim_api.php @@ -0,0 +1,11 @@ + env('VATSIM_API_BASE', 'https://api.vatsim.net/api'), + +]; diff --git a/config/vatsim_auth.php b/config/vatsim_auth.php new file mode 100644 index 0000000..19c7619 --- /dev/null +++ b/config/vatsim_auth.php @@ -0,0 +1,28 @@ + env('VATSIM_OAUTH_BASE', 'https://auth.vatsim.net'), + + /** + * Our consumer id. + */ + 'id' => env('VATSIM_OAUTH_CLIENT', ''), + + /** + * Our secret acces key. + * Do not give this to anyone else or display it to your users. It must be kept server-side. + */ + 'secret' => env('VATSIM_OAUTH_SECRET'), + + /** + * The datascopes to be retrieved + */ + 'scopes' => explode(',', env('VATSIM_OAUTH_SCOPES', 'full_name,email,vatsim_details,country')), + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..97fc976 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1,2 @@ +*.sqlite +*.sqlite-journal diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..741edea --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,28 @@ +define(User::class, function (Faker $faker) { + return [ + 'name' => $faker->name, + 'email' => $faker->unique()->safeEmail, + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]; +}); diff --git a/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..9bddee3 --- /dev/null +++ b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +id(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/database/migrations/2020_05_12_113601_create_aerodromes_table.php b/database/migrations/2020_05_12_113601_create_aerodromes_table.php new file mode 100644 index 0000000..8c164ba --- /dev/null +++ b/database/migrations/2020_05_12_113601_create_aerodromes_table.php @@ -0,0 +1,50 @@ +increments('id'); + $table->string('name'); + $table->text('description'); + $table->string('icao', 4)->unique(); + $table->string('iata', 3); + $table->string('country'); + $table->string('city'); + $table->string('state'); + $table->boolean('military')->default(false); + $table->boolean('civilian')->default(true); + $table->boolean('major')->default(false); + $table->boolean('active')->default(true); + $table->double('latitude', 12, 8)->nullable(); + $table->double('longitude', 12, 8)->nullable(); + $table->float('elevation')->default(0.00); + $table->text('departure_procedures')->nullable(); + $table->text('arrival_procedures')->nullable(); + $table->text('vfr_procedures')->nullable(); + $table->text('other_information')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('aerodromes'); + } +} diff --git a/database/migrations/2020_05_13_063301_networkdata_tables.php b/database/migrations/2020_05_13_063301_networkdata_tables.php new file mode 100644 index 0000000..8833011 --- /dev/null +++ b/database/migrations/2020_05_13_063301_networkdata_tables.php @@ -0,0 +1,94 @@ +integer('id')->primary(); // No autoincrement here. We do get only valid network id's + $table->string('firstname', 128); + $table->string('lastname', 128); + $table->integer('rating_atc'); + $table->integer('rating_pilot'); + $table->timestamps(); + }); + + Schema::create('networkdata_atc', function (Blueprint $table) { + $table->bigIncrements('id')->unsigned(); + $table->unsignedInteger('account_id'); // Reference to networkdata_accounts.id + $table->string('callsign', 12); + $table->double('frequency', 6, 3)->unsigned()->nullable(); + $table->smallInteger('qualification_id')->unsigned(); + $table->tinyInteger('facility_type')->unsigned(); + $table->timestamp('connected_at')->nullable(); + $table->timestamp('disconnected_at')->nullable(); + $table->integer('minutes_online')->unsigned()->nullable(); + $table->timestamps(); + }); + + // Schema::table('networkdata_atc', function (Blueprint $table) { + // $table->foreign('account_id')->references('id')->on('networkdata_accounts'); + // }); + + Schema::create('networkdata_pilots', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); // Reference to networkdata_accounts.id + $table->string('callsign', 10); + $table->string('flight_type', 1); + $table->string('departure_airport'); + $table->string('arrival_airport'); + $table->string('alternative_airport'); + $table->string('aircraft'); + $table->string('cruise_altitude'); + $table->string('cruise_tas'); + $table->text('route'); + $table->text('remarks'); + $table->double('current_latitude', 12, 8)->nullable(); + $table->double('current_longitude', 12, 8)->nullable(); + $table->mediumInteger('current_altitude')->nullable(); + $table->unsignedSmallInteger('current_groundspeed')->nullable(); + $table->unsignedSmallInteger('current_heading')->nullable(); + $table->timestamp('departed_at')->nullable(); + $table->timestamp('arrived_at')->nullable(); + $table->timestamp('connected_at')->nullable(); + $table->timestamp('disconnected_at')->nullable(); + $table->unsignedInteger('minutes_online')->nullable(); + $table->timestamps(); + }); + + // Schema::table('networkdata_pilots', function (Blueprint $table) { + // $table->foreign('account_id')->references('id')->on('networkdata_accounts'); + // }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // Schema::table('networkdata_pilots', function (Blueprint $table) { + // $table->dropForeign('networkdata_pilots_account_id_foreign'); + // }); + Schema::dropIfExists('networkdata_pilots'); + // Schema::table('networkdata_atc', function (Blueprint $table) { + // $table->dropForeign('networkdata_pilots_account_id_foreign'); + // }); + Schema::dropIfExists('networkdata_atc'); + Schema::dropIfExists('networkdata_accounts'); + } +} diff --git a/database/migrations/2020_05_15_071455_create_statistics_tables.php b/database/migrations/2020_05_15_071455_create_statistics_tables.php new file mode 100644 index 0000000..e7925eb --- /dev/null +++ b/database/migrations/2020_05_15_071455_create_statistics_tables.php @@ -0,0 +1,66 @@ +bigIncrements('id')->unsigned(); + $table->unsignedInteger('account_id')->default(0); // Reference to networkdata_accounts.id + $table->string('callsign', 12)->default(''); + $table->double('frequency', 6, 3)->unsigned()->nullable(); + $table->smallInteger('qualification_id')->unsigned(); + $table->tinyInteger('facility_type')->unsigned(); + $table->timestamp('connected_at')->nullable(); + $table->timestamp('disconnected_at')->nullable(); + $table->integer('minutes_online')->unsigned()->nullable(); + $table->timestamps(); + }); + + Schema::create('statistics_pilots', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id')->default(0); // Reference to networkdata_accounts.id + $table->string('callsign', 10)->default(''); + $table->string('flight_type', 1)->default('V'); + $table->string('departure_airport')->default('')->index(); + $table->string('arrival_airport')->default('')->index(); + $table->string('alternative_airport')->default(''); + $table->string('aircraft')->default(''); + $table->string('cruise_altitude')->default(''); + $table->string('cruise_tas')->default(''); + $table->text('route')->nullable(); + $table->text('remarks')->nullable(); + $table->double('current_latitude', 12, 8)->nullable(); + $table->double('current_longitude', 12, 8)->nullable(); + $table->mediumInteger('current_altitude')->nullable(); + $table->unsignedSmallInteger('current_groundspeed')->nullable(); + $table->unsignedSmallInteger('current_heading')->nullable(); + $table->timestamp('departed_at')->nullable(); + $table->timestamp('arrived_at')->nullable(); + $table->timestamp('connected_at')->nullable(); + $table->timestamp('disconnected_at')->nullable(); + $table->unsignedInteger('minutes_online')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('statistics_pilots'); + Schema::dropIfExists('statistics_atc'); + } +} diff --git a/database/migrations/2020_05_15_132505_create_membership_tables.php b/database/migrations/2020_05_15_132505_create_membership_tables.php new file mode 100644 index 0000000..2d98507 --- /dev/null +++ b/database/migrations/2020_05_15_132505_create_membership_tables.php @@ -0,0 +1,81 @@ +unsignedInteger('id')->primary(); // Primary ID of an account (equivalent to the VATSIM-ID) + $table->string('firstname'); + $table->string('lastname'); + $table->string('email')->unique(); // Accounts e-mail address (make sure it is only there once) + $table->string('password')->nullable(); // Nullable password field. We only need this as a local backup + $table->string('remember_token', 100)->nullable(); // Session Remember token (To keep login alive) + $table->string('api_token', 80)->unique()->nullable()->default(null); // Unique API key. Needed to authenticate when accessing membership api + + // VATAUTH OAuth2 REQUIRED FIELDS + $table->text('access_token')->nullable(); + $table->text('refresh_token')->nullable(); + $table->unsignedBigInteger('token_expires')->nullable(); + + $table->boolean('setup_completed')->default(false); // Has the account been setup properly? + $table->timestamps(); // Timestamps of creation and updates + $table->softDeletes(); // Prevent complete removal of data when an account get's deleted. + }); + + Schema::create('membership_account_data', function(Blueprint $table){ + $table->unsignedInteger('account_id')->primary(); // Reference to the account + // Data acquired directly from VATSIM-CERT + $table->integer('rating_atc')->default(0); // VATSIM Controller Rating + $table->integer('rating_pilot')->default(0); // VATSIM Pilot Rating ( BITMASK ) + $table->string('experience')->nullable(); + $table->timestamp('registered_at')->nullable(); + $table->double('time_atc', 9, 3)->unsigned()->default(0.0); + $table->double('time_pilot', 9, 3)->unsigned()->default(0.0); + $table->string('country_code')->nullable(); + $table->string('country_name')->nullable(); + $table->string('region_code')->nullable(); + $table->string('region_name')->nullable(); + $table->string('division_code')->nullable(); + $table->string('division_name')->nullable(); + $table->string('subdivision_code')->nullable(); + $table->string('subdivision_name')->nullable(); + $table->boolean('active')->default(true); + $table->boolean('suspended')->default(false); + // Update timestamps + $table->timestamps(); + $table->softDeletes(); + + }); + + Schema::create('membership_account_settings', function(Blueprint $table){ + $table->unsignedInteger('account_id')->primary(); + $table->enum('language', ['de', 'en']); + $table->unsignedInteger('forum_id')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('membership_accounts'); + Schema::dropIfExists('membership_account_data'); + Schema::dropIfExists('membership_account_settings'); + } +} diff --git a/database/migrations/2020_05_18_085633_create_notifications_table.php b/database/migrations/2020_05_18_085633_create_notifications_table.php new file mode 100644 index 0000000..9797596 --- /dev/null +++ b/database/migrations/2020_05_18_085633_create_notifications_table.php @@ -0,0 +1,35 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('notifications'); + } +} diff --git a/database/migrations/2020_05_18_110308_create_stations_table.php b/database/migrations/2020_05_18_110308_create_stations_table.php new file mode 100644 index 0000000..fd45ef3 --- /dev/null +++ b/database/migrations/2020_05_18_110308_create_stations_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('name'); + $table->string('ident'); + $table->double('frequency', 6, 3); + $table->text('description')->nullable(); + $table->boolean('bookable')->default(true); + $table->boolean('atis')->default(false); + $table->unsignedTinyInteger('required_rating')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigation_stations'); + } +} diff --git a/database/migrations/2020_05_19_090427_create_notes_table.php b/database/migrations/2020_05_19_090427_create_notes_table.php new file mode 100644 index 0000000..d89b224 --- /dev/null +++ b/database/migrations/2020_05_19_090427_create_notes_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('author_id'); + $table->text('note'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('membership_notes'); + } +} diff --git a/database/migrations/2020_05_19_101502_create_bans_table.php b/database/migrations/2020_05_19_101502_create_bans_table.php new file mode 100644 index 0000000..261bb45 --- /dev/null +++ b/database/migrations/2020_05_19_101502_create_bans_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('author_id'); + $table->text('reason'); + $table->boolean('permanent')->default(true); + $table->timestamp('banned_till')->nullable(); + $table->boolean('teamspeak')->default(true); + $table->boolean('homepage')->default(true); + $table->boolean('forum')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('membership_bans'); + } +} diff --git a/database/migrations/2020_05_21_131450_create_activity_log_table.php b/database/migrations/2020_05_21_131450_create_activity_log_table.php new file mode 100644 index 0000000..4f0f943 --- /dev/null +++ b/database/migrations/2020_05_21_131450_create_activity_log_table.php @@ -0,0 +1,38 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->unsignedBigInteger('subject_id')->nullable(); + $table->string('subject_type')->nullable(); + $table->unsignedBigInteger('causer_id')->nullable(); + $table->string('causer_type')->nullable(); + $table->json('properties')->nullable(); + $table->timestamps(); + + $table->index('log_name'); + $table->index(['subject_id', 'subject_type'], 'subject'); + $table->index(['causer_id', 'causer_type'], 'causer'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/database/migrations/2020_06_01_095124_create_atc_session_bookings_table.php b/database/migrations/2020_06_01_095124_create_atc_session_bookings_table.php new file mode 100644 index 0000000..56a3f0e --- /dev/null +++ b/database/migrations/2020_06_01_095124_create_atc_session_bookings_table.php @@ -0,0 +1,40 @@ +increments('id'); + $table->unsignedInteger('vatbook_id')->nullable(); + $table->unsignedInteger('station_id'); + $table->unsignedInteger('controller_id'); + $table->boolean('voice')->default(true); + $table->boolean('training')->default(false); + $table->boolean('event')->default(false); + $table->unsignedInteger('event_id')->nullable(); + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('bookings_atc'); + } +} diff --git a/database/migrations/2020_06_01_095308_create_events_table.php b/database/migrations/2020_06_01_095308_create_events_table.php new file mode 100644 index 0000000..7c99dfd --- /dev/null +++ b/database/migrations/2020_06_01_095308_create_events_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('title'); + $table->longtext('description'); + $table->string('url')->nullable(); + $table->string('banner_url')->nullable(); + $table->timestamp('begins_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('events'); + } +} diff --git a/database/migrations/2020_06_03_114627_create_country_table.php b/database/migrations/2020_06_03_114627_create_country_table.php new file mode 100644 index 0000000..e2100c1 --- /dev/null +++ b/database/migrations/2020_06_03_114627_create_country_table.php @@ -0,0 +1,1240 @@ + 4, + 'name' => 'Afghanistan', + 'alpha2' => 'af', + 'alpha3' => 'afg' + ), + array( + 'id' => 818, + 'name' => 'Ägypten', + 'alpha2' => 'eg', + 'alpha3' => 'egy' + ), + array( + 'id' => 8, + 'name' => 'Albanien', + 'alpha2' => 'al', + 'alpha3' => 'alb' + ), + array( + 'id' => 12, + 'name' => 'Algerien', + 'alpha2' => 'dz', + 'alpha3' => 'dza' + ), + array( + 'id' => 20, + 'name' => 'Andorra', + 'alpha2' => 'ad', + 'alpha3' => 'and' + ), + array( + 'id' => 24, + 'name' => 'Angola', + 'alpha2' => 'ao', + 'alpha3' => 'ago' + ), + array( + 'id' => 28, + 'name' => 'Antigua und Barbuda', + 'alpha2' => 'ag', + 'alpha3' => 'atg' + ), + array( + 'id' => 226, + 'name' => 'Äquatorialguinea', + 'alpha2' => 'gq', + 'alpha3' => 'gnq' + ), + array( + 'id' => 32, + 'name' => 'Argentinien', + 'alpha2' => 'ar', + 'alpha3' => 'arg' + ), + array( + 'id' => 51, + 'name' => 'Armenien', + 'alpha2' => 'am', + 'alpha3' => 'arm' + ), + array( + 'id' => 31, + 'name' => 'Aserbaidschan', + 'alpha2' => 'az', + 'alpha3' => 'aze' + ), + array( + 'id' => 231, + 'name' => 'Äthiopien', + 'alpha2' => 'et', + 'alpha3' => 'eth' + ), + array( + 'id' => 36, + 'name' => 'Australien', + 'alpha2' => 'au', + 'alpha3' => 'aus' + ), + array( + 'id' => 44, + 'name' => 'Bahamas', + 'alpha2' => 'bs', + 'alpha3' => 'bhs' + ), + array( + 'id' => 48, + 'name' => 'Bahrain', + 'alpha2' => 'bh', + 'alpha3' => 'bhr' + ), + array( + 'id' => 50, + 'name' => 'Bangladesch', + 'alpha2' => 'bd', + 'alpha3' => 'bgd' + ), + array( + 'id' => 52, + 'name' => 'Barbados', + 'alpha2' => 'bb', + 'alpha3' => 'brb' + ), + array( + 'id' => 112, + 'name' => 'Weißrussland', + 'alpha2' => 'by', + 'alpha3' => 'blr' + ), + array( + 'id' => 56, + 'name' => 'Belgien', + 'alpha2' => 'be', + 'alpha3' => 'bel' + ), + array( + 'id' => 84, + 'name' => 'Belize', + 'alpha2' => 'bz', + 'alpha3' => 'blz' + ), + array( + 'id' => 204, + 'name' => 'Benin', + 'alpha2' => 'bj', + 'alpha3' => 'ben' + ), + array( + 'id' => 64, + 'name' => 'Bhutan', + 'alpha2' => 'bt', + 'alpha3' => 'btn' + ), + array( + 'id' => 68, + 'name' => 'Bolivien', + 'alpha2' => 'bo', + 'alpha3' => 'bol' + ), + array( + 'id' => 70, + 'name' => 'Bosnien und Herzegowina', + 'alpha2' => 'ba', + 'alpha3' => 'bih' + ), + array( + 'id' => 72, + 'name' => 'Botswana', + 'alpha2' => 'bw', + 'alpha3' => 'bwa' + ), + array( + 'id' => 76, + 'name' => 'Brasilien', + 'alpha2' => 'br', + 'alpha3' => 'bra' + ), + array( + 'id' => 96, + 'name' => 'Brunei', + 'alpha2' => 'bn', + 'alpha3' => 'brn' + ), + array( + 'id' => 100, + 'name' => 'Bulgarien', + 'alpha2' => 'bg', + 'alpha3' => 'bgr' + ), + array( + 'id' => 854, + 'name' => 'Burkina Faso', + 'alpha2' => 'bf', + 'alpha3' => 'bfa' + ), + array( + 'id' => 108, + 'name' => 'Burundi', + 'alpha2' => 'bi', + 'alpha3' => 'bdi' + ), + array( + 'id' => 152, + 'name' => 'Chile', + 'alpha2' => 'cl', + 'alpha3' => 'chl' + ), + array( + 'id' => 156, + 'name' => 'Volksrepublik China', + 'alpha2' => 'cn', + 'alpha3' => 'chn' + ), + array( + 'id' => 188, + 'name' => 'Costa Rica', + 'alpha2' => 'cr', + 'alpha3' => 'cri' + ), + array( + 'id' => 208, + 'name' => 'Dänemark', + 'alpha2' => 'dk', + 'alpha3' => 'dnk' + ), + array( + 'id' => 276, + 'name' => 'Deutschland', + 'alpha2' => 'de', + 'alpha3' => 'deu' + ), + array( + 'id' => 212, + 'name' => 'Dominica', + 'alpha2' => 'dm', + 'alpha3' => 'dma' + ), + array( + 'id' => 214, + 'name' => 'Dominikanische Republik', + 'alpha2' => 'do', + 'alpha3' => 'dom' + ), + array( + 'id' => 262, + 'name' => 'Dschibuti', + 'alpha2' => 'dj', + 'alpha3' => 'dji' + ), + array( + 'id' => 218, + 'name' => 'Ecuador', + 'alpha2' => 'ec', + 'alpha3' => 'ecu' + ), + array( + 'id' => 384, + 'name' => 'Elfenbeinküste', + 'alpha2' => 'ci', + 'alpha3' => 'civ' + ), + array( + 'id' => 222, + 'name' => 'El Salvador', + 'alpha2' => 'sv', + 'alpha3' => 'slv' + ), + array( + 'id' => 232, + 'name' => 'Eritrea', + 'alpha2' => 'er', + 'alpha3' => 'eri' + ), + array( + 'id' => 233, + 'name' => 'Estland', + 'alpha2' => 'ee', + 'alpha3' => 'est' + ), + array( + 'id' => 748, + 'name' => 'Swasiland', + 'alpha2' => 'sz', + 'alpha3' => 'swz' + ), + array( + 'id' => 242, + 'name' => 'Fidschi', + 'alpha2' => 'fj', + 'alpha3' => 'fji' + ), + array( + 'id' => 246, + 'name' => 'Finnland', + 'alpha2' => 'fi', + 'alpha3' => 'fin' + ), + array( + 'id' => 250, + 'name' => 'Frankreich', + 'alpha2' => 'fr', + 'alpha3' => 'fra' + ), + array( + 'id' => 266, + 'name' => 'Gabun', + 'alpha2' => 'ga', + 'alpha3' => 'gab' + ), + array( + 'id' => 270, + 'name' => 'Gambia', + 'alpha2' => 'gm', + 'alpha3' => 'gmb' + ), + array( + 'id' => 268, + 'name' => 'Georgien', + 'alpha2' => 'ge', + 'alpha3' => 'geo' + ), + array( + 'id' => 288, + 'name' => 'Ghana', + 'alpha2' => 'gh', + 'alpha3' => 'gha' + ), + array( + 'id' => 308, + 'name' => 'Grenada', + 'alpha2' => 'gd', + 'alpha3' => 'grd' + ), + array( + 'id' => 300, + 'name' => 'Griechenland', + 'alpha2' => 'gr', + 'alpha3' => 'grc' + ), + array( + 'id' => 320, + 'name' => 'Guatemala', + 'alpha2' => 'gt', + 'alpha3' => 'gtm' + ), + array( + 'id' => 324, + 'name' => 'Guinea', + 'alpha2' => 'gn', + 'alpha3' => 'gin' + ), + array( + 'id' => 624, + 'name' => 'Guinea-Bissau', + 'alpha2' => 'gw', + 'alpha3' => 'gnb' + ), + array( + 'id' => 328, + 'name' => 'Guyana', + 'alpha2' => 'gy', + 'alpha3' => 'guy' + ), + array( + 'id' => 332, + 'name' => 'Haiti', + 'alpha2' => 'ht', + 'alpha3' => 'hti' + ), + array( + 'id' => 340, + 'name' => 'Honduras', + 'alpha2' => 'hn', + 'alpha3' => 'hnd' + ), + array( + 'id' => 356, + 'name' => 'Indien', + 'alpha2' => 'in', + 'alpha3' => 'ind' + ), + array( + 'id' => 360, + 'name' => 'Indonesien', + 'alpha2' => 'id', + 'alpha3' => 'idn' + ), + array( + 'id' => 368, + 'name' => 'Irak', + 'alpha2' => 'iq', + 'alpha3' => 'irq' + ), + array( + 'id' => 364, + 'name' => 'Iran', + 'alpha2' => 'ir', + 'alpha3' => 'irn' + ), + array( + 'id' => 372, + 'name' => 'Irland', + 'alpha2' => 'ie', + 'alpha3' => 'irl' + ), + array( + 'id' => 352, + 'name' => 'Island', + 'alpha2' => 'is', + 'alpha3' => 'isl' + ), + array( + 'id' => 376, + 'name' => 'Israel', + 'alpha2' => 'il', + 'alpha3' => 'isr' + ), + array( + 'id' => 380, + 'name' => 'Italien', + 'alpha2' => 'it', + 'alpha3' => 'ita' + ), + array( + 'id' => 388, + 'name' => 'Jamaika', + 'alpha2' => 'jm', + 'alpha3' => 'jam' + ), + array( + 'id' => 392, + 'name' => 'Japan', + 'alpha2' => 'jp', + 'alpha3' => 'jpn' + ), + array( + 'id' => 887, + 'name' => 'Jemen', + 'alpha2' => 'ye', + 'alpha3' => 'yem' + ), + array( + 'id' => 400, + 'name' => 'Jordanien', + 'alpha2' => 'jo', + 'alpha3' => 'jor' + ), + array( + 'id' => 116, + 'name' => 'Kambodscha', + 'alpha2' => 'kh', + 'alpha3' => 'khm' + ), + array( + 'id' => 120, + 'name' => 'Kamerun', + 'alpha2' => 'cm', + 'alpha3' => 'cmr' + ), + array( + 'id' => 124, + 'name' => 'Kanada', + 'alpha2' => 'ca', + 'alpha3' => 'can' + ), + array( + 'id' => 132, + 'name' => 'Kap Verde', + 'alpha2' => 'cv', + 'alpha3' => 'cpv' + ), + array( + 'id' => 398, + 'name' => 'Kasachstan', + 'alpha2' => 'kz', + 'alpha3' => 'kaz' + ), + array( + 'id' => 634, + 'name' => 'Katar', + 'alpha2' => 'qa', + 'alpha3' => 'qat' + ), + array( + 'id' => 404, + 'name' => 'Kenia', + 'alpha2' => 'ke', + 'alpha3' => 'ken' + ), + array( + 'id' => 417, + 'name' => 'Kirgisistan', + 'alpha2' => 'kg', + 'alpha3' => 'kgz' + ), + array( + 'id' => 296, + 'name' => 'Kiribati', + 'alpha2' => 'ki', + 'alpha3' => 'kir' + ), + array( + 'id' => 170, + 'name' => 'Kolumbien', + 'alpha2' => 'co', + 'alpha3' => 'col' + ), + array( + 'id' => 174, + 'name' => 'Komoren', + 'alpha2' => 'km', + 'alpha3' => 'com' + ), + array( + 'id' => 180, + 'name' => 'Kongo, Demokratische Republik', + 'alpha2' => 'cd', + 'alpha3' => 'cod' + ), + array( + 'id' => 178, + 'name' => 'Kongo, Republik', + 'alpha2' => 'cg', + 'alpha3' => 'cog' + ), + array( + 'id' => 408, + 'name' => 'Korea, Nord', + 'alpha2' => 'kp', + 'alpha3' => 'prk' + ), + array( + 'id' => 410, + 'name' => 'Korea, Süd', + 'alpha2' => 'kr', + 'alpha3' => 'kor' + ), + array( + 'id' => 191, + 'name' => 'Kroatien', + 'alpha2' => 'hr', + 'alpha3' => 'hrv' + ), + array( + 'id' => 192, + 'name' => 'Kuba', + 'alpha2' => 'cu', + 'alpha3' => 'cub' + ), + array( + 'id' => 414, + 'name' => 'Kuwait', + 'alpha2' => 'kw', + 'alpha3' => 'kwt' + ), + array( + 'id' => 418, + 'name' => 'Laos', + 'alpha2' => 'la', + 'alpha3' => 'lao' + ), + array( + 'id' => 426, + 'name' => 'Lesotho', + 'alpha2' => 'ls', + 'alpha3' => 'lso' + ), + array( + 'id' => 428, + 'name' => 'Lettland', + 'alpha2' => 'lv', + 'alpha3' => 'lva' + ), + array( + 'id' => 422, + 'name' => 'Libanon', + 'alpha2' => 'lb', + 'alpha3' => 'lbn' + ), + array( + 'id' => 430, + 'name' => 'Liberia', + 'alpha2' => 'lr', + 'alpha3' => 'lbr' + ), + array( + 'id' => 434, + 'name' => 'Libyen', + 'alpha2' => 'ly', + 'alpha3' => 'lby' + ), + array( + 'id' => 438, + 'name' => 'Liechtenstein', + 'alpha2' => 'li', + 'alpha3' => 'lie' + ), + array( + 'id' => 440, + 'name' => 'Litauen', + 'alpha2' => 'lt', + 'alpha3' => 'ltu' + ), + array( + 'id' => 442, + 'name' => 'Luxemburg', + 'alpha2' => 'lu', + 'alpha3' => 'lux' + ), + array( + 'id' => 450, + 'name' => 'Madagaskar', + 'alpha2' => 'mg', + 'alpha3' => 'mdg' + ), + array( + 'id' => 454, + 'name' => 'Malawi', + 'alpha2' => 'mw', + 'alpha3' => 'mwi' + ), + array( + 'id' => 458, + 'name' => 'Malaysia', + 'alpha2' => 'my', + 'alpha3' => 'mys' + ), + array( + 'id' => 462, + 'name' => 'Malediven', + 'alpha2' => 'mv', + 'alpha3' => 'mdv' + ), + array( + 'id' => 466, + 'name' => 'Mali', + 'alpha2' => 'ml', + 'alpha3' => 'mli' + ), + array( + 'id' => 470, + 'name' => 'Malta', + 'alpha2' => 'mt', + 'alpha3' => 'mlt' + ), + array( + 'id' => 504, + 'name' => 'Marokko', + 'alpha2' => 'ma', + 'alpha3' => 'mar' + ), + array( + 'id' => 584, + 'name' => 'Marshallinseln', + 'alpha2' => 'mh', + 'alpha3' => 'mhl' + ), + array( + 'id' => 478, + 'name' => 'Mauretanien', + 'alpha2' => 'mr', + 'alpha3' => 'mrt' + ), + array( + 'id' => 480, + 'name' => 'Mauritius', + 'alpha2' => 'mu', + 'alpha3' => 'mus' + ), + array( + 'id' => 484, + 'name' => 'Mexiko', + 'alpha2' => 'mx', + 'alpha3' => 'mex' + ), + array( + 'id' => 583, + 'name' => 'Mikronesien', + 'alpha2' => 'fm', + 'alpha3' => 'fsm' + ), + array( + 'id' => 498, + 'name' => 'Moldau', + 'alpha2' => 'md', + 'alpha3' => 'mda' + ), + array( + 'id' => 492, + 'name' => 'Monaco', + 'alpha2' => 'mc', + 'alpha3' => 'mco' + ), + array( + 'id' => 496, + 'name' => 'Mongolei', + 'alpha2' => 'mn', + 'alpha3' => 'mng' + ), + array( + 'id' => 499, + 'name' => 'Montenegro', + 'alpha2' => 'me', + 'alpha3' => 'mne' + ), + array( + 'id' => 508, + 'name' => 'Mosambik', + 'alpha2' => 'mz', + 'alpha3' => 'moz' + ), + array( + 'id' => 104, + 'name' => 'Myanmar', + 'alpha2' => 'mm', + 'alpha3' => 'mmr' + ), + array( + 'id' => 516, + 'name' => 'Namibia', + 'alpha2' => 'na', + 'alpha3' => 'nam' + ), + array( + 'id' => 520, + 'name' => 'Nauru', + 'alpha2' => 'nr', + 'alpha3' => 'nru' + ), + array( + 'id' => 524, + 'name' => 'Nepal', + 'alpha2' => 'np', + 'alpha3' => 'npl' + ), + array( + 'id' => 554, + 'name' => 'Neuseeland', + 'alpha2' => 'nz', + 'alpha3' => 'nzl' + ), + array( + 'id' => 558, + 'name' => 'Nicaragua', + 'alpha2' => 'ni', + 'alpha3' => 'nic' + ), + array( + 'id' => 528, + 'name' => 'Niederlande', + 'alpha2' => 'nl', + 'alpha3' => 'nld' + ), + array( + 'id' => 562, + 'name' => 'Niger', + 'alpha2' => 'ne', + 'alpha3' => 'ner' + ), + array( + 'id' => 566, + 'name' => 'Nigeria', + 'alpha2' => 'ng', + 'alpha3' => 'nga' + ), + array( + 'id' => 807, + 'name' => 'Nordmazedonien', + 'alpha2' => 'mk', + 'alpha3' => 'mkd' + ), + array( + 'id' => 578, + 'name' => 'Norwegen', + 'alpha2' => 'no', + 'alpha3' => 'nor' + ), + array( + 'id' => 512, + 'name' => 'Oman', + 'alpha2' => 'om', + 'alpha3' => 'omn' + ), + array( + 'id' => 40, + 'name' => 'Österreich', + 'alpha2' => 'at', + 'alpha3' => 'aut' + ), + array( + 'id' => 626, + 'name' => 'Osttimor', + 'alpha2' => 'tl', + 'alpha3' => 'tls' + ), + array( + 'id' => 586, + 'name' => 'Pakistan', + 'alpha2' => 'pk', + 'alpha3' => 'pak' + ), + array( + 'id' => 585, + 'name' => 'Palau', + 'alpha2' => 'pw', + 'alpha3' => 'plw' + ), + array( + 'id' => 591, + 'name' => 'Panama', + 'alpha2' => 'pa', + 'alpha3' => 'pan' + ), + array( + 'id' => 598, + 'name' => 'Papua-Neuguinea', + 'alpha2' => 'pg', + 'alpha3' => 'png' + ), + array( + 'id' => 600, + 'name' => 'Paraguay', + 'alpha2' => 'py', + 'alpha3' => 'pry' + ), + array( + 'id' => 604, + 'name' => 'Peru', + 'alpha2' => 'pe', + 'alpha3' => 'per' + ), + array( + 'id' => 608, + 'name' => 'Philippinen', + 'alpha2' => 'ph', + 'alpha3' => 'phl' + ), + array( + 'id' => 616, + 'name' => 'Polen', + 'alpha2' => 'pl', + 'alpha3' => 'pol' + ), + array( + 'id' => 620, + 'name' => 'Portugal', + 'alpha2' => 'pt', + 'alpha3' => 'prt' + ), + array( + 'id' => 646, + 'name' => 'Ruanda', + 'alpha2' => 'rw', + 'alpha3' => 'rwa' + ), + array( + 'id' => 642, + 'name' => 'Rumänien', + 'alpha2' => 'ro', + 'alpha3' => 'rou' + ), + array( + 'id' => 643, + 'name' => 'Russland', + 'alpha2' => 'ru', + 'alpha3' => 'rus' + ), + array( + 'id' => 90, + 'name' => 'Salomonen', + 'alpha2' => 'sb', + 'alpha3' => 'slb' + ), + array( + 'id' => 894, + 'name' => 'Sambia', + 'alpha2' => 'zm', + 'alpha3' => 'zmb' + ), + array( + 'id' => 882, + 'name' => 'Samoa', + 'alpha2' => 'ws', + 'alpha3' => 'wsm' + ), + array( + 'id' => 674, + 'name' => 'San Marino', + 'alpha2' => 'sm', + 'alpha3' => 'smr' + ), + array( + 'id' => 678, + 'name' => 'São Tomé und Príncipe', + 'alpha2' => 'st', + 'alpha3' => 'stp' + ), + array( + 'id' => 682, + 'name' => 'Saudi-Arabien', + 'alpha2' => 'sa', + 'alpha3' => 'sau' + ), + array( + 'id' => 752, + 'name' => 'Schweden', + 'alpha2' => 'se', + 'alpha3' => 'swe' + ), + array( + 'id' => 756, + 'name' => 'Schweiz', + 'alpha2' => 'ch', + 'alpha3' => 'che' + ), + array( + 'id' => 686, + 'name' => 'Senegal', + 'alpha2' => 'sn', + 'alpha3' => 'sen' + ), + array( + 'id' => 688, + 'name' => 'Serbien', + 'alpha2' => 'rs', + 'alpha3' => 'srb' + ), + array( + 'id' => 690, + 'name' => 'Seychellen', + 'alpha2' => 'sc', + 'alpha3' => 'syc' + ), + array( + 'id' => 694, + 'name' => 'Sierra Leone', + 'alpha2' => 'sl', + 'alpha3' => 'sle' + ), + array( + 'id' => 716, + 'name' => 'Simbabwe', + 'alpha2' => 'zw', + 'alpha3' => 'zwe' + ), + array( + 'id' => 702, + 'name' => 'Singapur', + 'alpha2' => 'sg', + 'alpha3' => 'sgp' + ), + array( + 'id' => 703, + 'name' => 'Slowakei', + 'alpha2' => 'sk', + 'alpha3' => 'svk' + ), + array( + 'id' => 705, + 'name' => 'Slowenien', + 'alpha2' => 'si', + 'alpha3' => 'svn' + ), + array( + 'id' => 706, + 'name' => 'Somalia', + 'alpha2' => 'so', + 'alpha3' => 'som' + ), + array( + 'id' => 724, + 'name' => 'Spanien', + 'alpha2' => 'es', + 'alpha3' => 'esp' + ), + array( + 'id' => 144, + 'name' => 'Sri Lanka', + 'alpha2' => 'lk', + 'alpha3' => 'lka' + ), + array( + 'id' => 659, + 'name' => 'St. Kitts und Nevis', + 'alpha2' => 'kn', + 'alpha3' => 'kna' + ), + array( + 'id' => 662, + 'name' => 'St. Lucia', + 'alpha2' => 'lc', + 'alpha3' => 'lca' + ), + array( + 'id' => 670, + 'name' => 'St. Vincent und die Grenadinen', + 'alpha2' => 'vc', + 'alpha3' => 'vct' + ), + array( + 'id' => 710, + 'name' => 'Südafrika', + 'alpha2' => 'za', + 'alpha3' => 'zaf' + ), + array( + 'id' => 729, + 'name' => 'Sudan', + 'alpha2' => 'sd', + 'alpha3' => 'sdn' + ), + array( + 'id' => 728, + 'name' => 'Südsudan', + 'alpha2' => 'ss', + 'alpha3' => 'ssd' + ), + array( + 'id' => 740, + 'name' => 'Suriname', + 'alpha2' => 'sr', + 'alpha3' => 'sur' + ), + array( + 'id' => 760, + 'name' => 'Syrien', + 'alpha2' => 'sy', + 'alpha3' => 'syr' + ), + array( + 'id' => 762, + 'name' => 'Tadschikistan', + 'alpha2' => 'tj', + 'alpha3' => 'tjk' + ), + array( + 'id' => 834, + 'name' => 'Tansania', + 'alpha2' => 'tz', + 'alpha3' => 'tza' + ), + array( + 'id' => 764, + 'name' => 'Thailand', + 'alpha2' => 'th', + 'alpha3' => 'tha' + ), + array( + 'id' => 768, + 'name' => 'Togo', + 'alpha2' => 'tg', + 'alpha3' => 'tgo' + ), + array( + 'id' => 776, + 'name' => 'Tonga', + 'alpha2' => 'to', + 'alpha3' => 'ton' + ), + array( + 'id' => 780, + 'name' => 'Trinidad und Tobago', + 'alpha2' => 'tt', + 'alpha3' => 'tto' + ), + array( + 'id' => 148, + 'name' => 'Tschad', + 'alpha2' => 'td', + 'alpha3' => 'tcd' + ), + array( + 'id' => 203, + 'name' => 'Tschechien', + 'alpha2' => 'cz', + 'alpha3' => 'cze' + ), + array( + 'id' => 788, + 'name' => 'Tunesien', + 'alpha2' => 'tn', + 'alpha3' => 'tun' + ), + array( + 'id' => 792, + 'name' => 'Türkei', + 'alpha2' => 'tr', + 'alpha3' => 'tur' + ), + array( + 'id' => 795, + 'name' => 'Turkmenistan', + 'alpha2' => 'tm', + 'alpha3' => 'tkm' + ), + array( + 'id' => 798, + 'name' => 'Tuvalu', + 'alpha2' => 'tv', + 'alpha3' => 'tuv' + ), + array( + 'id' => 800, + 'name' => 'Uganda', + 'alpha2' => 'ug', + 'alpha3' => 'uga' + ), + array( + 'id' => 804, + 'name' => 'Ukraine', + 'alpha2' => 'ua', + 'alpha3' => 'ukr' + ), + array( + 'id' => 348, + 'name' => 'Ungarn', + 'alpha2' => 'hu', + 'alpha3' => 'hun' + ), + array( + 'id' => 858, + 'name' => 'Uruguay', + 'alpha2' => 'uy', + 'alpha3' => 'ury' + ), + array( + 'id' => 860, + 'name' => 'Usbekistan', + 'alpha2' => 'uz', + 'alpha3' => 'uzb' + ), + array( + 'id' => 548, + 'name' => 'Vanuatu', + 'alpha2' => 'vu', + 'alpha3' => 'vut' + ), + array( + 'id' => 862, + 'name' => 'Venezuela', + 'alpha2' => 've', + 'alpha3' => 'ven' + ), + array( + 'id' => 784, + 'name' => 'Vereinigte Arabische Emirate', + 'alpha2' => 'ae', + 'alpha3' => 'are' + ), + array( + 'id' => 840, + 'name' => 'Vereinigte Staaten', + 'alpha2' => 'us', + 'alpha3' => 'usa' + ), + array( + 'id' => 826, + 'name' => 'Vereinigtes Königreich', + 'alpha2' => 'gb', + 'alpha3' => 'gbr' + ), + array( + 'id' => 704, + 'name' => 'Vietnam', + 'alpha2' => 'vn', + 'alpha3' => 'vnm' + ), + array( + 'id' => 140, + 'name' => 'Zentral­afrikanische Republik', + 'alpha2' => 'cf', + 'alpha3' => 'caf' + ), + array( + 'id' => 196, + 'name' => 'Zypern', + 'alpha2' => 'cy', + 'alpha3' => 'cyp' + ), + array( + 'id' => 534, + 'name' => 'Sint Maarten', + 'alpha2' => 'sx', + 'alpha3' => 'sxm' + ), + array( + 'id' => 304, + 'name' => 'Grönland', + 'alpha2' => 'gl', + 'alpha3' => 'grl' + ), + array( + 'id' => 10, + 'name' => 'Antarctica', + 'alpha2' => 'aq', + 'alpha3' => 'ata' + ), + array( + 'id' => 654, + 'name' => 'Saint Helena, Ascension and Tristan da Cunha', + 'alpha2' => 'sh', + 'alpha3' => 'shn' + ), + array( + 'id' => 630, + 'name' => 'Puerto Rico', + 'alpha2' => 'pr', + 'alpha3' => 'pri' + ), + array( + 'id' => 86, + 'name' => 'British Indian Ocean Territory', + 'alpha2' => 'io', + 'alpha3' => 'iot', + ) + ); + + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('countries', function (Blueprint $table) { + $table->unsignedInteger('id')->primary(); + $table->string('name', 80)->default(''); + $table->string('alpha_2', 2)->default(''); + $table->string('alpha_3', 3)->default(''); + }); + + foreach ($this->countries as $country) { + $c = new \App\Models\Navigation\Country(); + $c->id = $country['id']; + $c->name = $country['name']; + $c->alpha_2 = strtoupper($country['alpha2']); + $c->alpha_3 = strtoupper($country['alpha3']); + $c->save(); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('countries'); + } +} diff --git a/database/migrations/2020_06_04_123220_create_regionalgroup_tables.php b/database/migrations/2020_06_04_123220_create_regionalgroup_tables.php new file mode 100644 index 0000000..ff8b29a --- /dev/null +++ b/database/migrations/2020_06_04_123220_create_regionalgroup_tables.php @@ -0,0 +1,104 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('regionalgroups_regionalgroups', function(Blueprint $table) { + $table->id(); + $table->string('name'); + $table->unsignedInteger('fir_id')->nullable(); + $table->unsignedSmallInteger('vacc_nbr')->default(0); + $table->string('email')->default('rg@vatsim-germany.org'); + $table->unsignedInteger('chief_id')->nullable(); + $table->unsignedInteger('deputy_id')->nullable(); + $table->timestamps(); + }); + + Schema::create('regionalgroups_account_regionalgroup', function(Blueprint $table) { + $table->id(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('regionalgroup_id'); + $table->boolean('pilot')->default(true); + $table->boolean('controller')->default(false); + $table->boolean('guest')->default(false); + $table->timestamps(); + }); + + Schema::create('regionalgroups_mentors', function(Blueprint $table) { + $table->id(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('regionalgroup_id'); + $table->boolean('chief')->default(false); + $table->boolean('senior')->default(false); + $table->timestamps(); + }); + + Schema::create('regionalgroups_navigators', function(Blueprint $table) { + $table->id(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('regionalgroup_id'); + $table->boolean('chief')->default(false); + $table->boolean('deputy')->default(false); + $table->timestamps(); + }); + + Schema::create('regionalgroups_eventler', function(Blueprint $table) { + $table->id(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('regionalgroup_id'); + $table->boolean('chief')->default(false); + $table->boolean('deputy')->default(false); + $table->timestamps(); + }); + + Schema::create('regionalgroups_requests', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('regionalgroup_id'); + $table->unsignedInteger('account_id'); + $table->enum('type', ['member', 'guest']); + $table->enum('as', ['pilot', 'controller', 'both']); + $table->text('reason'); + $table->timestamps(); + }); + /* + Schema::create('regionalgroups_aerodrome_regionalgroup', function(Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('regionalgroup_id'); + $table->unsignedInteger('aerodrome_id'); + }); + */ + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('regionalgroups_aerodrome_regionalgroup'); + Schema::dropIfExists('regionalgroups_requests'); + Schema::dropIfExists('regionalgroups_eventler'); + Schema::dropIfExists('regionalgroups_navigators'); + Schema::dropIfExists('regionalgroups_mentors'); + Schema::dropIfExists('regionalgroups_account_regionalgroup'); + Schema::dropIfExists('regionalgroups_regionalgroups'); + Schema::dropIfExists('regionalgroups_firs'); + } +} diff --git a/database/migrations/2020_06_07_094544_create_aerodrome_regionalgroup_tables.php b/database/migrations/2020_06_07_094544_create_aerodrome_regionalgroup_tables.php new file mode 100644 index 0000000..3547755 --- /dev/null +++ b/database/migrations/2020_06_07_094544_create_aerodrome_regionalgroup_tables.php @@ -0,0 +1,33 @@ +id(); + $table->unsignedInteger('aerodrome_id'); + $table->unsignedInteger('regionalgroup_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigation_aerodrome_regionalgroup'); + } +} diff --git a/database/migrations/2020_06_07_155951_create_aerodrome_station_table.php b/database/migrations/2020_06_07_155951_create_aerodrome_station_table.php new file mode 100644 index 0000000..e1e85b7 --- /dev/null +++ b/database/migrations/2020_06_07_155951_create_aerodrome_station_table.php @@ -0,0 +1,34 @@ +id(); + $table->unsignedInteger('aerodrome_id'); + $table->unsignedInteger('station_id'); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigation_aerodrome_station'); + } +} diff --git a/database/migrations/2020_06_08_095147_create_charts_table.php b/database/migrations/2020_06_08_095147_create_charts_table.php new file mode 100644 index 0000000..598f03e --- /dev/null +++ b/database/migrations/2020_06_08_095147_create_charts_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('name'); + $table->string('href'); + $table->integer('airac'); + $table->enum('type', ['aoi', 'afc', 'agc', 'apc', 'sid', 'star', 'iac'])->default('sid'); + $table->enum('fri', ['ifr', 'vfr'])->default('ifr'); + $table->boolean('published')->default(true); + $table->boolean('public_available')->default(false); + $table->timestamps(); + }); + + Schema::create('navigation_aerodrome_chart', function (Blueprint $table) + { + $table->id(); + $table->unsignedInteger('aerodrome_id'); + $table->unsignedInteger('chart_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigation_aerodrome_chart'); + Schema::dropIfExists('navigation_charts'); + } +} diff --git a/database/migrations/2020_06_10_055242_add_wikilink_to_aerodromes.php b/database/migrations/2020_06_10_055242_add_wikilink_to_aerodromes.php new file mode 100644 index 0000000..130c493 --- /dev/null +++ b/database/migrations/2020_06_10_055242_add_wikilink_to_aerodromes.php @@ -0,0 +1,32 @@ +string('wiki_link')->nullable()->after('other_information'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('aerodromes', function (Blueprint $table) { + $table->dropColumn('wiki_link'); + }); + } +} diff --git a/database/migrations/2020_06_10_065539_modify_regionalgroup_requests.php b/database/migrations/2020_06_10_065539_modify_regionalgroup_requests.php new file mode 100644 index 0000000..940203f --- /dev/null +++ b/database/migrations/2020_06_10_065539_modify_regionalgroup_requests.php @@ -0,0 +1,34 @@ +enum('topic', ['join', 'change', 'leave'])->after('account_id'); + $table->unsignedInteger('destination_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('regionalgroups_requests', function (Blueprint $table) { + $table->dropColumn('topic'); + $table->dropColumn('destination_id'); + }); + } +} diff --git a/database/migrations/2020_06_23_073418_create_teamspeak_tables.php b/database/migrations/2020_06_23_073418_create_teamspeak_tables.php new file mode 100644 index 0000000..bd3dd73 --- /dev/null +++ b/database/migrations/2020_06_23_073418_create_teamspeak_tables.php @@ -0,0 +1,46 @@ +increments('id')->unsigned(); + $table->integer('account_id')->unsigned()->index(); + $table->string('registration_ip'); + $table->string('last_ip')->nullable()->default('0.0.0.0'); + $table->timestamp('last_login')->nullable(); + $table->string('last_os')->nullable(); + $table->string('uid')->nullable(); + $table->smallInteger('dbid')->unsigned()->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('teamspeak_confirmation', function(Blueprint $table){ + $table->integer('registration_id')->primary()->unsigned(); + $table->string('privilege_key', 50); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('teamspeak_registration'); + Schema::dropIfExists('teamspeak_confirmation'); + } +} diff --git a/database/migrations/2020_06_28_093111_create_atd_solo_tables.php b/database/migrations/2020_06_28_093111_create_atd_solo_tables.php new file mode 100644 index 0000000..a697d1b --- /dev/null +++ b/database/migrations/2020_06_28_093111_create_atd_solo_tables.php @@ -0,0 +1,72 @@ +increments('id'); + $table->string('title'); + $table->unsignedInteger('days')->default(30); + $table->timestamps(); + }); + + /* + Solo Option Tower + Option 1: Beginn Minor --> TRG --> 90 Tage Solo --> Wechsel auf Major --> TRG --> 90 Tage Solo --> CPT Major + Option 2: Beginn Minor --> TRG --> 60 Tage Solo --> Wechsel auf Major --> TRG --> 120 Tage Solo --> CPT Major + Option 3: Beginn Minor --> TRG --> 120 Tage Solo --> Wechsel auf Major --> TRG --> 60 Tage Solo --> CPT Major + Option 4: Beginn Minor --> TRG --> 180 Tage Solo --> CPT Minor + Option 5: Beginn Major --> TRG --> 180 Tage Solo --> CPT Major + Alle anderen sind 30 Tage + */ + DB::table('uts_atd_solophases')->insert([ + ['title' => 'Ground', 'days' => 30], + ['title' => 'Tower O1 P1', 'days' => 90], + ['title' => 'Tower O1 P2', 'days' => 90], + ['title' => 'Tower O2 P1', 'days' => 60], + ['title' => 'Tower O2 P2', 'days' => 120], + ['title' => 'Tower O3 P1', 'days' => 120], + ['title' => 'Tower O3 P2', 'days' => 60], + ['title' => 'Tower Minor', 'days' => 180], + ['title' => 'Tower Major', 'days' => 180], + ['title' => 'Approach Minor', 'days' => 30], + ['title' => 'Approach Major', 'days' => 30], + ['title' => 'Center', 'days' => 30], + ]); + + // Tabelle der Solofreigaben des ATD + Schema::create('uts_atd_solo_clearances', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('candidate_id'); + $table->unsignedInteger('station_id'); + $table->unsignedInteger('solophase_id'); + $table->timestamp('begins_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->boolean('approved')->default(false); + $table->unsignedInteger('extensions')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('uts_atd_solo_clearances'); + Schema::dropIfExists('uts_atd_solophases'); + } +} diff --git a/database/migrations/2020_07_26_140107_create_forumgroups_tables.php b/database/migrations/2020_07_26_140107_create_forumgroups_tables.php new file mode 100644 index 0000000..beeee38 --- /dev/null +++ b/database/migrations/2020_07_26_140107_create_forumgroups_tables.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedInteger('forum_id'); // GROUP ID ON THE FORUM + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('forumgroup_group', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger('group_id'); // Local group id + $table->unsignedInteger('forumgroup_id'); // Id of the forum-group in our database + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('forumgroup_group'); + Schema::dropIfExists('forumgroups'); + } +} diff --git a/database/migrations/2020_08_17_120831_modify_regionalgroups_add_forumgroups.php b/database/migrations/2020_08_17_120831_modify_regionalgroups_add_forumgroups.php new file mode 100644 index 0000000..19967e1 --- /dev/null +++ b/database/migrations/2020_08_17_120831_modify_regionalgroups_add_forumgroups.php @@ -0,0 +1,44 @@ +unsignedInteger('staff_group_id')->default(2)->after('deputy_id'); // Local group id + $table->unsignedInteger('voting_group_id')->default(2)->after('staff_group_id'); // Local group id + $table->unsignedInteger('mentor_group_id')->default(2)->after('voting_group_id'); // Local group id + $table->unsignedInteger('navler_group_id')->default(2)->after('mentor_group_id'); // Local group id + $table->unsignedInteger('eventler_group_id')->default(2)->after('navler_group_id'); // Local group id + $table->unsignedInteger('member_group_id')->default(2)->after('eventler_group_id'); // full members group + $table->unsignedInteger('guest_group_id')->default(2)->after('member_group_id'); // guest member group + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('regionalgroups_regionalgroups', function(Blueprint $table) { + $table->dropColumn('staff_group_id'); + $table->dropColumn('voting_group_id'); + $table->dropColumn('mentor_group_id'); + $table->dropColumn('navler_group_id'); + $table->dropColumn('eventler_group_id'); + $table->dropColumn('member_group_id'); + $table->dropColumn('guest_group_id'); + }); + } +} diff --git a/database/migrations/2020_08_22_090957_create_navigation_aid_tables.php b/database/migrations/2020_08_22_090957_create_navigation_aid_tables.php new file mode 100644 index 0000000..6d08074 --- /dev/null +++ b/database/migrations/2020_08_22_090957_create_navigation_aid_tables.php @@ -0,0 +1,59 @@ +id(); + $table->unsignedInteger('aerodrome_id'); + $table->string('ident', 3); + $table->string('heading', 3); + $table->unsignedInteger('width'); + $table->unsignedInteger('length'); + $table->unsignedSmallInteger('surface_type'); + $table->unsignedInteger('opposite_id')->nullable(); + $table->timestamps(); + }); + + Schema::create('navigation_navaids', function (Blueprint $table) { + $table->id(); + $table->unsignedTinyInteger('type'); + $table->string('name')->nullable(); + $table->string('heading', 3)->nullable(); + $table->string('ident', 5); + $table->decimal('frequency', 6, 3); + $table->unsignedSmallInteger('frequency_band'); + $table->string('remarks')->nullable(); + $table->timestamps(); + }); + + Schema::create('navigation_aerodrome_navaid', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger('aerodrome_id'); + $table->unsignedInteger('navaid_id'); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigation_runways'); + Schema::dropIfExists('navigation_navaids'); + Schema::dropIfExists('navigation_aerodrome_navaid'); + } +} diff --git a/database/migrations/2020_09_01_124402_create_training_tables_atd.php b/database/migrations/2020_09_01_124402_create_training_tables_atd.php new file mode 100644 index 0000000..1ce8b7c --- /dev/null +++ b/database/migrations/2020_09_01_124402_create_training_tables_atd.php @@ -0,0 +1,77 @@ +id(); + $table->unsignedInteger('regionalgroup_id'); + $table->unsignedInteger('trainee_id'); + $table->timestamps(); + }); + + Schema::create('uts_atd_sessions', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger('training_id'); + $table->unsignedInteger('mentor_id')->nullable(); + $table->unsignedInteger('second_mentor_id')->nullable(); + $table->unsignedInteger('station_id'); + $table->unsignedInteger('type'); + $table->longtext('mentor_comment_internal')->nullable(); + $table->longtext('mentor_comment_external')->nullable(); + $table->boolean('recomendation_solo')->default(false); + $table->boolean('recomendation_cpt')->default(false); + $table->enum('traffic', ['low', 'medium', 'high'])->nullable(); + $table->enum('complexity', ['easy', 'normal', 'difficult'])->nullable(); + $table->unsignedInteger('phraso_vfr_de')->default(0); + $table->unsignedInteger('phraso_vfr_en')->default(0); + $table->unsignedInteger('phraso_ifr_en')->default(0); + + $table->unsignedInteger('clearances')->default(0); + $table->unsignedInteger('conditionals')->default(0); + $table->unsignedInteger('handling_vfr')->default(0); + $table->unsignedInteger('handling_ifr')->default(0); + $table->unsignedInteger('ifr_pickup_cancelation')->default(0); + $table->unsignedInteger('vectoring')->default(0); + $table->unsignedInteger('separation')->default(0); + $table->unsignedInteger('speedmanagement')->default(0); + + $table->unsignedInteger('departurelist')->default(0); + $table->unsignedInteger('tags')->default(0); + + $table->unsignedInteger('handoff')->default(0); + $table->unsignedInteger('priorities')->default(0); + $table->unsignedInteger('bigpicture')->default(0); + + $table->boolean('accepted')->default(false); + + $table->timestamp('started_at')->nullable(); + $table->timestamp('ended_at')->nullable(); + $table->timestamps(); + }); + */ + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('uts_atd_sessions'); + Schema::dropIfExists('uts_atd_trainings'); + } +} diff --git a/database/migrations/2020_10_11_141153_create_imagestore_tables.php b/database/migrations/2020_10_11_141153_create_imagestore_tables.php new file mode 100644 index 0000000..af6ec7d --- /dev/null +++ b/database/migrations/2020_10_11_141153_create_imagestore_tables.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedInteger('account_id'); + $table->string('path'); + $table->string('name'); + $table->string('ext'); + $table->boolean('approved')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('imagestore'); + } +} diff --git a/database/migrations/2021_02_26_222101_create_regionalgroups_templates.php b/database/migrations/2021_02_26_222101_create_regionalgroups_templates.php new file mode 100644 index 0000000..68cbf90 --- /dev/null +++ b/database/migrations/2021_02_26_222101_create_regionalgroups_templates.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('regionalgroup_id'); + $table->string('name', 25); + $table->integer('order')->unsigned()->default(0); + $table->longText('message'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('regionalgroups_templates'); + } +} diff --git a/database/migrations/2021_03_22_204211_create_eventroutes_tables.php b/database/migrations/2021_03_22_204211_create_eventroutes_tables.php new file mode 100644 index 0000000..63285b4 --- /dev/null +++ b/database/migrations/2021_03_22_204211_create_eventroutes_tables.php @@ -0,0 +1,65 @@ +id(); + $table->string('name', 32); + $table->text('description'); + $table->text('link'); + $table->text('img_url'); + $table->boolean('require_order')->default(false); + $table->char('flight_rules', 1)->nullable()->default(null); + $table->text('aircrafts'); + $table->timestamp('begins_at')->default(\Carbon\Carbon::now()); + $table->timestamp('ends_at')->default(\Carbon\Carbon::now()); + $table->timestamps(); + }); + Schema::create('event_routelegs', function (Blueprint $table) { + $table->id(); + $table->foreignId('route_id'); + $table->foreignId('departureaerodrome_id'); + $table->foreignId('arrivalaerodrome_id'); + $table->timestamps(); + }); + Schema::create('event_account_routelegs', function (Blueprint $table) { + $table->id(); + $table->foreignId('account_id'); + $table->foreignId('routeleg_id'); + $table->timestamp('completed_at')->nullable()->default(null); + $table->foreignId('fight_data_id')->nullable()->default(null); + //$table->timestamps(); + }); + + \Illuminate\Support\Facades\DB::table('membership_permissions') + ->insert(array('name'=>'Eventrouten Verwaltung', 'slug'=>'administration.event.routes', 'description'=>'Erlaubt das Anlegen, Verwalten und Teilnehmeransicht von sogennanten Eventrouten.', + 'created_at'=>\Carbon\Carbon::now(), + 'updated_at'=>\Carbon\Carbon::now + ())); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('event_routes'); + Schema::dropIfExists('event_routelegs'); + Schema::dropIfExists('event_account_routelegs'); + } +} diff --git a/database/migrations/2022_03_16_130652_add_gitlab_settings.php b/database/migrations/2022_03_16_130652_add_gitlab_settings.php new file mode 100644 index 0000000..d1d3de0 --- /dev/null +++ b/database/migrations/2022_03_16_130652_add_gitlab_settings.php @@ -0,0 +1,37 @@ +unsignedInteger('gitlab_id')->nullable()->after('forum_id'); + }); + \Illuminate\Support\Facades\DB::table('membership_permissions') + ->insert(array('name'=>'Gitlab Account Verwaltung', 'slug'=>'administration.services.gitlab', 'description'=>'Erlaubt das Anlegen und Verwalten des eigenen Gitlabaccounts.', + 'created_at'=>\Carbon\Carbon::now(), + 'updated_at'=>\Carbon\Carbon::now + ())); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('membership_account_settings', function (Blueprint $table) { + $table->dropColumn('gitlab_id'); + }); + } +} diff --git a/database/migrations/2022_06_22_201843_create_partners_table.php b/database/migrations/2022_06_22_201843_create_partners_table.php new file mode 100644 index 0000000..a79d1bf --- /dev/null +++ b/database/migrations/2022_06_22_201843_create_partners_table.php @@ -0,0 +1,43 @@ +id(); + $table->unsignedInteger('created_by')->nullable(); + $table->string('name'); + $table->string('logo_url')->nullable(); + $table->string('link_url')->nullable(); + $table->longText('description_de')->nullable(); + $table->longText('description_en')->nullable(); + $table->timestamps(); + + $table->foreign('created_by') + ->references('id') + ->on('membership_accounts') + ->onUpdate('NO ACTION') + ->onDelete('SET NULL'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('partners'); + } +}; diff --git a/database/migrations/2023_01_30_231319_create_survey_keys_table.php b/database/migrations/2023_01_30_231319_create_survey_keys_table.php new file mode 100644 index 0000000..09b8dbb --- /dev/null +++ b/database/migrations/2023_01_30_231319_create_survey_keys_table.php @@ -0,0 +1,27 @@ +id(); + $table->integer('account_id')->unsigned(); + $table->string('name'); + $table->string('token'); + $table->string('url'); + $table->timestamp('valid_till')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('membership_accounts'); + }); + } + + public function down() + { + Schema::dropIfExists('survey_keys'); + } +} diff --git a/database/migrations/2023_04_17_091453_add_badge_to_eventroute.php b/database/migrations/2023_04_17_091453_add_badge_to_eventroute.php new file mode 100644 index 0000000..d08a750 --- /dev/null +++ b/database/migrations/2023_04_17_091453_add_badge_to_eventroute.php @@ -0,0 +1,32 @@ +integer("forum_badge_id")->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('event_routes', function (Blueprint $table) { + $table->dropColumn("forum_badge_id"); + }); + } +} diff --git a/database/migrations/vendor/junges/acl/2020_05_18_113137_create_permissions_table.php b/database/migrations/vendor/junges/acl/2020_05_18_113137_create_permissions_table.php new file mode 100644 index 0000000..c0da184 --- /dev/null +++ b/database/migrations/vendor/junges/acl/2020_05_18_113137_create_permissions_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('name')->unique()->nullable(false); + $table->string('slug')->unique()->nullable(false); + $table->text('description')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $tables = config('acl.tables'); + Schema::dropIfExists($tables['permissions']); + } +} diff --git a/database/migrations/vendor/junges/acl/2020_05_18_113138_create_user_has_permissions_table.php b/database/migrations/vendor/junges/acl/2020_05_18_113138_create_user_has_permissions_table.php new file mode 100644 index 0000000..b540bae --- /dev/null +++ b/database/migrations/vendor/junges/acl/2020_05_18_113138_create_user_has_permissions_table.php @@ -0,0 +1,46 @@ +unsignedInteger('account_id', false, true); + $table->bigInteger('permission_id', false, true); + $table->foreign('account_id', 'ahp_acc_id') + ->references('id') + ->on($usersTable) + ->onDelete('cascade'); + $table->foreign('permission_id', 'ahp_per_id') + ->references('id') + ->on($permissionsTable) + ->onDelete('cascade'); + $table->primary(['account_id', 'permission_id'], 'ahp_primary'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $userHasPermissionTable = config('acl.tables.user_has_permissions', 'user_has_permissions'); + Schema::dropIfExists($userHasPermissionTable); + } +} diff --git a/database/migrations/vendor/junges/acl/2020_05_18_113144_create_groups_table.php b/database/migrations/vendor/junges/acl/2020_05_18_113144_create_groups_table.php new file mode 100644 index 0000000..fb1b6a1 --- /dev/null +++ b/database/migrations/vendor/junges/acl/2020_05_18_113144_create_groups_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('name')->unique()->nullable(false); + $table->string('slug')->unique()->nullable(false); + $table->text('description')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $groupsTable = config('acl.tables.groups', 'groups'); + Schema::dropIfExists($groupsTable); + } +} diff --git a/database/migrations/vendor/junges/acl/2020_05_18_113159_create_group_has_permissions_table.php b/database/migrations/vendor/junges/acl/2020_05_18_113159_create_group_has_permissions_table.php new file mode 100644 index 0000000..34ec4ac --- /dev/null +++ b/database/migrations/vendor/junges/acl/2020_05_18_113159_create_group_has_permissions_table.php @@ -0,0 +1,46 @@ +bigInteger('group_id', false, true); + $table->bigInteger('permission_id', false, true); + $table->foreign('group_id', 'ghp_grp_id') + ->references('id') + ->on($groupsTable) + ->onDelete('cascade'); + $table->foreign('permission_id', 'ghp_per_id') + ->references('id') + ->on($permissionsTable) + ->onDelete('cascade'); + $table->primary(['group_id', 'permission_id'], 'ghp_primary'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $groupHasPermissionsTable = config('acl.tables.group_has_permissions', 'group_has_permissions'); + Schema::dropIfExists($groupHasPermissionsTable); + } +} diff --git a/database/migrations/vendor/junges/acl/2020_05_18_113214_create_user_has_groups_table.php b/database/migrations/vendor/junges/acl/2020_05_18_113214_create_user_has_groups_table.php new file mode 100644 index 0000000..ea039cc --- /dev/null +++ b/database/migrations/vendor/junges/acl/2020_05_18_113214_create_user_has_groups_table.php @@ -0,0 +1,45 @@ +unsignedInteger('account_id', false, true); + $table->bigInteger('group_id', false, true); + $table->foreign('account_id', 'ahg_acc_id') + ->references('id') + ->on($usersTable) + ->onDelete('cascade'); + $table->foreign('group_id', 'ahg_grp_id') + ->references('id') + ->on($groupsTable) + ->onDelete('cascade'); + $table->primary(['account_id', 'group_id'], 'ahg_primary'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $userHasGroupsTable = config('acl.tables.user_has_groups', 'user_has_groups'); + Schema::dropIfExists($userHasGroupsTable); + } +} diff --git a/database/seeds/AerodromeSeeder.php b/database/seeds/AerodromeSeeder.php new file mode 100644 index 0000000..e690ae5 --- /dev/null +++ b/database/seeds/AerodromeSeeder.php @@ -0,0 +1,44 @@ +command->getOutput()->writeln('Loaded '.$ac.' aerodromes from file.'); + + DB::table('navigation_aerodromes')->truncate(); + $this->command->getOutput()->writeln('Truncated aerodromes table.'); + + $this->command->getOutput()->writeln('Starting seeding of new information...'); + $this->command->getOutput()->progressStart($ac); + + foreach ($airports as $icao => $data) { + $a = new \App\Models\Navigation\Aerodrome(); + $a->icao = $data['icao']; + $a->name = $data['name']; + $a->description = ''; + $a->iata = $data['iata']; + $a->elevation = (float) $data['elevation']; + $a->latitude = (float) $data['lat']; + $a->longitude = (float) $data['lon']; + $a->city = $data['city']; + $a->country = $data['country']; + $a->state = $data['state']; + $a->save(); + $this->command->getOutput()->progressAdvance(); + } + + $this->command->getOutput()->progressFinish(); + $this->command->getOutput()->writeln('Finished seeding.'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 0000000..3b7d87b --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,25 @@ +call(AerodromeSeeder::class); + + $this->call(PermissionSeeder::class); + + $this->call(StationSeeder::class); + + $this->call(RegionalgroupSeeder::class); + + $this->call(GroupSeeder::class); + + } +} diff --git a/database/seeds/GroupSeeder.php b/database/seeds/GroupSeeder.php new file mode 100644 index 0000000..63e6f88 --- /dev/null +++ b/database/seeds/GroupSeeder.php @@ -0,0 +1,29 @@ + $group, + 'slug' => Illuminate\Support\Str::slug($group, '.'), + 'description' => $group, + ]); + } + } +} diff --git a/database/seeds/PermissionSeeder.php b/database/seeds/PermissionSeeder.php new file mode 100644 index 0000000..54000aa --- /dev/null +++ b/database/seeds/PermissionSeeder.php @@ -0,0 +1,67 @@ +command->getOutput()->writeln('Starting PermissionSeeder.'); + + $this->command->getOutput()->writeln('Removing old permissions.'); + $permissionModel = app(config('acl.models.permission')); + $permissionModel->delete(); + + $this->command->getOutput()->writeln('Starting seeding of new information...'); + $this->command->getOutput()->progressStart(count($this->_permissions)); + + foreach ($this->_permissions as $permission) { + try { + $permissionModel = app(config('acl.models.permission')); + try { + $p = $permissionModel->where('slug', $permission[1]) + ->orWhere('name', $permission[0]) + ->first(); + if (! is_null($p)) { + throw PermissionAlreadyExistsException::create(); + } + $permissionModel->create([ + 'name' => $permission[0], + 'slug' => $permission[1], + 'description' => $permission[2], + ]); + } catch (\Exception $exception) { + $this->command->getOutput()->writeln($exception->getMessage()); + } + } catch (\Exception $exception) { + $this->command->getOutput()->writeln($exception->getMessage()); + } + $this->command->getOutput()->progressAdvance(); + } + + $this->command->getOutput()->progressFinish(); + $this->command->getOutput()->writeln('Finished seeding.'); + } +} diff --git a/database/seeds/RegionalgroupSeeder.php b/database/seeds/RegionalgroupSeeder.php new file mode 100644 index 0000000..426fb36 --- /dev/null +++ b/database/seeds/RegionalgroupSeeder.php @@ -0,0 +1,45 @@ + 'RG Bremen', 'fir_id' => 1], + ['name' => 'RG Berlin', 'fir_id' => 1], + ['name' => 'RG Düsseldorf', 'fir_id' => 2], + ['name' => 'RG Frankfurt', 'fir_id' => 2], + ['name' => 'RG München', 'fir_id' => 3], + ]; + + /** + * Run the database seeds. + * + * @return void + */ + public function run() + { + DB::table('regionalgroups_firs')->truncate(); + DB::table('regionalgroups_regionalgroups')->truncate(); + + foreach ($this->firs as $fir) { + $f = new \App\Models\Regionalgroups\FlightInformationRegion(); + $f->name = $fir; + $f->save(); + } + + foreach ($this->regionalgroups as $rg) { + $regionalgroup = new \App\Models\Regionalgroups\Regionalgroup(); + $regionalgroup->name = $rg['name']; + $regionalgroup->fir_id = $rg['fir_id']; + $regionalgroup->save(); + } + } +} diff --git a/database/seeds/StationSeeder.php b/database/seeds/StationSeeder.php new file mode 100644 index 0000000..e95174b --- /dev/null +++ b/database/seeds/StationSeeder.php @@ -0,0 +1,1875 @@ + [ + 'callsign' => 'EDAB_I_TWR', + 'freq' => '120.60', + 'name' => 'Bautzen Info', + ], + 1 => [ + 'callsign' => 'EDAC_I_TWR', + 'freq' => '123.57', + 'name' => 'Altenburg Info', + ], + 2 => [ + 'callsign' => 'EDAG_I_TWR', + 'freq' => '123.00', + 'name' => 'Großrückerswalde Info', + ], + 3 => [ + 'callsign' => 'EDAH_TWR', + 'freq' => '132.82', + 'name' => 'Heringsdorf Tower', + ], + 4 => [ + 'callsign' => 'EDAK_I_TWR', + 'freq' => '122.70', + 'name' => 'Grossenhain Info', + ], + 5 => [ + 'callsign' => 'EDAU_I_TWR', + 'freq' => '122.60', + 'name' => 'Riesa Info', + ], + 6 => [ + 'callsign' => 'EDAV_I_TWR', + 'freq' => '119.05', + 'name' => 'Finow Info', + ], + 7 => [ + 'callsign' => 'EDAY_I_TWR', + 'freq' => '123.05', + 'name' => 'Strausberg Info', + ], + 8 => [ + 'callsign' => 'EDAZ_I_TWR', + 'freq' => '131.15', + 'name' => 'Schönhagen Info', + ], + 9 => [ + 'callsign' => 'EDBB_DEP', + 'freq' => '120.62', + 'name' => 'Bremen Radar (Berlin departure)', + ], + 10 => [ + 'callsign' => 'EDBB_F_APP', + 'freq' => '121.12', + 'name' => 'Berlin Director', + ], + 11 => [ + 'callsign' => 'EDBB_N_APP', + 'freq' => '119.62', + 'name' => 'Bremen Radar (Berlin Arrival North)', + ], + 12 => [ + 'callsign' => 'EDBB_S_APP', + 'freq' => '126.42', + 'name' => 'Bremen Radar (Berlin Arrival South)', + ], + 13 => [ + 'callsign' => 'EDBC_GND', + 'freq' => '121.82', + 'name' => 'Cochstedt Ground', + ], + 14 => [ + 'callsign' => 'EDBC_TWR', + 'freq' => '131.12', + 'name' => 'Cochstedt Tower', + ], + 15 => [ + 'callsign' => 'EDBH_I_TWR', + 'freq' => '118.07', + 'name' => 'Barth Info', + ], + 16 => [ + 'callsign' => 'EDBK_I_TWR', + 'freq' => '122.72', + 'name' => 'Kyritz Info', + ], + 17 => [ + 'callsign' => 'EDBM_I_TWR', + 'freq' => '119.30', + 'name' => 'Magdeburg Info', + ], + 18 => [ + 'callsign' => 'EDBN_I_TWR', + 'freq' => '119.17', + 'name' => 'Neubrandenburg Info', + ], + 19 => [ + 'callsign' => 'EDBW_I_TWR', + 'freq' => '122.60', + 'name' => 'Werneuchen Info', + ], + 20 => [ + 'callsign' => 'EDBY_I_TWR', + 'freq' => '122.85', + 'name' => 'Schmoldow Info', + ], + 21 => [ + 'callsign' => 'EDCA_I_TWR', + 'freq' => '122.65', + 'name' => 'Anklam Info', + ], + 22 => [ + 'callsign' => 'EDCD_I_TWR', + 'freq' => '118.12', + 'name' => 'Drewitz Info', + ], + 23 => [ + 'callsign' => 'EDCG_I_TWR', + 'freq' => '123.00', + 'name' => 'Rügen Info', + ], + 24 => [ + 'callsign' => 'EDCJ_I_TWR', + 'freq' => '122.50', + 'name' => 'Jahnsdorf Info', + ], + 25 => [ + 'callsign' => 'EDCM_I_TWR', + 'freq' => '122.05', + 'name' => 'Kamenz Info', + ], + 26 => [ + 'callsign' => 'EDCP_I_TWR', + 'freq' => '122.47', + 'name' => 'Peenemünde Info', + ], + 27 => [ + 'callsign' => 'EDCS_I_TWR', + 'freq' => '123.65', + 'name' => 'Saarmund Info', + ], + 28 => [ + 'callsign' => 'EDDB_A_GND', + 'freq' => '129.60', + 'name' => 'Schönefeld Ground', + ], + 29 => [ + 'callsign' => 'EDDB_ATIS', + 'freq' => '124.95', + 'name' => 'Schönefeld ATIS', + ], + 30 => [ + 'callsign' => 'EDDB_DEL', + 'freq' => '121.60', + 'name' => 'Schönefeld Delivery', + ], + 31 => [ + 'callsign' => 'EDDB_N_GND', + 'freq' => '129.50', + 'name' => 'Schönefeld Ground', + ], + 32 => [ + 'callsign' => 'EDDB_N_TWR', + 'freq' => '120.02', + 'name' => 'Schönefeld Tower', + ], + 33 => [ + 'callsign' => 'EDDB_S_GND', + 'freq' => '121.70', + 'name' => 'Schönefeld Ground', + ], + 34 => [ + 'callsign' => 'EDDB_S_TWR', + 'freq' => '118.80', + 'name' => 'Schönefeld Tower (South)', + ], + 35 => [ + 'callsign' => 'EDDB_V_TWR', + 'freq' => '119.57', + 'name' => 'Schönefeld Tower (VFR)', + ], + 36 => [ + 'callsign' => 'EDDC_A_GND', + 'freq' => '121.75', + 'name' => 'Dresden Apron', + ], + 37 => [ + 'callsign' => 'EDDC_APP', + 'freq' => '125.87', + 'name' => 'München Radar (SASL Sachsen Low)', + ], + 38 => [ + 'callsign' => 'EDDC_ATIS', + 'freq' => '118.87', + 'name' => 'Dresden ATIS', + ], + 39 => [ + 'callsign' => 'EDDC_GND', + 'freq' => '121.97', + 'name' => 'Dresden Ground', + ], + 40 => [ + 'callsign' => 'EDDC_TWR', + 'freq' => '122.92', + 'name' => 'Dresden Tower', + ], + 41 => [ + 'callsign' => 'EDDE_A_GND', + 'freq' => '121.90', + 'name' => 'Erfurt Apron', + ], + 42 => [ + 'callsign' => 'EDDE_ATIS', + 'freq' => '133.42', + 'name' => 'Erfurt ATIS', + ], + 43 => [ + 'callsign' => 'EDDE_GND', + 'freq' => '121.75', + 'name' => 'Erfurt Ground', + ], + 44 => [ + 'callsign' => 'EDDE_TWR', + 'freq' => '121.15', + 'name' => 'Erfurt Tower', + ], + 45 => [ + 'callsign' => 'EDDF_ATIS', + 'freq' => '118.02', + 'name' => 'Frankfurt ATIS', + ], + 46 => [ + 'callsign' => 'EDDF_D_APP', + 'freq' => '120.15', + 'name' => 'Langen Radar (Frankfurt Departure)', + ], + 47 => [ + 'callsign' => 'EDDF_DEL', + 'freq' => '121.90', + 'name' => 'Frankfurt Delivery', + ], + 48 => [ + 'callsign' => 'EDDF_E_GND', + 'freq' => '121.95', + 'name' => 'Frankfurt East Apron', + ], + 49 => [ + 'callsign' => 'EDDF_F_APP', + 'freq' => '127.27', + 'name' => 'Frankfurt North Director', + ], + 50 => [ + 'callsign' => 'EDDF_GND', + 'freq' => '121.80', + 'name' => 'Frankfurt Ground', + ], + 51 => [ + 'callsign' => 'EDDF_H_APP', + 'freq' => '119.02', + 'name' => 'Langen Radar (Frankfurt High Approach)', + ], + 52 => [ + 'callsign' => 'EDDF_N_APP', + 'freq' => '120.80', + 'name' => 'Langen Radar (Frankfurt North Approach)', + ], + 53 => [ + 'callsign' => 'EDDF_P_GND', + 'freq' => '121.85', + 'name' => 'Frankfurt Pushback Apron', + ], + 54 => [ + 'callsign' => 'EDDF_S_APP', + 'freq' => '125.35', + 'name' => 'Langen Radar (Frankfurt South Approach)', + ], + 55 => [ + 'callsign' => 'EDDF_TWR', + 'freq' => '119.90', + 'name' => 'Frankfurt Main Tower', + ], + 56 => [ + 'callsign' => 'EDDF_U_APP', + 'freq' => '118.50', + 'name' => 'Frankfurt South Director', + ], + 57 => [ + 'callsign' => 'EDDF_W_GND', + 'freq' => '121.75', + 'name' => 'Frankfurt West Apron', + ], + 58 => [ + 'callsign' => 'EDDF_W_TWR', + 'freq' => '124.85', + 'name' => 'Frankfurt Tower (West)', + ], + 59 => [ + 'callsign' => 'EDDG_APP', + 'freq' => '129.30', + 'name' => 'Langen Radar (HMML Hamm Low)', + ], + 60 => [ + 'callsign' => 'EDDG_ATIS', + 'freq' => '127.17', + 'name' => 'Münster ATIS', + ], + 61 => [ + 'callsign' => 'EDDG_GND', + 'freq' => '121.87', + 'name' => 'Münster Ground', + ], + 62 => [ + 'callsign' => 'EDDG_TWR', + 'freq' => '129.80', + 'name' => 'Münster Tower', + ], + 63 => [ + 'callsign' => 'EDDH_ATIS', + 'freq' => '123.12', + 'name' => 'Hamburg ATIS', + ], + 64 => [ + 'callsign' => 'EDDH_DEL', + 'freq' => '121.80', + 'name' => 'Hamburg Ground', + ], + 65 => [ + 'callsign' => 'EDDH_E_APP', + 'freq' => '127.67', + 'name' => 'Bremen Radar (HAME Hamburg Arrival East)', + ], + 66 => [ + 'callsign' => 'EDDH_F_APP', + 'freq' => '118.20', + 'name' => 'Hamburg Director', + ], + 67 => [ + 'callsign' => 'EDDH_GND', + 'freq' => '121.70', + 'name' => 'Hamburg Apron', + ], + 68 => [ + 'callsign' => 'EDDH_TWR', + 'freq' => '126.85', + 'name' => 'Hamburg Tower', + ], + 69 => [ + 'callsign' => 'EDDH_W_APP', + 'freq' => '134.25', + 'name' => 'Bremen Radar (HAMW Hamburg Arrival West)', + ], + 70 => [ + 'callsign' => 'EDDI_ATIS', + 'freq' => '126.02', + 'name' => 'Tempelhof ATIS', + ], + 71 => [ + 'callsign' => 'EDDI_GND', + 'freq' => '121.95', + 'name' => 'Tempelhof Ground', + ], + 72 => [ + 'callsign' => 'EDDI_TWR', + 'freq' => '119.57', + 'name' => 'Tempelhof Tower', + ], + 73 => [ + 'callsign' => 'EDDK_APP', + 'freq' => '118.75', + 'name' => 'Langen Radar (KAE Köln Arrival East)', + ], + 74 => [ + 'callsign' => 'EDDK_ATIS', + 'freq' => '124.10', + 'name' => 'Köln/Bonn ATIS', + ], + 75 => [ + 'callsign' => 'EDDK_DEL', + 'freq' => '121.85', + 'name' => 'Köln/Bonn Delivery', + ], + 76 => [ + 'callsign' => 'EDDK_F_APP', + 'freq' => '121.05', + 'name' => 'Köln/Bonn Director', + ], + 77 => [ + 'callsign' => 'EDDK_GND', + 'freq' => '121.72', + 'name' => 'Köln/Bonn Ground', + ], + 78 => [ + 'callsign' => 'EDDK_TWR', + 'freq' => '124.97', + 'name' => 'Köln/Bonn Tower', + ], + 79 => [ + 'callsign' => 'EDDK_W_APP', + 'freq' => '135.35', + 'name' => 'Langen Radar (KAW Köln Arrival West)', + ], + 80 => [ + 'callsign' => 'EDDL_APP', + 'freq' => '128.55', + 'name' => 'Langen Radar (Düsseldorf Arrival)', + ], + 81 => [ + 'callsign' => 'EDDL_ATIS', + 'freq' => '123.77', + 'name' => 'Düsseldorf ATIS', + ], + 82 => [ + 'callsign' => 'EDDL_DEL', + 'freq' => '121.77', + 'name' => 'Düsseldorf Delivery', + ], + 83 => [ + 'callsign' => 'EDDL_E_GND', + 'freq' => '121.60', + 'name' => 'Düsseldorf Ground (East)', + ], + 84 => [ + 'callsign' => 'EDDL_F_APP', + 'freq' => '128.65', + 'name' => 'Düsseldorf Director', + ], + 85 => [ + 'callsign' => 'EDDL_N_APP', + 'freq' => '128.50', + 'name' => 'Langen Radar (Departure North)', + ], + 86 => [ + 'callsign' => 'EDDL_S_APP', + 'freq' => '121.35', + 'name' => 'Langen Radar (Departure South)', + ], + 87 => [ + 'callsign' => 'EDDL_TWR', + 'freq' => '118.30', + 'name' => 'Düsseldorf Tower', + ], + 88 => [ + 'callsign' => 'EDDL_W_GND', + 'freq' => '121.90', + 'name' => 'Düsseldorf Ground (West)', + ], + 89 => [ + 'callsign' => 'EDDM_1_APP', + 'freq' => '128.02', + 'name' => 'München Radar (North High)', + ], + 90 => [ + 'callsign' => 'EDDM_1_GND', + 'freq' => '121.77', + 'name' => 'München Apron (Apron 1 and 6-9)', + ], + 91 => [ + 'callsign' => 'EDDM_2_APP', + 'freq' => '120.77', + 'name' => 'München Radar (South High)', + ], + 92 => [ + 'callsign' => 'EDDM_2_GND', + 'freq' => '121.70', + 'name' => 'München Apron (Apron 2)', + ], + 93 => [ + 'callsign' => 'EDDM_3_GND', + 'freq' => '121.92', + 'name' => 'München Apron (Apron 3)', + ], + 94 => [ + 'callsign' => 'EDDM_ATIS', + 'freq' => '123.12', + 'name' => 'München ATIS', + ], + 95 => [ + 'callsign' => 'EDDM_DEL', + 'freq' => '121.72', + 'name' => 'München Delivery', + ], + 96 => [ + 'callsign' => 'EDDM_F_APP', + 'freq' => '118.82', + 'name' => 'München Director', + ], + 97 => [ + 'callsign' => 'EDDM_N_APP', + 'freq' => '123.90', + 'name' => 'München Radar (North Low)', + ], + 98 => [ + 'callsign' => 'EDDM_N_GND', + 'freq' => '121.97', + 'name' => 'München Ground (North)', + ], + 99 => [ + 'callsign' => 'EDDM_N_TWR', + 'freq' => '118.70', + 'name' => 'München Tower (North)', + ], + 100 => [ + 'callsign' => 'EDDM_S_APP', + 'freq' => '127.95', + 'name' => 'München Radar (South Low)', + ], + 101 => [ + 'callsign' => 'EDDM_S_GND', + 'freq' => '121.82', + 'name' => 'München Ground (South)', + ], + 102 => [ + 'callsign' => 'EDDM_S_TWR', + 'freq' => '120.50', + 'name' => 'München Tower (South)', + ], + 103 => [ + 'callsign' => 'EDDN_APP', + 'freq' => '129.52', + 'name' => 'München Radar (FRKL Franken Low)', + ], + 104 => [ + 'callsign' => 'EDDN_ATIS', + 'freq' => '123.07', + 'name' => 'Nürnberg ATIS', + ], + 105 => [ + 'callsign' => 'EDDN_F_APP', + 'freq' => '119.47', + 'name' => 'Nürnberg Director', + ], + 106 => [ + 'callsign' => 'EDDN_GND', + 'freq' => '118.10', + 'name' => 'Nürnberg Ground', + ], + 107 => [ + 'callsign' => 'EDDN_TWR', + 'freq' => '118.30', + 'name' => 'Nürnberg Tower', + ], + 108 => [ + 'callsign' => 'EDDP_APP', + 'freq' => '126.17', + 'name' => 'München Radar (TRGL Thüringen Low)', + ], + 109 => [ + 'callsign' => 'EDDP_ATIS', + 'freq' => '123.95', + 'name' => 'Leipzig ATIS', + ], + 110 => [ + 'callsign' => 'EDDP_DEL', + 'freq' => '121.80', + 'name' => 'Leipzig Delivery', + ], + 111 => [ + 'callsign' => 'EDDP_F_APP', + 'freq' => '128.47', + 'name' => 'Leipzig Director', + ], + 112 => [ + 'callsign' => 'EDDP_GND', + 'freq' => '121.67', + 'name' => 'Leipzig Ground', + ], + 113 => [ + 'callsign' => 'EDDP_N_TWR', + 'freq' => '125.95', + 'name' => 'Leipzig Tower North', + ], + 114 => [ + 'callsign' => 'EDDP_S_TWR', + 'freq' => '121.10', + 'name' => 'Leipzig Tower South', + ], + 115 => [ + 'callsign' => 'EDDR_APP', + 'freq' => '129.67', + 'name' => 'Langen Radar (PFA Pfalz)', + ], + 116 => [ + 'callsign' => 'EDDR_ATIS', + 'freq' => '125.30', + 'name' => 'Saarbrücken ATIS', + ], + 117 => [ + 'callsign' => 'EDDR_TWR', + 'freq' => '118.35', + 'name' => 'Saarbrücken Tower', + ], + 118 => [ + 'callsign' => 'EDDS_2_TWR', + 'freq' => '119.05', + 'name' => 'Stuttgart Tower (VFR)', + ], + 119 => [ + 'callsign' => 'EDDS_ATIS', + 'freq' => '126.12', + 'name' => 'Stuttgart ATIS', + ], + 120 => [ + 'callsign' => 'EDDS_DEL', + 'freq' => '121.90', + 'name' => 'Stuttgart Delivery', + ], + 121 => [ + 'callsign' => 'EDDS_F_APP', + 'freq' => '119.85', + 'name' => 'Stuttgart Director', + ], + 122 => [ + 'callsign' => 'EDDS_GND', + 'freq' => '118.60', + 'name' => 'Stuttgart Ground', + ], + 123 => [ + 'callsign' => 'EDDS_N_APP', + 'freq' => '125.05', + 'name' => 'Langen Radar (STG Stuttgart)', + ], + 124 => [ + 'callsign' => 'EDDS_S_APP', + 'freq' => '119.20', + 'name' => 'Langen Radar (RTL Reutlingen)', + ], + 125 => [ + 'callsign' => 'EDDS_TWR', + 'freq' => '118.80', + 'name' => 'Stuttgart Tower', + ], + 126 => [ + 'callsign' => 'EDDT_ATIS', + 'freq' => '125.90', + 'name' => 'Tegel ATIS', + ], + 127 => [ + 'callsign' => 'EDDT_DEL', + 'freq' => '121.92', + 'name' => 'Tegel Delivery', + ], + 128 => [ + 'callsign' => 'EDDT_GND', + 'freq' => '121.75', + 'name' => 'Tegel Ground', + ], + 129 => [ + 'callsign' => 'EDDT_TWR', + 'freq' => '124.52', + 'name' => 'Tegel Tower', + ], + 130 => [ + 'callsign' => 'EDDV_APP', + 'freq' => '131.32', + 'name' => 'Bremen Radar (HAN Hannover)', + ], + 131 => [ + 'callsign' => 'EDDV_ATIS', + 'freq' => '132.12', + 'name' => 'Hannover ATIS', + ], + 132 => [ + 'callsign' => 'EDDV_DEL', + 'freq' => '120.40', + 'name' => 'Hannover Delivery', + ], + 133 => [ + 'callsign' => 'EDDV_F_APP', + 'freq' => '119.60', + 'name' => 'Hannover Director', + ], + 134 => [ + 'callsign' => 'EDDV_GND', + 'freq' => '121.95', + 'name' => 'Hannover Ground', + ], + 135 => [ + 'callsign' => 'EDDV_TWR', + 'freq' => '120.17', + 'name' => 'Hannover Tower', + ], + 136 => [ + 'callsign' => 'EDDW_APP', + 'freq' => '124.80', + 'name' => 'Bremen Radar (ALEL Aller East Low)', + ], + 137 => [ + 'callsign' => 'EDDW_ATIS', + 'freq' => '120.12', + 'name' => 'Bremen ATIS', + ], + 138 => [ + 'callsign' => 'EDDW_DEL', + 'freq' => '134.82', + 'name' => 'Bremen Delivery', + ], + 139 => [ + 'callsign' => 'EDDW_F_APP', + 'freq' => '125.85', + 'name' => 'Bremen Director', + ], + 140 => [ + 'callsign' => 'EDDW_GND', + 'freq' => '121.75', + 'name' => 'Bremen Ground', + ], + 141 => [ + 'callsign' => 'EDDW_TWR', + 'freq' => '120.32', + 'name' => 'Bremen Tower', + ], + 142 => [ + 'callsign' => 'EDEH_I_TWR', + 'freq' => '132.05', + 'name' => 'Herrenteich Info', + ], + 143 => [ + 'callsign' => 'EDEL_I_TWR', + 'freq' => '122.87', + 'name' => 'Langenlonsheim Info', + ], + 144 => [ + 'callsign' => 'EDER_I_TWR', + 'freq' => '118.65', + 'name' => 'Wasserkuppe Info', + ], + 145 => [ + 'callsign' => 'EDFA_I_TWR', + 'freq' => '121.02', + 'name' => 'Anspach Info', + ], + 146 => [ + 'callsign' => 'EDFB_I_TWR', + 'freq' => '120.42', + 'name' => 'Reichelsheim Info', + ], + 147 => [ + 'callsign' => 'EDFC_I_TWR', + 'freq' => '132.42', + 'name' => 'Aschaffenburg Info', + ], + 148 => [ + 'callsign' => 'EDFE_GND', + 'freq' => '121.72', + 'name' => 'Egelsbach Apron', + ], + 149 => [ + 'callsign' => 'EDFE_I_TWR', + 'freq' => '118.40', + 'name' => 'Egelsbach Info', + ], + 150 => [ + 'callsign' => 'EDFG_I_TWR', + 'freq' => '123.05', + 'name' => 'Gelnhausen Info', + ], + 151 => [ + 'callsign' => 'EDFH_APP', + 'freq' => '125.60', + 'name' => 'Langen Radar (EIF Eifel)', + ], + 152 => [ + 'callsign' => 'EDFH_ATIS', + 'freq' => '120.90', + 'name' => 'Hahn ATIS', + ], + 153 => [ + 'callsign' => 'EDFH_GND', + 'freq' => '121.97', + 'name' => 'Hahn Ground', + ], + 154 => [ + 'callsign' => 'EDFH_TWR', + 'freq' => '119.65', + 'name' => 'Hahn Tower', + ], + 155 => [ + 'callsign' => 'EDFM_APP', + 'freq' => '129.35', + 'name' => 'Langen Radar (NKRL Neckar Low)', + ], + 156 => [ + 'callsign' => 'EDFM_ATIS', + 'freq' => '122.50', + 'name' => 'Mannheim ATIS', + ], + 157 => [ + 'callsign' => 'EDFM_TWR', + 'freq' => '129.77', + 'name' => 'Mannheim Tower', + ], + 158 => [ + 'callsign' => 'EDFQ_APP', + 'freq' => '124.72', + 'name' => 'Langen Radar (GIN Giessen)', + ], + 159 => [ + 'callsign' => 'EDFQ_ATIS', + 'freq' => '118.82', + 'name' => 'Allendorf ATIS', + ], + 160 => [ + 'callsign' => 'EDFQ_I_TWR', + 'freq' => '118.17', + 'name' => 'Allendorf Info', + ], + 161 => [ + 'callsign' => 'EDFU_I_TWR', + 'freq' => '122.37', + 'name' => 'Mainbullau Info', + ], + 162 => [ + 'callsign' => 'EDFV_I_TWR', + 'freq' => '124.60', + 'name' => 'Worms Info', + ], + 163 => [ + 'callsign' => 'EDFY_I_TWR', + 'freq' => '123.60', + 'name' => 'Elz Info', + ], + 164 => [ + 'callsign' => 'EDFZ_I_TWR', + 'freq' => '122.92', + 'name' => 'Mainz Info', + ], + 165 => [ + 'callsign' => 'EDGG_A_CTR', + 'freq' => '134.20', + 'name' => 'Langen Radar (HAB Hammelburg)', + ], + 166 => [ + 'callsign' => 'EDGG_B_CTR', + 'freq' => '127.05', + 'name' => 'Langen Radar (BAD Baden)', + ], + 167 => [ + 'callsign' => 'EDGG_CTR', + 'freq' => '135.72', + 'name' => 'Langen Radar (Complete)', + ], + 168 => [ + 'callsign' => 'EDGG_D_CTR', + 'freq' => '125.20', + 'name' => 'Langen Radar (DKB Dinkelsbühl)', + ], + 169 => [ + 'callsign' => 'EDGG_E_CTR', + 'freq' => '127.72', + 'name' => 'Langen Radar (HEF Hersfeld)', + ], + 170 => [ + 'callsign' => 'EDGG_F_CTR', + 'freq' => '128.95', + 'name' => 'Langen Information (FIS, South and East)', + ], + 171 => [ + 'callsign' => 'EDGG_G_CTR', + 'freq' => '124.42', + 'name' => 'Langen Radar (GED Gedern)', + ], + 172 => [ + 'callsign' => 'EDGG_H_CTR', + 'freq' => '129.17', + 'name' => 'Langen Radar (HMMM Hamm Medium)', + ], + 173 => [ + 'callsign' => 'EDGG_I_CTR', + 'freq' => '124.37', + 'name' => 'Langen Radar (KIR Kirn)', + ], + 174 => [ + 'callsign' => 'EDGG_K_CTR', + 'freq' => '125.67', + 'name' => 'Langen Radar (KNG König)', + ], + 175 => [ + 'callsign' => 'EDGG_L_CTR', + 'freq' => '131.30', + 'name' => 'Langen Radar (LBU Luburg)', + ], + 176 => [ + 'callsign' => 'EDGG_M_CTR', + 'freq' => '119.67', + 'name' => 'Langen Radar (MAN Main)', + ], + 177 => [ + 'callsign' => 'EDGG_N_CTR', + 'freq' => '127.50', + 'name' => 'Langen Radar (NKRH Neckar High)', + ], + 178 => [ + 'callsign' => 'EDGG_P_CTR', + 'freq' => '135.65', + 'name' => 'Langen Radar (PAD Paderborn High)', + ], + 179 => [ + 'callsign' => 'EDGG_R_CTR', + 'freq' => '124.47', + 'name' => 'Langen Radar (RUD Rüdesheim)', + ], + 180 => [ + 'callsign' => 'EDGG_S_CTR', + 'freq' => '125.40', + 'name' => 'Langen Radar (PSA Spessart)', + ], + 181 => [ + 'callsign' => 'EDGG_T_CTR', + 'freq' => '127.62', + 'name' => 'Langen Radar (TAU Taunus)', + ], + 182 => [ + 'callsign' => 'EDGG_U_CTR', + 'freq' => '123.52', + 'name' => 'Langen Information (FIS, West)', + ], + 183 => [ + 'callsign' => 'EDGG_V_CTR', + 'freq' => '119.15', + 'name' => 'Langen Information (FIS, North and East)', + ], + 184 => [ + 'callsign' => 'EDGG_W_CTR', + 'freq' => '129.87', + 'name' => 'Langen Information (FIS, North and West)', + ], + 185 => [ + 'callsign' => 'EDGG_X_CTR', + 'freq' => '130.97', + 'name' => 'Langen Radar (Special event)', + ], + 186 => [ + 'callsign' => 'EDGG_Z_CTR', + 'freq' => '120.57', + 'name' => 'Langen Radar (KTG Kitzingen)', + ], + 187 => [ + 'callsign' => 'EDGP_I_TWR', + 'freq' => '122.00', + 'name' => 'Oppenheim Information', + ], + 188 => [ + 'callsign' => 'EDGS_APP', + 'freq' => '124.90', + 'name' => 'Langen Radar (SIG Siegen)', + ], + 189 => [ + 'callsign' => 'EDGS_ATIS', + 'freq' => '128.70', + 'name' => 'Siegerland ATIS', + ], + 190 => [ + 'callsign' => 'EDGS_I_TWR', + 'freq' => '120.37', + 'name' => 'Siegerland Info', + ], + 191 => [ + 'callsign' => 'EDHE_I_TWR', + 'freq' => '122.70', + 'name' => 'Uetersen Info', + ], + 192 => [ + 'callsign' => 'EDHG_I_TWR', + 'freq' => '122.17', + 'name' => 'Lüneburg Info', + ], + 193 => [ + 'callsign' => 'EDHI_TWR', + 'freq' => '123.25', + 'name' => 'Finkenwerder Tower', + ], + 194 => [ + 'callsign' => 'EDHK_I_TWR', + 'freq' => '119.97', + 'name' => 'Kiel Info', + ], + 195 => [ + 'callsign' => 'EDHL_ATIS', + 'freq' => '119.92', + 'name' => 'Lübeck ATIS', + ], + 196 => [ + 'callsign' => 'EDHL_GND', + 'freq' => '121.77', + 'name' => 'Lübeck Ground', + ], + 197 => [ + 'callsign' => 'EDHL_TWR', + 'freq' => '128.70', + 'name' => 'Lübeck Tower', + ], + 198 => [ + 'callsign' => 'EDJA_APP', + 'freq' => '129.45', + 'name' => 'München Radar (LCH Lech)', + ], + 199 => [ + 'callsign' => 'EDJA_ATIS', + 'freq' => '118.85', + 'name' => 'Memmingen ATIS', + ], + 200 => [ + 'callsign' => 'EDJA_GND', + 'freq' => '121.67', + 'name' => 'Memmingen Ground', + ], + 201 => [ + 'callsign' => 'EDJA_TWR', + 'freq' => '126.85', + 'name' => 'Memmingen Tower', + ], + 202 => [ + 'callsign' => 'EDKA_I_TWR', + 'freq' => '122.87', + 'name' => 'Aachen Info', + ], + 203 => [ + 'callsign' => 'EDKB_I_TWR', + 'freq' => '135.15', + 'name' => 'Bonn-Hangelar Info', + ], + 204 => [ + 'callsign' => 'EDKF_I_TWR', + 'freq' => '123.65', + 'name' => 'Bergneustadt Info', + ], + 205 => [ + 'callsign' => 'EDKL_I_TWR', + 'freq' => '122.42', + 'name' => 'Leverkusen Info', + ], + 206 => [ + 'callsign' => 'EDKM_I_TWR', + 'freq' => '122.05', + 'name' => 'Meschede Info', + ], + 207 => [ + 'callsign' => 'EDLD_I_TWR', + 'freq' => '122.70', + 'name' => 'Dinslaken Info', + ], + 208 => [ + 'callsign' => 'EDLE_I_TWR', + 'freq' => '119.75', + 'name' => 'Essen/Mülheim Info', + ], + 209 => [ + 'callsign' => 'EDLI_I_TWR', + 'freq' => '118.35', + 'name' => 'Bielefeld Info', + ], + 210 => [ + 'callsign' => 'EDLM_I_TWR', + 'freq' => '122.00', + 'name' => 'Marl Info', + ], + 211 => [ + 'callsign' => 'EDLN_GND', + 'freq' => '121.92', + 'name' => 'Mönchengladbach Ground', + ], + 212 => [ + 'callsign' => 'EDLN_TWR', + 'freq' => '118.12', + 'name' => 'Mönchengladbach Tower', + ], + 213 => [ + 'callsign' => 'EDLO_I_TWR', + 'freq' => '122.17', + 'name' => 'Oerlinghausen Info', + ], + 214 => [ + 'callsign' => 'EDLP_APP', + 'freq' => '125.22', + 'name' => 'Langen Radar (PADL Paderborn Low)', + ], + 215 => [ + 'callsign' => 'EDLP_ATIS', + 'freq' => '125.72', + 'name' => 'Paderborn ATIS', + ], + 216 => [ + 'callsign' => 'EDLP_GND', + 'freq' => '121.92', + 'name' => 'Paderborn Ground', + ], + 217 => [ + 'callsign' => 'EDLP_TWR', + 'freq' => '133.37', + 'name' => 'Paderborn Tower', + ], + 218 => [ + 'callsign' => 'EDLT_I_TWR', + 'freq' => '122.85', + 'name' => 'Telgte Info', + ], + 219 => [ + 'callsign' => 'EDLV_ATIS', + 'freq' => '124.45', + 'name' => 'Niederrhein ATIS', + ], + 220 => [ + 'callsign' => 'EDLV_TWR', + 'freq' => '129.40', + 'name' => 'Niederrhein Tower', + ], + 221 => [ + 'callsign' => 'EDLW_ATIS', + 'freq' => '125.12', + 'name' => 'Dortmund ATIS', + ], + 222 => [ + 'callsign' => 'EDLW_GND', + 'freq' => '121.82', + 'name' => 'Dortmund Ground', + ], + 223 => [ + 'callsign' => 'EDLW_TWR', + 'freq' => '134.17', + 'name' => 'Dortmund Tower', + ], + 224 => [ + 'callsign' => 'EDMA_ATIS', + 'freq' => '124.57', + 'name' => 'Augsburg ATIS', + ], + 225 => [ + 'callsign' => 'EDMA_TWR', + 'freq' => '124.97', + 'name' => 'Augsburg Tower', + ], + 226 => [ + 'callsign' => 'EDMB_I_TWR', + 'freq' => '122.75', + 'name' => 'Biberach Info', + ], + 227 => [ + 'callsign' => 'EDME_ATIS', + 'freq' => '125.07', + 'name' => 'Eggenfelden ATIS', + ], + 228 => [ + 'callsign' => 'EDME_I_TWR', + 'freq' => '120.30', + 'name' => 'Eggenfelden Info', + ], + 229 => [ + 'callsign' => 'EDMJ_I_TWR', + 'freq' => '122.42', + 'name' => 'Jesenwang Info', + ], + 230 => [ + 'callsign' => 'EDML_I_TWR', + 'freq' => '129.80', + 'name' => 'Landshut Info', + ], + 231 => [ + 'callsign' => 'EDMM_A_CTR', + 'freq' => '129.10', + 'name' => 'München Radar (ALB Allersberg)', + ], + 232 => [ + 'callsign' => 'EDMM_C_CTR', + 'freq' => '133.67', + 'name' => 'München Radar (CHI Chiem)', + ], + 233 => [ + 'callsign' => 'EDMM_CTR', + 'freq' => '124.05', + 'name' => 'München Radar (Complete)', + ], + 234 => [ + 'callsign' => 'EDMM_E_CTR', + 'freq' => '129.55', + 'name' => 'München Radar (EGG Eggenfelden)', + ], + 235 => [ + 'callsign' => 'EDMM_F_CTR', + 'freq' => '124.82', + 'name' => 'München Radar (FRKH Franken High)', + ], + 236 => [ + 'callsign' => 'EDMM_I_CTR', + 'freq' => '120.65', + 'name' => 'München Information (FIS)', + ], + 237 => [ + 'callsign' => 'EDMM_K_CTR', + 'freq' => '134.15', + 'name' => 'München Radar (KPT Kempten)', + ], + 238 => [ + 'callsign' => 'EDMM_N_CTR', + 'freq' => '126.45', + 'name' => 'München Radar (NDG Nördlingen)', + ], + 239 => [ + 'callsign' => 'EDMM_R_CTR', + 'freq' => '132.55', + 'name' => 'München Radar (RDG Roding)', + ], + 240 => [ + 'callsign' => 'EDMM_S_CTR', + 'freq' => '131.02', + 'name' => 'München Radar (SASH Sachsen High)', + ], + 241 => [ + 'callsign' => 'EDMM_T_CTR', + 'freq' => '133.57', + 'name' => 'München Radar (TRGH Thüringen High)', + ], + 242 => [ + 'callsign' => 'EDMO_TWR', + 'freq' => '119.55', + 'name' => 'Oberpfaffenhofen Tower', + ], + 243 => [ + 'callsign' => 'EDMS_ATIS', + 'freq' => '135.52', + 'name' => 'Straubing ATIS', + ], + 244 => [ + 'callsign' => 'EDMS_I_TWR', + 'freq' => '127.15', + 'name' => 'Straubing Info', + ], + 245 => [ + 'callsign' => 'EDMT_I_TWR', + 'freq' => '122.82', + 'name' => 'Tannheim Info', + ], + 246 => [ + 'callsign' => 'EDMV_I_TWR', + 'freq' => '119.17', + 'name' => 'Vilshofen Info', + ], + 247 => [ + 'callsign' => 'EDNC_I_TWR', + 'freq' => '118.35', + 'name' => 'Beilngries Info', + ], + 248 => [ + 'callsign' => 'EDNL_I_TWR', + 'freq' => '122.87', + 'name' => 'Leutkirch Info', + ], + 249 => [ + 'callsign' => 'EDNX_I_TWR', + 'freq' => '129.40', + 'name' => 'Schleissheim Info', + ], + 250 => [ + 'callsign' => 'EDNY_ATIS', + 'freq' => '129.60', + 'name' => 'Friedrichshafen ATIS', + ], + 251 => [ + 'callsign' => 'EDNY_TWR', + 'freq' => '120.07', + 'name' => 'Friedrichshafen Tower', + ], + 252 => [ + 'callsign' => 'EDOP_TWR', + 'freq' => '128.90', + 'name' => 'Parchim Tower', + ], + 253 => [ + 'callsign' => 'EDPA_I_TWR', + 'freq' => '121.40', + 'name' => 'Aalen Info', + ], + 254 => [ + 'callsign' => 'EDPH_I_TWR', + 'freq' => '135.42', + 'name' => 'Schwabach Info', + ], + 255 => [ + 'callsign' => 'EDPQ_I_TWR', + 'freq' => '123.00', + 'name' => 'Schmidgaden Info', + ], + 256 => [ + 'callsign' => 'EDPU_I_TWR', + 'freq' => '122.20', + 'name' => 'Bartholomä Segelflug', + ], + 257 => [ + 'callsign' => 'EDPY_I_TWR', + 'freq' => '122.00', + 'name' => 'Ellwangen Info', + ], + 258 => [ + 'callsign' => 'EDQC_ATIS', + 'freq' => '120.10', + 'name' => 'Coburg ATIS', + ], + 259 => [ + 'callsign' => 'EDQC_I_TWR', + 'freq' => '128.67', + 'name' => 'Coburg Info', + ], + 260 => [ + 'callsign' => 'EDQD_I_TWR', + 'freq' => '127.52', + 'name' => 'Bayreuth Info', + ], + 261 => [ + 'callsign' => 'EDQE_I_TWR', + 'freq' => '130.77', + 'name' => 'Feuerstein Info', + ], + 262 => [ + 'callsign' => 'EDQH_I_TWR', + 'freq' => '122.85', + 'name' => 'Herzogenaurach Info', + ], + 263 => [ + 'callsign' => 'EDQK_I_TWR', + 'freq' => '118.52', + 'name' => 'Kulmbach Info', + ], + 264 => [ + 'callsign' => 'EDQM_TWR', + 'freq' => '124.35', + 'name' => 'Hof Tower', + ], + 265 => [ + 'callsign' => 'EDQN_I_TWR', + 'freq' => '118.92', + 'name' => 'Neustadt/Aisch Info', + ], + 266 => [ + 'callsign' => 'EDQP_I_TWR', + 'freq' => '127.45', + 'name' => 'Rosenthal Info', + ], + 267 => [ + 'callsign' => 'EDQT_I_TWR', + 'freq' => '119.80', + 'name' => 'Hassfurt Info', + ], + 268 => [ + 'callsign' => 'EDQW_I_TWR', + 'freq' => '120.25', + 'name' => 'Weiden Info', + ], + 269 => [ + 'callsign' => 'EDQY_I_TWR', + 'freq' => '129.80', + 'name' => 'Steinrücken Info', + ], + 270 => [ + 'callsign' => 'EDRA_I_TWR', + 'freq' => '122.35', + 'name' => 'Neuenahr Info', + ], + 271 => [ + 'callsign' => 'EDRF_I_TWR', + 'freq' => '122.40', + 'name' => 'Dürkheim Info', + ], + 272 => [ + 'callsign' => 'EDRJ_I_TWR', + 'freq' => '122.60', + 'name' => 'Saarlouis Info', + ], + 273 => [ + 'callsign' => 'EDRK_I_TWR', + 'freq' => '122.65', + 'name' => 'Koblenz Info', + ], + 274 => [ + 'callsign' => 'EDRM_I_TWR', + 'freq' => '123.00', + 'name' => 'Traben-Trarbach Info', + ], + 275 => [ + 'callsign' => 'EDRY_I_TWR', + 'freq' => '118.07', + 'name' => 'Speyer Info', + ], + 276 => [ + 'callsign' => 'EDRZ_TWR', + 'freq' => '123.82', + 'name' => 'Zweibrücken Tower (Info when CTR not active)', + ], + 277 => [ + 'callsign' => 'EDSB_ATIS', + 'freq' => '121.27', + 'name' => 'Baden ATIS', + ], + 278 => [ + 'callsign' => 'EDSB_GND', + 'freq' => '121.82', + 'name' => 'Baden Ground', + ], + 279 => [ + 'callsign' => 'EDSB_TWR', + 'freq' => '134.10', + 'name' => 'Baden Tower', + ], + 280 => [ + 'callsign' => 'EDSH_I_TWR', + 'freq' => '126.50', + 'name' => 'Backnang Info', + ], + 281 => [ + 'callsign' => 'EDTC_I_TWR', + 'freq' => '128.37', + 'name' => 'Bruchsal Info', + ], + 282 => [ + 'callsign' => 'EDTD_I_TWR', + 'freq' => '124.25', + 'name' => 'Donaueschingen Info', + ], + 283 => [ + 'callsign' => 'EDTF_I_TWR', + 'freq' => '118.25', + 'name' => 'Freiburg Info', + ], + 284 => [ + 'callsign' => 'EDTH_I_TWR', + 'freq' => '123.02', + 'name' => 'Heubach Info', + ], + 285 => [ + 'callsign' => 'EDTL_TWR', + 'freq' => '125.17', + 'name' => 'Lahr Tower (Info when CTR not active)', + ], + 286 => [ + 'callsign' => 'EDTM_I_TWR', + 'freq' => '135.17', + 'name' => 'Mengen Info', + ], + 287 => [ + 'callsign' => 'EDTO_I_TWR', + 'freq' => '119.75', + 'name' => 'Offenburg Info', + ], + 288 => [ + 'callsign' => 'EDTQ_I_TWR', + 'freq' => '123.65', + 'name' => 'Pattonville Info', + ], + 289 => [ + 'callsign' => 'EDTS_I_TWR', + 'freq' => '122.85', + 'name' => 'Schwenningen Info', + ], + 290 => [ + 'callsign' => 'EDTU_I_TWR', + 'freq' => '123.60', + 'name' => 'Saulgau Info', + ], + 291 => [ + 'callsign' => 'EDTW_I_TWR', + 'freq' => '123.65', + 'name' => 'Winzeln Info', + ], + 292 => [ + 'callsign' => 'EDTY_ATIS', + 'freq' => '133.87', + 'name' => 'Schwäbisch Hall ATIS', + ], + 293 => [ + 'callsign' => 'EDTY_I_TWR', + 'freq' => '129.22', + 'name' => 'Schwäbisch Hall Info', + ], + 294 => [ + 'callsign' => 'EDTZ_I_TWR', + 'freq' => '124.35', + 'name' => 'Konstanz Info', + ], + 295 => [ + 'callsign' => 'EDUB_I_TWR', + 'freq' => '130.12', + 'name' => 'Briest Info', + ], + 296 => [ + 'callsign' => 'EDUU_CTR', + 'freq' => '132.32', + 'name' => 'Rhein Radar (Complete)', + ], + 297 => [ + 'callsign' => 'EDUU_D_CTR', + 'freq' => '132.72', + 'name' => 'Rhein Radar (DON Donau)', + ], + 298 => [ + 'callsign' => 'EDUU_E_CTR', + 'freq' => '128.07', + 'name' => 'Rhein Radar (East)', + ], + 299 => [ + 'callsign' => 'EDUU_L_CTR', + 'freq' => '127.30', + 'name' => 'Rhein Radar (ALP Alpen)', + ], + 300 => [ + 'callsign' => 'EDUU_N_CTR', + 'freq' => '132.77', + 'name' => 'Rhein Radar (NTM Nattenheim)', + ], + 301 => [ + 'callsign' => 'EDUU_S_CTR', + 'freq' => '128.97', + 'name' => 'Rhein Radar (South)', + ], + 302 => [ + 'callsign' => 'EDUU_T_CTR', + 'freq' => '132.40', + 'name' => 'Rhein Radar (TGO Tango)', + ], + 303 => [ + 'callsign' => 'EDUU_W_CTR', + 'freq' => '133.65', + 'name' => 'Rhein Radar (West)', + ], + 304 => [ + 'callsign' => 'EDUW_I_TWR', + 'freq' => '130.12', + 'name' => 'Tutow Info', + ], + 305 => [ + 'callsign' => 'EDVC_I_TWR', + 'freq' => '123.65', + 'name' => 'Arloh Info (Celle)', + ], + 306 => [ + 'callsign' => 'EDVE_ATIS', + 'freq' => '134.45', + 'name' => 'Braunschweig ATIS', + ], + 307 => [ + 'callsign' => 'EDVE_TWR', + 'freq' => '120.05', + 'name' => 'Braunschweig Tower', + ], + 308 => [ + 'callsign' => 'EDVI_I_TWR', + 'freq' => '130.27', + 'name' => 'Höxter Info', + ], + 309 => [ + 'callsign' => 'EDVK_ATIS', + 'freq' => '129.20', + 'name' => 'Kassel ATIS', + ], + 310 => [ + 'callsign' => 'EDVK_GND', + 'freq' => '121.90', + 'name' => 'Kassel Ground', + ], + 311 => [ + 'callsign' => 'EDVK_TWR', + 'freq' => '118.10', + 'name' => 'Kassel Tower', + ], + 312 => [ + 'callsign' => 'EDVY_I_TWR', + 'freq' => '122.37', + 'name' => 'Porta Westfalica Info', + ], + 313 => [ + 'callsign' => 'EDWB_I_TWR', + 'freq' => '129.05', + 'name' => 'Bremerhaven Info', + ], + 314 => [ + 'callsign' => 'EDWE_I_TWR', + 'freq' => '118.60', + 'name' => 'Emden Info', + ], + 315 => [ + 'callsign' => 'EDWG_I_TWR', + 'freq' => '122.40', + 'name' => 'Wooge Info', + ], + 316 => [ + 'callsign' => 'EDWI_ATIS', + 'freq' => '124.32', + 'name' => 'Wilhelmshaven ATIS', + ], + 317 => [ + 'callsign' => 'EDWI_I_TWR', + 'freq' => '129.25', + 'name' => 'Wilhelmshaven Info', + ], + 318 => [ + 'callsign' => 'EDWJ_I_TWR', + 'freq' => '120.50', + 'name' => 'Juist Info', + ], + 319 => [ + 'callsign' => 'EDWL_I_TWR', + 'freq' => '122.02', + 'name' => 'Langeoog Info', + ], + 320 => [ + 'callsign' => 'EDWN_I_TWR', + 'freq' => '122.65', + 'name' => 'Nordhorn Info', + ], + 321 => [ + 'callsign' => 'EDWR_I_TWR', + 'freq' => '123.00', + 'name' => 'Borkum Info', + ], + 322 => [ + 'callsign' => 'EDWS_I_TWR', + 'freq' => '120.50', + 'name' => 'Norddeich Info', + ], + 323 => [ + 'callsign' => 'EDWW_A_CTR', + 'freq' => '123.92', + 'name' => 'Bremen Radar (ALEH Aller East High)', + ], + 324 => [ + 'callsign' => 'EDWW_B_CTR', + 'freq' => '123.22', + 'name' => 'Bremen Radar (BOR Börde)', + ], + 325 => [ + 'callsign' => 'EDWW_CTR', + 'freq' => '125.02', + 'name' => 'Bremen Radar (Complete)', + ], + 326 => [ + 'callsign' => 'EDWW_D_CTR', + 'freq' => '128.75', + 'name' => 'Bremen Radar (DST Deister)', + ], + 327 => [ + 'callsign' => 'EDWW_E_CTR', + 'freq' => '120.22', + 'name' => 'Bremen Radar (EID Eider)', + ], + 328 => [ + 'callsign' => 'EDWW_I_CTR', + 'freq' => '119.82', + 'name' => 'Bremen Information (FIS)', + ], + 329 => [ + 'callsign' => 'EDWW_M_CTR', + 'freq' => '124.17', + 'name' => 'Bremen Radar (MRZ Müritz)', + ], + 330 => [ + 'callsign' => 'EDWY_I_TWR', + 'freq' => '122.60', + 'name' => 'Norderney Info', + ], + 331 => [ + 'callsign' => 'EDXF_I_TWR', + 'freq' => '122.85', + 'name' => 'Flensburg Info', + ], + 332 => [ + 'callsign' => 'EDXH_I_TWR', + 'freq' => '122.45', + 'name' => 'Helgoland Info', + ], + 333 => [ + 'callsign' => 'EDXO_I_TWR', + 'freq' => '129.77', + 'name' => 'St. Peter Info', + ], + 334 => [ + 'callsign' => 'EDXP_I_TWR', + 'freq' => '122.40', + 'name' => 'Harle Info', + ], + 335 => [ + 'callsign' => 'EDXW_ATIS', + 'freq' => '118.42', + 'name' => 'Sylt ATIS', + ], + 336 => [ + 'callsign' => 'EDXW_TWR', + 'freq' => '119.75', + 'name' => 'Sylt Tower', + ], + 337 => [ + 'callsign' => 'EDYY_C_CTR', + 'freq' => '133.95', + 'name' => 'Maastricht Radar (CE Celle)', + ], + 338 => [ + 'callsign' => 'EDYY_H_CTR', + 'freq' => '120.95', + 'name' => 'Maastricht Radar (HO Holstein)', + ], + 339 => [ + 'callsign' => 'EDYY_J_CTR', + 'freq' => '134.70', + 'name' => 'Maastricht Radar (JE Jever)', + ], + 340 => [ + 'callsign' => 'EDYY_M_CTR', + 'freq' => '133.85', + 'name' => 'Maastricht Radar (MN Münster)', + ], + 341 => [ + 'callsign' => 'EDYY_O_CTR', + 'freq' => '132.85', + 'name' => 'Maastricht Radar (Olno - high traffic loads)', + ], + 342 => [ + 'callsign' => 'EDYY_R_CTR', + 'freq' => '132.62', + 'name' => 'Maastricht Radar (RH Ruhr)', + ], + 343 => [ + 'callsign' => 'EDYY_S_CTR', + 'freq' => '131.37', + 'name' => 'Maastricht Radar (SO Solling)', + ], + 344 => [ + 'callsign' => 'ETAR_GND', + 'freq' => '121.77', + 'name' => 'Ramstein Ground', + ], + 345 => [ + 'callsign' => 'ETAR_TWR', + 'freq' => '123.55', + 'name' => 'Ramstein Tower', + ], + 346 => [ + 'callsign' => 'ETHN_APP', + 'freq' => '123.30', + 'name' => 'Stetten Radar', + ], + 347 => [ + 'callsign' => 'ETHN_I_TWR', + 'freq' => '119.77', + 'name' => 'Stetten Info (when CTR is not active)', + ], + 348 => [ + 'callsign' => 'ETHN_TWR', + 'freq' => '122.10', + 'name' => 'Stetten Tower', + ], + 349 => [ + 'callsign' => 'ETHR_TWR', + 'freq' => '122.10', + 'name' => 'Roth Tower', + ], + 350 => [ + 'callsign' => 'ETIC_TWR', + 'freq' => '122.10', + 'name' => 'Grafenwöhr Tower', + ], + 351 => [ + 'callsign' => 'ETNG_APP', + 'freq' => '123.72', + 'name' => 'Frisbee Radar (Geilenkirchen Arrival)', + ], + 352 => [ + 'callsign' => 'ETNG_TWR', + 'freq' => '120.05', + 'name' => 'Frisbee Tower', + ], + 353 => [ + 'callsign' => 'ETNH_APP', + 'freq' => '123.30', + 'name' => 'Hohn Radar', + ], + 354 => [ + 'callsign' => 'ETNH_F_APP', + 'freq' => '125.60', + 'name' => 'Hohn Precision', + ], + 355 => [ + 'callsign' => 'ETNH_TWR', + 'freq' => '122.10', + 'name' => 'Hohn Tower', + ], + 356 => [ + 'callsign' => 'ETNL_APP', + 'freq' => '133.62', + 'name' => 'Laage Radar', + ], + 357 => [ + 'callsign' => 'ETNL_TWR', + 'freq' => '118.42', + 'name' => 'Laage Tower', + ], + 358 => [ + 'callsign' => 'ETNN_APP', + 'freq' => '123.30', + 'name' => 'Noervenich Radar', + ], + 359 => [ + 'callsign' => 'ETNN_TWR', + 'freq' => '122.10', + 'name' => 'Noervenich Tower', + ], + 360 => [ + 'callsign' => 'ETNT_APP', + 'freq' => '123.60', + 'name' => 'Wittmund Radar', + ], + 361 => [ + 'callsign' => 'ETNT_TWR', + 'freq' => '118.72', + 'name' => 'Wittmund Tower', + ], + 362 => [ + 'callsign' => 'ETNW_TWR', + 'freq' => '118.05', + 'name' => 'Wunstorf Tower', + ], + 363 => [ + 'callsign' => 'ETSA_APP', + 'freq' => '130.50', + 'name' => 'Landsberg Radar', + ], + 364 => [ + 'callsign' => 'ETSA_TWR', + 'freq' => '122.10', + 'name' => 'Landsberg Tower', + ], + 365 => [ + 'callsign' => 'ETSI_APP', + 'freq' => '120.60', + 'name' => 'Ingo Radar', + ], + 366 => [ + 'callsign' => 'ETSI_TWR', + 'freq' => '125.25', + 'name' => 'Ingo Tower', + ], + ]; + + /** + * Run the database seeds. + * + * @return void + */ + public function run() + { + DB::table('navigation_stations')->truncate(); + $this->command->getOutput()->writeln('Truncated stations table.'); + + $this->command->getOutput()->writeln('Starting seeding of new information...'); + $this->command->getOutput()->progressStart(count($this->stations)); + foreach ($this->stations as $s) { + $ns = new \App\Models\Navigation\Station(); + $ns->name = $s['name']; + $ns->ident = $s['callsign']; + $ns->frequency = $s['freq']; + $ns->description = ''; + $ns->bookable = \Illuminate\Support\Str::endsWith($s['callsign'], 'ATIS') ? false : true; + $ns->atis = \Illuminate\Support\Str::endsWith($s['callsign'], 'ATIS') ? true : false; + $ns->save(); + $this->command->getOutput()->progressAdvance(); + } + + $this->command->getOutput()->progressFinish(); + $this->command->getOutput()->writeln('Finished seeding.'); + + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ea3b9ed --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "cross-env NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch": "npm run development -- --watch", + "watch-poll": "npm run watch -- --watch-poll", + "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js", + "prod": "npm run production", + "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + }, + "devDependencies": { + "axios": "^0.21.1", + "bootstrap": "^4.5.2", + "cross-env": "^7.0.3", + "jquery": "^3.6.0", + "laravel-mix": "^5.0.9", + "lodash": "^4.17.19", + "popper.js": "^1.12", + "resolve-url-loader": "^2.3.1", + "sass": "^1.30.0", + "sass-loader": "^8.0.0", + "vue": "^2.6.12", + "vue-template-compiler": "^2.6.12", + "vuedraggable": "^2.24.0" + }, + "dependencies": { + "@trevoreyre/autocomplete-vue": "^2.2.0", + "admin-lte": "3.0.5", + "datatables.net": "^1.10.24", + "datatables.net-bs4": "^1.10.24", + "laravel-echo": "^1.10.0", + "moment": "^2.28.0", + "socket.io-client": "^2.3.0", + "vue-async-computed": "^3.8.2", + "vue-moment": "^4.1.0", + "vue-router": "^3.4.9", + "vue2-datepicker": "^2.13.0" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..964ff0c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + ./tests/Unit + + + ./tests/Feature + + + + + ./app + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..3aec5e2 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,21 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/css/.gitignore b/public/css/.gitignore new file mode 100644 index 0000000..8535805 --- /dev/null +++ b/public/css/.gitignore @@ -0,0 +1,2 @@ +app.css +vendor/* diff --git a/public/css/webfonts/fa-brands-400.eot b/public/css/webfonts/fa-brands-400.eot new file mode 100644 index 0000000..e79f40f Binary files /dev/null and b/public/css/webfonts/fa-brands-400.eot differ diff --git a/public/css/webfonts/fa-brands-400.svg b/public/css/webfonts/fa-brands-400.svg new file mode 100644 index 0000000..ba0d850 --- /dev/null +++ b/public/css/webfonts/fa-brands-400.svg @@ -0,0 +1,3442 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/css/webfonts/fa-brands-400.ttf b/public/css/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..217ffe9 Binary files /dev/null and b/public/css/webfonts/fa-brands-400.ttf differ diff --git a/public/css/webfonts/fa-brands-400.woff b/public/css/webfonts/fa-brands-400.woff new file mode 100644 index 0000000..a2d8025 Binary files /dev/null and b/public/css/webfonts/fa-brands-400.woff differ diff --git a/public/css/webfonts/fa-brands-400.woff2 b/public/css/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..e27b0bf Binary files /dev/null and b/public/css/webfonts/fa-brands-400.woff2 differ diff --git a/public/css/webfonts/fa-regular-400.eot b/public/css/webfonts/fa-regular-400.eot new file mode 100644 index 0000000..d62be2f Binary files /dev/null and b/public/css/webfonts/fa-regular-400.eot differ diff --git a/public/css/webfonts/fa-regular-400.svg b/public/css/webfonts/fa-regular-400.svg new file mode 100644 index 0000000..751083e --- /dev/null +++ b/public/css/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/css/webfonts/fa-regular-400.ttf b/public/css/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..eb3cb5e Binary files /dev/null and b/public/css/webfonts/fa-regular-400.ttf differ diff --git a/public/css/webfonts/fa-regular-400.woff b/public/css/webfonts/fa-regular-400.woff new file mode 100644 index 0000000..43b1a9a Binary files /dev/null and b/public/css/webfonts/fa-regular-400.woff differ diff --git a/public/css/webfonts/fa-regular-400.woff2 b/public/css/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..b9344a7 Binary files /dev/null and b/public/css/webfonts/fa-regular-400.woff2 differ diff --git a/public/css/webfonts/fa-solid-900.eot b/public/css/webfonts/fa-solid-900.eot new file mode 100644 index 0000000..c77baa8 Binary files /dev/null and b/public/css/webfonts/fa-solid-900.eot differ diff --git a/public/css/webfonts/fa-solid-900.svg b/public/css/webfonts/fa-solid-900.svg new file mode 100644 index 0000000..627128b --- /dev/null +++ b/public/css/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/css/webfonts/fa-solid-900.ttf b/public/css/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..c6c3dd4 Binary files /dev/null and b/public/css/webfonts/fa-solid-900.ttf differ diff --git a/public/css/webfonts/fa-solid-900.woff b/public/css/webfonts/fa-solid-900.woff new file mode 100644 index 0000000..77c1786 Binary files /dev/null and b/public/css/webfonts/fa-solid-900.woff differ diff --git a/public/css/webfonts/fa-solid-900.woff2 b/public/css/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..e30fb67 Binary files /dev/null and b/public/css/webfonts/fa-solid-900.woff2 differ diff --git a/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHg6babWk.woff2 b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHg6babWk.woff2 new file mode 100644 index 0000000..bb4bebd Binary files /dev/null and b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHg6babWk.woff2 differ diff --git a/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgKbabWk.woff2 b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgKbabWk.woff2 new file mode 100644 index 0000000..a1fa5b4 Binary files /dev/null and b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgKbabWk.woff2 differ diff --git a/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgqbabWk.woff2 b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgqbabWk.woff2 new file mode 100644 index 0000000..8f6a8ff Binary files /dev/null and b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgqbabWk.woff2 differ diff --git a/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHiababWk.woff2 b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHiababWk.woff2 new file mode 100644 index 0000000..0706313 Binary files /dev/null and b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHiababWk.woff2 differ diff --git a/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHjaba.woff2 b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHjaba.woff2 new file mode 100644 index 0000000..0017722 Binary files /dev/null and b/public/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHjaba.woff2 differ diff --git a/public/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2 b/public/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2 new file mode 100644 index 0000000..35aa202 Binary files /dev/null and b/public/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2 differ diff --git a/public/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2 b/public/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2 new file mode 100644 index 0000000..a74a303 Binary files /dev/null and b/public/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2 differ diff --git a/public/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2 b/public/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2 new file mode 100644 index 0000000..de24d94 Binary files /dev/null and b/public/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2 differ diff --git a/public/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2 b/public/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2 new file mode 100644 index 0000000..64f87b1 Binary files /dev/null and b/public/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2 differ diff --git a/public/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2 b/public/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2 new file mode 100644 index 0000000..738b789 Binary files /dev/null and b/public/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2 differ diff --git a/public/css/webfonts/nunito/nunito.css b/public/css/webfonts/nunito/nunito.css new file mode 100644 index 0000000..7eed79f --- /dev/null +++ b/public/css/webfonts/nunito/nunito.css @@ -0,0 +1,160 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Nunito'; + font-style: italic; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgKbabWk.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Nunito'; + font-style: italic; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHiababWk.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito'; + font-style: italic; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHgqbabWk.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito'; + font-style: italic; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHg6babWk.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito'; + font-style: italic; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXK3I6Li01BKofIMPyPbj8d7IEAGXNirXAHjaba.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 300; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 300; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 300; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 300; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 300; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url(/css/webfonts/nunito/XRXV3I6Li01BKofINeaB.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/images/airport.png b/public/images/airport.png new file mode 100644 index 0000000..3f5065b Binary files /dev/null and b/public/images/airport.png differ diff --git a/public/images/arrow.svg b/public/images/arrow.svg new file mode 100644 index 0000000..f2543c5 --- /dev/null +++ b/public/images/arrow.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/dashboard.jpg b/public/images/dashboard.jpg new file mode 100644 index 0000000..09d6fa2 Binary files /dev/null and b/public/images/dashboard.jpg differ diff --git a/public/images/encyclopedia.jpg b/public/images/encyclopedia.jpg new file mode 100644 index 0000000..80be6b9 Binary files /dev/null and b/public/images/encyclopedia.jpg differ diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100644 index 0000000..b448103 Binary files /dev/null and b/public/images/favicon.ico differ diff --git a/public/images/germany.svg b/public/images/germany.svg new file mode 100644 index 0000000..0f88aa7 --- /dev/null +++ b/public/images/germany.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/overlay.png b/public/images/overlay.png new file mode 100644 index 0000000..ec3b66f Binary files /dev/null and b/public/images/overlay.png differ diff --git a/public/images/radar.png b/public/images/radar.png new file mode 100644 index 0000000..38c2c3b Binary files /dev/null and b/public/images/radar.png differ diff --git a/public/images/splash/arrival.jpg b/public/images/splash/arrival.jpg new file mode 100644 index 0000000..9025e19 Binary files /dev/null and b/public/images/splash/arrival.jpg differ diff --git a/public/images/splash/bus.jpg b/public/images/splash/bus.jpg new file mode 100644 index 0000000..2b5feb1 Binary files /dev/null and b/public/images/splash/bus.jpg differ diff --git a/public/images/splash/dus.jpg b/public/images/splash/dus.jpg new file mode 100644 index 0000000..37f8c46 Binary files /dev/null and b/public/images/splash/dus.jpg differ diff --git a/public/images/splash/instruments.jpg b/public/images/splash/instruments.jpg new file mode 100644 index 0000000..2eed536 Binary files /dev/null and b/public/images/splash/instruments.jpg differ diff --git a/public/images/splash/muc.jpg b/public/images/splash/muc.jpg new file mode 100644 index 0000000..50bfe86 Binary files /dev/null and b/public/images/splash/muc.jpg differ diff --git a/public/images/splash/overhead.jpg b/public/images/splash/overhead.jpg new file mode 100644 index 0000000..ffd16da Binary files /dev/null and b/public/images/splash/overhead.jpg differ diff --git a/public/images/splash/wing.jpg b/public/images/splash/wing.jpg new file mode 100644 index 0000000..0c02149 Binary files /dev/null and b/public/images/splash/wing.jpg differ diff --git a/public/images/stats.jpg b/public/images/stats.jpg new file mode 100644 index 0000000..28ed46a Binary files /dev/null and b/public/images/stats.jpg differ diff --git a/public/images/united-kingdom.svg b/public/images/united-kingdom.svg new file mode 100644 index 0000000..578a4d5 --- /dev/null +++ b/public/images/united-kingdom.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/vacc_bre.png b/public/images/vacc_bre.png new file mode 100644 index 0000000..a1490d4 Binary files /dev/null and b/public/images/vacc_bre.png differ diff --git a/public/images/vacc_logo.png b/public/images/vacc_logo.png new file mode 100644 index 0000000..dd42b42 Binary files /dev/null and b/public/images/vacc_logo.png differ diff --git a/public/images/vacc_logo_ts.png b/public/images/vacc_logo_ts.png new file mode 100644 index 0000000..74884bd Binary files /dev/null and b/public/images/vacc_logo_ts.png differ diff --git a/public/images/vacc_logo_white.png b/public/images/vacc_logo_white.png new file mode 100644 index 0000000..f4a3a0b Binary files /dev/null and b/public/images/vacc_logo_white.png differ diff --git a/public/images/vacc_logo_white_small.png b/public/images/vacc_logo_white_small.png new file mode 100644 index 0000000..088e9ea Binary files /dev/null and b/public/images/vacc_logo_white_small.png differ diff --git a/public/images/vateud.png b/public/images/vateud.png new file mode 100644 index 0000000..6095996 Binary files /dev/null and b/public/images/vateud.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Black_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Black_1000px.png new file mode 100644 index 0000000..b70adec Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Black_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Black_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Black_No_Tagline_1000px.png new file mode 100644 index 0000000..f91cae4 Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Black_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Blue_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Blue_No_Tagline_1000px.png new file mode 100644 index 0000000..9ede157 Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Blue_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Green_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Green_No_Tagline_1000px.png new file mode 100644 index 0000000..8f7e1be Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Green_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Indigo_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Indigo_No_Tagline_1000px.png new file mode 100644 index 0000000..3e75c68 Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Indigo_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_No_Tagline_1000px.png new file mode 100644 index 0000000..7529065 Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Official_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Official_1000px.png new file mode 100644 index 0000000..c5c68fc Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Official_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_Official_White_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_Official_White_Tagline_1000px.png new file mode 100644 index 0000000..7ef7f1a Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_Official_White_Tagline_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_White_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_White_1000px.png new file mode 100644 index 0000000..8b74b5b Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_White_1000px.png differ diff --git a/public/images/vatsim/1000w/VATSIM_Logo_White_No_Tagline_1000px.png b/public/images/vatsim/1000w/VATSIM_Logo_White_No_Tagline_1000px.png new file mode 100644 index 0000000..eb57316 Binary files /dev/null and b/public/images/vatsim/1000w/VATSIM_Logo_White_No_Tagline_1000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Black_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Black_2000px.png new file mode 100644 index 0000000..a2d8a6e Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Black_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Black_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Black_No_Tagline_2000px.png new file mode 100644 index 0000000..40b6f69 Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Black_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Blue_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Blue_No_Tagline_2000px.png new file mode 100644 index 0000000..2bb2d02 Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Blue_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Green_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Green_No_Tagline_2000px.png new file mode 100644 index 0000000..b1bc457 Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Green_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Indigo_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Indigo_No_Tagline_2000px.png new file mode 100644 index 0000000..bd5758e Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Indigo_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_No_Tagline_2000px.png new file mode 100644 index 0000000..62a4c9d Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Official_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Official_2000px.png new file mode 100644 index 0000000..a0f25ee Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Official_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_Official_White_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_Official_White_Tagline_2000px.png new file mode 100644 index 0000000..448e2cf Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_Official_White_Tagline_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_White_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_White_2000px.png new file mode 100644 index 0000000..12d2f05 Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_White_2000px.png differ diff --git a/public/images/vatsim/2000w/VATSIM_Logo_White_No_Tagline_2000px.png b/public/images/vatsim/2000w/VATSIM_Logo_White_No_Tagline_2000px.png new file mode 100644 index 0000000..6e041ce Binary files /dev/null and b/public/images/vatsim/2000w/VATSIM_Logo_White_No_Tagline_2000px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Black_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Black_500px.png new file mode 100644 index 0000000..78d72b7 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Black_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Black_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Black_No_Tagline_500px.png new file mode 100644 index 0000000..436ccdf Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Black_No_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Blue_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Blue_No_Tagline_500px.png new file mode 100644 index 0000000..0a1ddc9 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Blue_No_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Green_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Green_No_Tagline_500px.png new file mode 100644 index 0000000..f43a84a Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Green_No_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Indigo_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Indigo_No_Tagline_500px.png new file mode 100644 index 0000000..ba52e7d Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Indigo_No_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_No_Tagline_500px.png new file mode 100644 index 0000000..e1da740 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_No_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Official_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Official_500px.png new file mode 100644 index 0000000..c3d65e3 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Official_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_Official_White_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_Official_White_Tagline_500px.png new file mode 100644 index 0000000..ded64ad Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_Official_White_Tagline_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_White_500px.png b/public/images/vatsim/500w/VATSIM_Logo_White_500px.png new file mode 100644 index 0000000..2b0baa0 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_White_500px.png differ diff --git a/public/images/vatsim/500w/VATSIM_Logo_White_No_Tagline_500px.png b/public/images/vatsim/500w/VATSIM_Logo_White_No_Tagline_500px.png new file mode 100644 index 0000000..6bfca02 Binary files /dev/null and b/public/images/vatsim/500w/VATSIM_Logo_White_No_Tagline_500px.png differ diff --git a/public/images/vatsim/VATSIM-social_icon.eps b/public/images/vatsim/VATSIM-social_icon.eps new file mode 100644 index 0000000..135c6a2 Binary files /dev/null and b/public/images/vatsim/VATSIM-social_icon.eps differ diff --git a/public/images/vatsim/VATSIM_Brand_Guidelines_v1.0.pdf b/public/images/vatsim/VATSIM_Brand_Guidelines_v1.0.pdf new file mode 100644 index 0000000..53bf807 Binary files /dev/null and b/public/images/vatsim/VATSIM_Brand_Guidelines_v1.0.pdf differ diff --git a/public/images/vatsim_0.png b/public/images/vatsim_0.png new file mode 100644 index 0000000..cdde133 Binary files /dev/null and b/public/images/vatsim_0.png differ diff --git a/public/images/vatsim_0_new.png b/public/images/vatsim_0_new.png new file mode 100644 index 0000000..25dead1 Binary files /dev/null and b/public/images/vatsim_0_new.png differ diff --git a/public/images/wing.jpg b/public/images/wing.jpg new file mode 100644 index 0000000..0c02149 Binary files /dev/null and b/public/images/wing.jpg differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..4584cbc --- /dev/null +++ b/public/index.php @@ -0,0 +1,60 @@ + + */ + +define('LARAVEL_START', microtime(true)); + +/* +|-------------------------------------------------------------------------- +| Register The Auto Loader +|-------------------------------------------------------------------------- +| +| Composer provides a convenient, automatically generated class loader for +| our application. We just need to utilize it! We'll simply require it +| into the script here so that we don't have to worry about manual +| loading any of our classes later on. It feels great to relax. +| +*/ + +require __DIR__.'/../vendor/autoload.php'; + +/* +|-------------------------------------------------------------------------- +| Turn On The Lights +|-------------------------------------------------------------------------- +| +| We need to illuminate PHP development, so let us turn on the lights. +| This bootstraps the framework and gets it ready for use, then it +| will load up this application so that we can run it and send +| the responses back to the browser and delight our users. +| +*/ + +$app = require_once __DIR__.'/../bootstrap/app.php'; + +/* +|-------------------------------------------------------------------------- +| Run The Application +|-------------------------------------------------------------------------- +| +| Once we have the application, we can handle the incoming request +| through the kernel, and send the associated response back to +| the client's browser allowing them to enjoy the creative +| and wonderful application we have prepared for them. +| +*/ + +$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); + +$response = $kernel->handle( + $request = Illuminate\Http\Request::capture() +); + +$response->send(); + +$kernel->terminate($request, $response); diff --git a/public/js/.gitignore b/public/js/.gitignore new file mode 100644 index 0000000..f9602e9 --- /dev/null +++ b/public/js/.gitignore @@ -0,0 +1 @@ +app.js \ No newline at end of file diff --git a/public/js/vendor/breakpoints.min.js b/public/js/vendor/breakpoints.min.js new file mode 100644 index 0000000..e20ae89 --- /dev/null +++ b/public/js/vendor/breakpoints.min.js @@ -0,0 +1,2 @@ +/* breakpoints.js v1.0 | @ajlkn | MIT licensed */ +var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser}); diff --git a/public/js/vendor/jquery.dropotron.min.js b/public/js/vendor/jquery.dropotron.min.js new file mode 100644 index 0000000..7b14e2b --- /dev/null +++ b/public/js/vendor/jquery.dropotron.min.js @@ -0,0 +1,2 @@ +/* jquery.dropotron.js v1.4.3 | (c) @ajlkn | github.com/ajlkn/jquery.dropotron | MIT licensed */ +!function(e){e.fn.disableSelection_dropotron=function(){return e(this).css("user-select","none").css("-khtml-user-select","none").css("-moz-user-select","none").css("-o-user-select","none").css("-webkit-user-select","none")},e.fn.dropotron=function(t){if(0==this.length)return e(this);if(this.length>1)for(var o=0;o0&&t.add(n).on("mouseleave",function(e){window.clearTimeout(c),c=window.setTimeout(function(){t.trigger("doCollapse")},o.hideDelay)}),t.disableSelection_dropotron().hide().addClass(o.menuClass).css("position","absolute").on("mouseenter",function(e){window.clearTimeout(c)}).on("doExpand",function(){if(t.is(":visible"))return!1;window.clearTimeout(c),s.each(function(){var t=e(this);e.contains(t.get(0),n.get(0))||t.trigger("doCollapse")});var i,a,d,f,u=n.offset(),p=n.position(),h=(n.parent().position(),n.outerWidth()),g=t.outerWidth(),v=t.css("z-index")==o.baseZIndex;if(v){switch(i=o.detach?u:p,f=i.top+n.outerHeight()+o.globalOffsetY,a=o.alignment,t.removeClass("left").removeClass("right").removeClass("center"),o.alignment){case"right":d=i.left-g+h,0>d&&(d=i.left,a="left");break;case"center":d=i.left-Math.floor((g-h)/2),0>d?(d=i.left,a="left"):d+g>l.width()&&(d=i.left-g+h,a="right");break;case"left":default:d=i.left,d+g>l.width()&&(d=i.left-g+h,a="right")}t.addClass(a)}else switch("relative"==n.css("position")||"absolute"==n.css("position")?(f=o.offsetY,d=-1*p.left):(f=p.top+o.offsetY,d=0),o.alignment){case"right":d+=-1*n.parent().outerWidth()+o.offsetX;break;case"center":case"left":default:d+=n.parent().outerWidth()+o.offsetX}navigator.userAgent.match(/MSIE ([0-9]+)\./)&&RegExp.$1<8&&(d+=o.IEOffsetX,f+=o.IEOffsetY),t.css("left",d+"px").css("top",f+"px").css("opacity","0.01").show();var C=!1;switch(d="relative"==n.css("position")||"absolute"==n.css("position")?-1*p.left:0,t.offset().left<0?(d+=n.parent().outerWidth()-o.offsetX,C=!0):t.offset().left+g>l.width()&&(d+=-1*n.parent().outerWidth()-o.offsetX,C=!0),C&&t.css("left",d+"px"),t.hide().css("opacity","1"),o.mode){case"zoom":r=!0,n.addClass(o.openerActiveClass),t.animate({width:"toggle",height:"toggle"},o.speed,o.easing,function(){r=!1});break;case"slide":r=!0,n.addClass(o.openerActiveClass),t.animate({height:"toggle"},o.speed,o.easing,function(){r=!1});break;case"fade":if(r=!0,v&&!o.noOpenerFade){var C;C="slow"==o.speed?80:"fast"==o.speed?40:Math.floor(o.speed/2),n.fadeTo(C,.01,function(){n.addClass(o.openerActiveClass),n.fadeTo(o.speed,1),t.fadeIn(o.speed,function(){r=!1})})}else n.addClass(o.openerActiveClass),n.fadeTo(o.speed,1),t.fadeIn(o.speed,function(){r=!1});break;case"instant":default:n.addClass(o.openerActiveClass),t.show()}return!1}).on("doCollapse",function(){return t.is(":visible")?(t.hide(),n.removeClass(o.openerActiveClass),t.find("."+o.openerActiveClass).removeClass(o.openerActiveClass),t.find("ul").hide(),!1):!1}).on("doToggle",function(e){return t.is(":visible")?t.trigger("doCollapse"):t.trigger("doExpand"),!1}),n.disableSelection_dropotron().addClass("opener").css("cursor","pointer").on("click touchend",function(e){r||(e.preventDefault(),e.stopPropagation(),t.trigger("doToggle"))}),"hover"==o.expandMode&&n.hover(function(e){r||(d=window.setTimeout(function(){t.trigger("doExpand")},o.hoverDelay))},function(e){window.clearTimeout(d)})}),s.find("a").css("display","block").on("click touchend",function(t){r||e(this).attr("href").length<1&&t.preventDefault()}),n.find("li").css("white-space","nowrap").each(function(){var t=e(this),o=t.children("a"),s=t.children("ul"),i=o.attr("href");o.on("click touchend",function(e){0==i.length||"#"==i?e.preventDefault():e.stopPropagation()}),o.length>0&&0==s.length&&t.on("click touchend",function(e){r||(n.trigger("doCollapseAll"),e.stopPropagation())})}),n.children("li").each(function(){var t,n=e(this),s=n.children("ul");if(s.length>0){o.detach&&(o.cloneOnDetach&&(t=s.clone(),t.attr("class","").hide().appendTo(s.parent())),s.detach().appendTo(i));for(var a=o.baseZIndex,l=1,r=s;r.length>0;l++)r.css("z-index",a++),o.submenuClassPrefix&&r.addClass(o.submenuClassPrefix+(a-1-o.baseZIndex)),r=r.find("> li > ul")}}),l.on("scroll",function(){n.trigger("doCollapseAll")}).on("keypress",function(e){r||27!=e.keyCode||(e.preventDefault(),n.trigger("doCollapseAll"))}),a.on("click touchend",function(){r||n.trigger("doCollapseAll")})}}(jQuery); diff --git a/public/js/vendor/jquery.inputmask.bundle.js b/public/js/vendor/jquery.inputmask.bundle.js new file mode 100644 index 0000000..3b07847 --- /dev/null +++ b/public/js/vendor/jquery.inputmask.bundle.js @@ -0,0 +1,9 @@ +/*! +* jquery.inputmask.bundle.js +* https://github.com/RobinHerbots/Inputmask +* Copyright (c) 2010 - 2019 Robin Herbots +* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php) +* Version: 4.0.9 +*/ + +(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(exports,"__esModule",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value==="object"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,"default",{enumerable:true,value:value});if(mode&2&&typeof value!="string")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module["default"]}:function getModuleExports(){return module};__webpack_require__.d(getter,"a",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p="";return __webpack_require__(__webpack_require__.s=0)})([function(module,exports,__webpack_require__){"use strict";__webpack_require__(1);__webpack_require__(6);__webpack_require__(7);var _inputmask=__webpack_require__(2);var _inputmask2=_interopRequireDefault(_inputmask);var _inputmask3=__webpack_require__(3);var _inputmask4=_interopRequireDefault(_inputmask3);var _jquery=__webpack_require__(4);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}if(_inputmask4.default===_jquery2.default){__webpack_require__(8)}window.Inputmask=_inputmask2.default},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(2)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function(Inputmask){Inputmask.extendDefinitions({A:{validator:"[A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",casing:"upper"},"&":{validator:"[0-9A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",casing:"upper"},"#":{validator:"[0-9A-Fa-f]",casing:"upper"}});Inputmask.extendAliases({cssunit:{regex:"[+-]?[0-9]+\\.?([0-9]+)?(px|em|rem|ex|%|in|cm|mm|pt|pc)"},url:{regex:"(https?|ftp)//.*",autoUnmask:false},ip:{mask:"i[i[i]].i[i[i]].i[i[i]].i[i[i]]",definitions:{i:{validator:function validator(chrs,maskset,pos,strict,opts){if(pos-1>-1&&maskset.buffer[pos-1]!=="."){chrs=maskset.buffer[pos-1]+chrs;if(pos-2>-1&&maskset.buffer[pos-2]!=="."){chrs=maskset.buffer[pos-2]+chrs}else chrs="0"+chrs}else chrs="00"+chrs;return new RegExp("25[0-5]|2[0-4][0-9]|[01][0-9][0-9]").test(chrs)}}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return maskedValue},inputmode:"numeric"},email:{mask:"*{1,64}[.*{1,64}][.*{1,64}][.*{1,63}]@-{1,63}.-{1,63}[.-{1,63}][.-{1,63}]",greedy:false,casing:"lower",onBeforePaste:function onBeforePaste(pastedValue,opts){pastedValue=pastedValue.toLowerCase();return pastedValue.replace("mailto:","")},definitions:{"*":{validator:"[0-9\uff11-\uff19A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5!#$%&'*+/=?^_`{|}~-]"},"-":{validator:"[0-9A-Za-z-]"}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return maskedValue},inputmode:"email"},mac:{mask:"##:##:##:##:##:##"},vin:{mask:"V{13}9{4}",definitions:{V:{validator:"[A-HJ-NPR-Za-hj-npr-z\\d]",casing:"upper"}},clearIncomplete:true,autoUnmask:true}});return Inputmask})},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(3),__webpack_require__(5)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function($,window,undefined){var document=window.document,ua=navigator.userAgent,ie=ua.indexOf("MSIE ")>0||ua.indexOf("Trident/")>0,mobile=isInputEventSupported("touchstart"),iemobile=/iemobile/i.test(ua),iphone=/iphone/i.test(ua)&&!iemobile;function Inputmask(alias,options,internal){if(!(this instanceof Inputmask)){return new Inputmask(alias,options,internal)}this.el=undefined;this.events={};this.maskset=undefined;this.refreshValue=false;if(internal!==true){if($.isPlainObject(alias)){options=alias}else{options=options||{};if(alias)options.alias=alias}this.opts=$.extend(true,{},this.defaults,options);this.noMasksCache=options&&options.definitions!==undefined;this.userOptions=options||{};this.isRTL=this.opts.numericInput;resolveAlias(this.opts.alias,options,this.opts)}}Inputmask.prototype={dataAttribute:"data-inputmask",defaults:{placeholder:"_",optionalmarker:["[","]"],quantifiermarker:["{","}"],groupmarker:["(",")"],alternatormarker:"|",escapeChar:"\\",mask:null,regex:null,oncomplete:$.noop,onincomplete:$.noop,oncleared:$.noop,repeat:0,greedy:false,autoUnmask:false,removeMaskOnSubmit:false,clearMaskOnLostFocus:true,insertMode:true,clearIncomplete:false,alias:null,onKeyDown:$.noop,onBeforeMask:null,onBeforePaste:function onBeforePaste(pastedValue,opts){return $.isFunction(opts.onBeforeMask)?opts.onBeforeMask.call(this,pastedValue,opts):pastedValue},onBeforeWrite:null,onUnMask:null,showMaskOnFocus:true,showMaskOnHover:true,onKeyValidation:$.noop,skipOptionalPartCharacter:" ",numericInput:false,rightAlign:false,undoOnEscape:true,radixPoint:"",_radixDance:false,groupSeparator:"",keepStatic:null,positionCaretOnTab:true,tabThrough:false,supportsInputType:["text","tel","url","password","search"],ignorables:[8,9,13,19,27,33,34,35,36,37,38,39,40,45,46,93,112,113,114,115,116,117,118,119,120,121,122,123,0,229],isComplete:null,preValidation:null,postValidation:null,staticDefinitionSymbol:undefined,jitMasking:false,nullable:true,inputEventOnly:false,noValuePatching:false,positionCaretOnClick:"lvp",casing:null,inputmode:"verbatim",colorMask:false,disablePredictiveText:false,importDataAttributes:true,shiftPositions:true},definitions:{9:{validator:"[0-9\uff11-\uff19]",definitionSymbol:"*"},a:{validator:"[A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",definitionSymbol:"*"},"*":{validator:"[0-9\uff11-\uff19A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]"}},aliases:{},masksCache:{},mask:function mask(elems){var that=this;function importAttributeOptions(npt,opts,userOptions,dataAttribute){if(opts.importDataAttributes===true){var attrOptions=npt.getAttribute(dataAttribute),option,dataoptions,optionData,p;var importOption=function importOption(option,optionData){optionData=optionData!==undefined?optionData:npt.getAttribute(dataAttribute+"-"+option);if(optionData!==null){if(typeof optionData==="string"){if(option.indexOf("on")===0)optionData=window[optionData];else if(optionData==="false")optionData=false;else if(optionData==="true")optionData=true}userOptions[option]=optionData}};if(attrOptions&&attrOptions!==""){attrOptions=attrOptions.replace(/'/g,'"');dataoptions=JSON.parse("{"+attrOptions+"}")}if(dataoptions){optionData=undefined;for(p in dataoptions){if(p.toLowerCase()==="alias"){optionData=dataoptions[p];break}}}importOption("alias",optionData);if(userOptions.alias){resolveAlias(userOptions.alias,userOptions,opts)}for(option in opts){if(dataoptions){optionData=undefined;for(p in dataoptions){if(p.toLowerCase()===option.toLowerCase()){optionData=dataoptions[p];break}}}importOption(option,optionData)}}$.extend(true,opts,userOptions);if(npt.dir==="rtl"||opts.rightAlign){npt.style.textAlign="right"}if(npt.dir==="rtl"||opts.numericInput){npt.dir="ltr";npt.removeAttribute("dir");opts.isRTL=true}return Object.keys(userOptions).length}if(typeof elems==="string"){elems=document.getElementById(elems)||document.querySelectorAll(elems)}elems=elems.nodeName?[elems]:elems;$.each(elems,function(ndx,el){var scopedOpts=$.extend(true,{},that.opts);if(importAttributeOptions(el,scopedOpts,$.extend(true,{},that.userOptions),that.dataAttribute)){var maskset=generateMaskSet(scopedOpts,that.noMasksCache);if(maskset!==undefined){if(el.inputmask!==undefined){el.inputmask.opts.autoUnmask=true;el.inputmask.remove()}el.inputmask=new Inputmask(undefined,undefined,true);el.inputmask.opts=scopedOpts;el.inputmask.noMasksCache=that.noMasksCache;el.inputmask.userOptions=$.extend(true,{},that.userOptions);el.inputmask.isRTL=scopedOpts.isRTL||scopedOpts.numericInput;el.inputmask.el=el;el.inputmask.maskset=maskset;$.data(el,"_inputmask_opts",scopedOpts);maskScope.call(el.inputmask,{action:"mask"})}}});return elems&&elems[0]?elems[0].inputmask||this:this},option:function option(options,noremask){if(typeof options==="string"){return this.opts[options]}else if((typeof options==="undefined"?"undefined":_typeof(options))==="object"){$.extend(this.userOptions,options);if(this.el&&noremask!==true){this.mask(this.el)}return this}},unmaskedvalue:function unmaskedvalue(value){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"unmaskedvalue",value:value})},remove:function remove(){return maskScope.call(this,{action:"remove"})},getemptymask:function getemptymask(){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"getemptymask"})},hasMaskedValue:function hasMaskedValue(){return!this.opts.autoUnmask},isComplete:function isComplete(){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"isComplete"})},getmetadata:function getmetadata(){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"getmetadata"})},isValid:function isValid(value){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"isValid",value:value})},format:function format(value,metadata){this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache);return maskScope.call(this,{action:"format",value:value,metadata:metadata})},setValue:function setValue(value){if(this.el){$(this.el).trigger("setvalue",[value])}},analyseMask:function analyseMask(mask,regexMask,opts){var tokenizer=/(?:[?*+]|\{[0-9\+\*]+(?:,[0-9\+\*]*)?(?:\|[0-9\+\*]*)?\})|[^.?*+^${[]()|\\]+|./g,regexTokenizer=/\[\^?]?(?:[^\\\]]+|\\[\S\s]?)*]?|\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\]+|./g,escaped=false,currentToken=new MaskToken,match,m,openenings=[],maskTokens=[],openingToken,currentOpeningToken,alternator,lastMatch,groupToken;function MaskToken(isGroup,isOptional,isQuantifier,isAlternator){this.matches=[];this.openGroup=isGroup||false;this.alternatorGroup=false;this.isGroup=isGroup||false;this.isOptional=isOptional||false;this.isQuantifier=isQuantifier||false;this.isAlternator=isAlternator||false;this.quantifier={min:1,max:1}}function insertTestDefinition(mtoken,element,position){position=position!==undefined?position:mtoken.matches.length;var prevMatch=mtoken.matches[position-1];if(regexMask){if(element.indexOf("[")===0||escaped&&/\\d|\\s|\\w]/i.test(element)||element==="."){mtoken.matches.splice(position++,0,{fn:new RegExp(element,opts.casing?"i":""),optionality:false,newBlockMarker:prevMatch===undefined?"master":prevMatch.def!==element,casing:null,def:element,placeholder:undefined,nativeDef:element})}else{if(escaped)element=element[element.length-1];$.each(element.split(""),function(ndx,lmnt){prevMatch=mtoken.matches[position-1];mtoken.matches.splice(position++,0,{fn:null,optionality:false,newBlockMarker:prevMatch===undefined?"master":prevMatch.def!==lmnt&&prevMatch.fn!==null,casing:null,def:opts.staticDefinitionSymbol||lmnt,placeholder:opts.staticDefinitionSymbol!==undefined?lmnt:undefined,nativeDef:(escaped?"'":"")+lmnt})})}escaped=false}else{var maskdef=(opts.definitions?opts.definitions[element]:undefined)||Inputmask.prototype.definitions[element];if(maskdef&&!escaped){mtoken.matches.splice(position++,0,{fn:maskdef.validator?typeof maskdef.validator=="string"?new RegExp(maskdef.validator,opts.casing?"i":""):new function(){this.test=maskdef.validator}:new RegExp("."),optionality:false,newBlockMarker:prevMatch===undefined?"master":prevMatch.def!==(maskdef.definitionSymbol||element),casing:maskdef.casing,def:maskdef.definitionSymbol||element,placeholder:maskdef.placeholder,nativeDef:element})}else{mtoken.matches.splice(position++,0,{fn:null,optionality:false,newBlockMarker:prevMatch===undefined?"master":prevMatch.def!==element&&prevMatch.fn!==null,casing:null,def:opts.staticDefinitionSymbol||element,placeholder:opts.staticDefinitionSymbol!==undefined?element:undefined,nativeDef:(escaped?"'":"")+element});escaped=false}}}function verifyGroupMarker(maskToken){if(maskToken&&maskToken.matches){$.each(maskToken.matches,function(ndx,token){var nextToken=maskToken.matches[ndx+1];if((nextToken===undefined||nextToken.matches===undefined||nextToken.isQuantifier===false)&&token&&token.isGroup){token.isGroup=false;if(!regexMask){insertTestDefinition(token,opts.groupmarker[0],0);if(token.openGroup!==true){insertTestDefinition(token,opts.groupmarker[1])}}}verifyGroupMarker(token)})}}function defaultCase(){if(openenings.length>0){currentOpeningToken=openenings[openenings.length-1];insertTestDefinition(currentOpeningToken,m);if(currentOpeningToken.isAlternator){alternator=openenings.pop();for(var mndx=0;mndx0){currentOpeningToken=openenings[openenings.length-1];currentOpeningToken.matches.push(alternator)}else{currentToken.matches.push(alternator)}}}else{insertTestDefinition(currentToken,m)}}function reverseTokens(maskToken){function reverseStatic(st){if(st===opts.optionalmarker[0])st=opts.optionalmarker[1];else if(st===opts.optionalmarker[1])st=opts.optionalmarker[0];else if(st===opts.groupmarker[0])st=opts.groupmarker[1];else if(st===opts.groupmarker[1])st=opts.groupmarker[0];return st}maskToken.matches=maskToken.matches.reverse();for(var match in maskToken.matches){if(maskToken.matches.hasOwnProperty(match)){var intMatch=parseInt(match);if(maskToken.matches[match].isQuantifier&&maskToken.matches[intMatch+1]&&maskToken.matches[intMatch+1].isGroup){var qt=maskToken.matches[match];maskToken.matches.splice(match,1);maskToken.matches.splice(intMatch+1,0,qt)}if(maskToken.matches[match].matches!==undefined){maskToken.matches[match]=reverseTokens(maskToken.matches[match])}else{maskToken.matches[match]=reverseStatic(maskToken.matches[match])}}}return maskToken}function groupify(matches){var groupToken=new MaskToken(true);groupToken.openGroup=false;groupToken.matches=matches;return groupToken}if(regexMask){opts.optionalmarker[0]=undefined;opts.optionalmarker[1]=undefined}while(match=regexMask?regexTokenizer.exec(mask):tokenizer.exec(mask)){m=match[0];if(regexMask){switch(m.charAt(0)){case"?":m="{0,1}";break;case"+":case"*":m="{"+m+"}";break}}if(escaped){defaultCase();continue}switch(m.charAt(0)){case"(?=":break;case"(?!":break;case"(?<=":break;case"(?0){currentOpeningToken=openenings[openenings.length-1];currentOpeningToken.matches.push(openingToken);if(currentOpeningToken.isAlternator){alternator=openenings.pop();for(var mndx=0;mndx0){currentOpeningToken=openenings[openenings.length-1];currentOpeningToken.matches.push(alternator)}else{currentToken.matches.push(alternator)}}}else{currentToken.matches.push(openingToken)}}else defaultCase();break;case opts.optionalmarker[0]:openenings.push(new MaskToken(false,true));break;case opts.groupmarker[0]:openenings.push(new MaskToken(true));break;case opts.quantifiermarker[0]:var quantifier=new MaskToken(false,false,true);m=m.replace(/[{}]/g,"");var mqj=m.split("|"),mq=mqj[0].split(","),mq0=isNaN(mq[0])?mq[0]:parseInt(mq[0]),mq1=mq.length===1?mq0:isNaN(mq[1])?mq[1]:parseInt(mq[1]);if(mq0==="*"||mq0==="+"){mq0=mq1==="*"?0:1}quantifier.quantifier={min:mq0,max:mq1,jit:mqj[1]};var matches=openenings.length>0?openenings[openenings.length-1].matches:currentToken.matches;match=matches.pop();if(match.isAlternator){matches.push(match);matches=match.matches;var groupToken=new MaskToken(true);var tmpMatch=matches.pop();matches.push(groupToken);matches=groupToken.matches;match=tmpMatch}if(!match.isGroup){match=groupify([match])}matches.push(match);matches.push(quantifier);break;case opts.alternatormarker:var groupQuantifier=function groupQuantifier(matches){var lastMatch=matches.pop();if(lastMatch.isQuantifier){lastMatch=groupify([matches.pop(),lastMatch])}return lastMatch};if(openenings.length>0){currentOpeningToken=openenings[openenings.length-1];var subToken=currentOpeningToken.matches[currentOpeningToken.matches.length-1];if(currentOpeningToken.openGroup&&(subToken.matches===undefined||subToken.isGroup===false&&subToken.isAlternator===false)){lastMatch=openenings.pop()}else{lastMatch=groupQuantifier(currentOpeningToken.matches)}}else{lastMatch=groupQuantifier(currentToken.matches)}if(lastMatch.isAlternator){openenings.push(lastMatch)}else{if(lastMatch.alternatorGroup){alternator=openenings.pop();lastMatch.alternatorGroup=false}else{alternator=new MaskToken(false,false,false,true)}alternator.matches.push(lastMatch);openenings.push(alternator);if(lastMatch.openGroup){lastMatch.openGroup=false;var alternatorGroup=new MaskToken(true);alternatorGroup.alternatorGroup=true;openenings.push(alternatorGroup)}}break;default:defaultCase()}}while(openenings.length>0){openingToken=openenings.pop();currentToken.matches.push(openingToken)}if(currentToken.matches.length>0){verifyGroupMarker(currentToken);maskTokens.push(currentToken)}if(opts.numericInput||opts.isRTL){reverseTokens(maskTokens[0])}return maskTokens},positionColorMask:function positionColorMask(input,template){input.style.left=template.offsetLeft+"px"}};Inputmask.extendDefaults=function(options){$.extend(true,Inputmask.prototype.defaults,options)};Inputmask.extendDefinitions=function(definition){$.extend(true,Inputmask.prototype.definitions,definition)};Inputmask.extendAliases=function(alias){$.extend(true,Inputmask.prototype.aliases,alias)};Inputmask.format=function(value,options,metadata){return Inputmask(options).format(value,metadata)};Inputmask.unmask=function(value,options){return Inputmask(options).unmaskedvalue(value)};Inputmask.isValid=function(value,options){return Inputmask(options).isValid(value)};Inputmask.remove=function(elems){if(typeof elems==="string"){elems=document.getElementById(elems)||document.querySelectorAll(elems)}elems=elems.nodeName?[elems]:elems;$.each(elems,function(ndx,el){if(el.inputmask)el.inputmask.remove()})};Inputmask.setValue=function(elems,value){if(typeof elems==="string"){elems=document.getElementById(elems)||document.querySelectorAll(elems)}elems=elems.nodeName?[elems]:elems;$.each(elems,function(ndx,el){if(el.inputmask)el.inputmask.setValue(value);else $(el).trigger("setvalue",[value])})};Inputmask.escapeRegex=function(str){var specials=["/",".","*","+","?","|","(",")","[","]","{","}","\\","$","^"];return str.replace(new RegExp("(\\"+specials.join("|\\")+")","gim"),"\\$1")};Inputmask.keyCode={BACKSPACE:8,BACKSPACE_SAFARI:127,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,RIGHT:39,SPACE:32,TAB:9,UP:38,X:88,CONTROL:17};Inputmask.dependencyLib=$;function resolveAlias(aliasStr,options,opts){var aliasDefinition=Inputmask.prototype.aliases[aliasStr];if(aliasDefinition){if(aliasDefinition.alias)resolveAlias(aliasDefinition.alias,undefined,opts);$.extend(true,opts,aliasDefinition);$.extend(true,opts,options);return true}else if(opts.mask===null){opts.mask=aliasStr}return false}function generateMaskSet(opts,nocache){function generateMask(mask,metadata,opts){var regexMask=false;if(mask===null||mask===""){regexMask=opts.regex!==null;if(regexMask){mask=opts.regex;mask=mask.replace(/^(\^)(.*)(\$)$/,"$2")}else{regexMask=true;mask=".*"}}if(mask.length===1&&opts.greedy===false&&opts.repeat!==0){opts.placeholder=""}if(opts.repeat>0||opts.repeat==="*"||opts.repeat==="+"){var repeatStart=opts.repeat==="*"?0:opts.repeat==="+"?1:opts.repeat;mask=opts.groupmarker[0]+mask+opts.groupmarker[1]+opts.quantifiermarker[0]+repeatStart+","+opts.repeat+opts.quantifiermarker[1]}var masksetDefinition,maskdefKey=regexMask?"regex_"+opts.regex:opts.numericInput?mask.split("").reverse().join(""):mask;if(Inputmask.prototype.masksCache[maskdefKey]===undefined||nocache===true){masksetDefinition={mask:mask,maskToken:Inputmask.prototype.analyseMask(mask,regexMask,opts),validPositions:{},_buffer:undefined,buffer:undefined,tests:{},excludes:{},metadata:metadata,maskLength:undefined,jitOffset:{}};if(nocache!==true){Inputmask.prototype.masksCache[maskdefKey]=masksetDefinition;masksetDefinition=$.extend(true,{},Inputmask.prototype.masksCache[maskdefKey])}}else masksetDefinition=$.extend(true,{},Inputmask.prototype.masksCache[maskdefKey]);return masksetDefinition}var ms;if($.isFunction(opts.mask)){opts.mask=opts.mask(opts)}if($.isArray(opts.mask)){if(opts.mask.length>1){if(opts.keepStatic===null){opts.keepStatic="auto";for(var i=0;i1){altMask+=opts.groupmarker[1]+opts.alternatormarker+opts.groupmarker[0]}if(msk.mask!==undefined&&!$.isFunction(msk.mask)){altMask+=msk.mask}else{altMask+=msk}});altMask+=opts.groupmarker[1];return generateMask(altMask,opts.mask,opts)}else opts.mask=opts.mask.pop()}if(opts.mask&&opts.mask.mask!==undefined&&!$.isFunction(opts.mask.mask)){ms=generateMask(opts.mask.mask,opts.mask,opts)}else{ms=generateMask(opts.mask,opts.mask,opts)}return ms}function isInputEventSupported(eventName){var el=document.createElement("input"),evName="on"+eventName,isSupported=evName in el;if(!isSupported){el.setAttribute(evName,"return;");isSupported=typeof el[evName]==="function"}el=null;return isSupported}function maskScope(actionObj,maskset,opts){maskset=maskset||this.maskset;opts=opts||this.opts;var inputmask=this,el=this.el,isRTL=this.isRTL,undoValue,$el,skipKeyPressEvent=false,skipInputEvent=false,ignorable=false,maxLength,mouseEnter=false,colorMask,originalPlaceholder;var getMaskTemplate=function getMaskTemplate(baseOnInput,minimalPos,includeMode,noJit,clearOptionalTail){var greedy=opts.greedy;if(clearOptionalTail)opts.greedy=false;minimalPos=minimalPos||0;var maskTemplate=[],ndxIntlzr,pos=0,test,testPos,lvp=getLastValidPosition();do{if(baseOnInput===true&&getMaskSet().validPositions[pos]){testPos=clearOptionalTail&&getMaskSet().validPositions[pos].match.optionality===true&&getMaskSet().validPositions[pos+1]===undefined&&(getMaskSet().validPositions[pos].generatedInput===true||getMaskSet().validPositions[pos].input==opts.skipOptionalPartCharacter&&pos>0)?determineTestTemplate(pos,getTests(pos,ndxIntlzr,pos-1)):getMaskSet().validPositions[pos];test=testPos.match;ndxIntlzr=testPos.locator.slice();maskTemplate.push(includeMode===true?testPos.input:includeMode===false?test.nativeDef:getPlaceholder(pos,test))}else{testPos=getTestTemplate(pos,ndxIntlzr,pos-1);test=testPos.match;ndxIntlzr=testPos.locator.slice();var jitMasking=noJit===true?false:opts.jitMasking!==false?opts.jitMasking:test.jit;if(jitMasking===false||jitMasking===undefined||typeof jitMasking==="number"&&isFinite(jitMasking)&&jitMasking>pos){maskTemplate.push(includeMode===false?test.nativeDef:getPlaceholder(pos,test))}}if(opts.keepStatic==="auto"){if(test.newBlockMarker&&test.fn!==null){opts.keepStatic=pos-1}}pos++}while((maxLength===undefined||pospos);if(maskTemplate[maskTemplate.length-1]===""){maskTemplate.pop()}if(includeMode!==false||getMaskSet().maskLength===undefined)getMaskSet().maskLength=pos-1;opts.greedy=greedy;return maskTemplate};function getMaskSet(){return maskset}function resetMaskSet(soft){var maskset=getMaskSet();maskset.buffer=undefined;if(soft!==true){maskset.validPositions={};maskset.p=0}}function getLastValidPosition(closestTo,strict,validPositions){var before=-1,after=-1,valids=validPositions||getMaskSet().validPositions;if(closestTo===undefined)closestTo=-1;for(var posNdx in valids){var psNdx=parseInt(posNdx);if(valids[psNdx]&&(strict||valids[psNdx].generatedInput!==true)){if(psNdx<=closestTo)before=psNdx;if(psNdx>=closestTo)after=psNdx}}return before===-1||before==closestTo?after:after==-1?before:closestTo-before0){decisionTaker=decisionTaker.split(",")[0]}return decisionTaker!==undefined?decisionTaker.toString():""}function getLocator(tst,align){var locator=(tst.alternation!=undefined?tst.mloc[getDecisionTaker(tst)]:tst.locator).join("");if(locator!=="")while(locator.length0?pos-1:0;var altTest=getTest(pos),targetLocator=getLocator(altTest),tstLocator,closest,bestMatch;for(var ndx=0;ndx500&&quantifierRecurse!==undefined){throw"Inputmask: There is probably an error in your mask definition or in the code. Create an issue on github with an example of the mask you are using. "+getMaskSet().mask}if(testPos===pos&&match.matches===undefined){matches.push({match:match,locator:loopNdx.reverse(),cd:cacheDependency,mloc:{}});return true}else if(match.matches!==undefined){if(match.isGroup&&quantifierRecurse!==match){match=handleMatch(maskToken.matches[$.inArray(match,maskToken.matches)+1],loopNdx,quantifierRecurse);if(match)return true}else if(match.isOptional){var optionalToken=match;match=resolveTestFromToken(match,ndxInitializer,loopNdx,quantifierRecurse);if(match){$.each(matches,function(ndx,mtch){mtch.match.optionality=true});latestMatch=matches[matches.length-1].match;if(quantifierRecurse===undefined&&isFirstMatch(latestMatch,optionalToken)){insertStop=true;testPos=pos}else return true}}else if(match.isAlternator){var alternateToken=match,malternateMatches=[],maltMatches,currentMatches=matches.slice(),loopNdxCnt=loopNdx.length;var altIndex=ndxInitializer.length>0?ndxInitializer.shift():-1;if(altIndex===-1||typeof altIndex==="string"){var currentPos=testPos,ndxInitializerClone=ndxInitializer.slice(),altIndexArr=[],amndx;if(typeof altIndex=="string"){altIndexArr=altIndex.split(",")}else{for(amndx=0;amndx=opts.keepStatic)altIndexArr=altIndexArr.slice(0,1);var unMatchedAlternation=false;for(var ndx=0;ndx0;match=malternateMatches.length>0;ndxInitializer=ndxInitializerClone.slice()}else match=handleMatch(alternateToken.matches[altIndex]||maskToken.matches[altIndex],[altIndex].concat(loopNdx),quantifierRecurse);if(match)return true}else if(match.isQuantifier&&quantifierRecurse!==maskToken.matches[$.inArray(match,maskToken.matches)-1]){var qt=match;for(var qndx=ndxInitializer.length>0?ndxInitializer.shift():0;qndx<(isNaN(qt.quantifier.max)?qndx+1:qt.quantifier.max)&&testPos<=pos;qndx++){var tokenGroup=maskToken.matches[$.inArray(qt,maskToken.matches)-1];match=handleMatch(tokenGroup,[qndx].concat(loopNdx),tokenGroup);if(match){latestMatch=matches[matches.length-1].match;latestMatch.optionalQuantifier=qndx>=qt.quantifier.min;latestMatch.jit=(qndx||1)*tokenGroup.matches.indexOf(latestMatch)>=qt.quantifier.jit;if(latestMatch.optionalQuantifier&&isFirstMatch(latestMatch,tokenGroup)){insertStop=true;testPos=pos;break}if(latestMatch.jit){getMaskSet().jitOffset[pos]=tokenGroup.matches.indexOf(latestMatch)}return true}}}else{match=resolveTestFromToken(match,ndxInitializer,loopNdx,quantifierRecurse);if(match)return true}}else{testPos++}}for(var tndx=ndxInitializer.length>0?ndxInitializer.shift():0;tndxpos){break}}}}function mergeLocators(pos,tests){var locator=[];if(!$.isArray(tests))tests=[tests];if(tests.length>0){if(tests[0].alternation===undefined){locator=determineTestTemplate(pos,tests.slice()).locator.slice();if(locator.length===0)locator=tests[0].locator.slice()}else{$.each(tests,function(ndx,tst){if(tst.def!==""){if(locator.length===0)locator=tst.locator.slice();else{for(var i=0;i-1){if(ndxIntlzr===undefined){var previousPos=pos-1,test;while((test=getMaskSet().validPositions[previousPos]||getMaskSet().tests[previousPos])===undefined&&previousPos>-1){previousPos--}if(test!==undefined&&previousPos>-1){ndxInitializer=mergeLocators(previousPos,test);cacheDependency=ndxInitializer.join("");testPos=previousPos}}if(getMaskSet().tests[pos]&&getMaskSet().tests[pos][0].cd===cacheDependency){return getMaskSet().tests[pos]}for(var mtndx=ndxInitializer.shift();mtndxpos){break}}}if(matches.length===0||insertStop){matches.push({match:{fn:null,optionality:false,casing:null,def:"",placeholder:""},locator:[],mloc:{},cd:cacheDependency})}if(ndxIntlzr!==undefined&&getMaskSet().tests[pos]){return $.extend(true,[],matches)}getMaskSet().tests[pos]=$.extend(true,[],matches);return getMaskSet().tests[pos]}function getBufferTemplate(){if(getMaskSet()._buffer===undefined){getMaskSet()._buffer=getMaskTemplate(false,1);if(getMaskSet().buffer===undefined)getMaskSet().buffer=getMaskSet()._buffer.slice()}return getMaskSet()._buffer}function getBuffer(noCache){if(getMaskSet().buffer===undefined||noCache===true){getMaskSet().buffer=getMaskTemplate(true,getLastValidPosition(),true);if(getMaskSet()._buffer===undefined)getMaskSet()._buffer=getMaskSet().buffer.slice()}return getMaskSet().buffer}function refreshFromBuffer(start,end,buffer){var i,p;if(start===true){resetMaskSet();start=0;end=buffer.length}else{for(i=start;i=0;lAltPos--){altPos=getMaskSet().validPositions[lAltPos];if(altPos&&altPos.alternation!==undefined){if(prevAltPos&&prevAltPos.locator[altPos.alternation]!==altPos.locator[altPos.alternation]){break}lastAlt=lAltPos;alternation=getMaskSet().validPositions[lastAlt].alternation;prevAltPos=altPos}}}if(alternation!==undefined){decisionPos=parseInt(lastAlt);getMaskSet().excludes[decisionPos]=getMaskSet().excludes[decisionPos]||[];if(pos!==true){getMaskSet().excludes[decisionPos].push(getDecisionTaker(prevAltPos))}var validInputsClone=[],staticInputsBeforePos=0;for(i=decisionPos;i0){var input=validInputs.shift();if(!(isValidRslt=isValid(getLastValidPosition(undefined,true)+1,input,false,fromSetValid,true))){break}}if(isValidRslt&&c!==undefined){var targetLvp=getLastValidPosition(pos)+1;for(i=decisionPos;itargetLvp?targetLvp:pos,c,strict,fromSetValid,true)}if(!isValidRslt){resetMaskSet();prevAltPos=getTest(decisionPos);getMaskSet().validPositions=$.extend(true,{},validPsClone);if(getMaskSet().excludes[decisionPos]){var decisionTaker=getDecisionTaker(prevAltPos);if(getMaskSet().excludes[decisionPos].indexOf(decisionTaker)!==-1){isValidRslt=alternate(pos,c,strict,fromSetValid,decisionPos-1);break}getMaskSet().excludes[decisionPos].push(decisionTaker);for(i=decisionPos;i1||posObj.begin-posObj.end===1:posObj.end-posObj.begin>1||posObj.end-posObj.begin===1}strict=strict===true;var maskPos=pos;if(pos.begin!==undefined){maskPos=isRTL?pos.end:pos.begin}function _isValid(position,c,strict){var rslt=false;$.each(getTests(position),function(ndx,tst){var test=tst.match;getBuffer(true);rslt=test.fn!=null?test.fn.test(c,getMaskSet(),position,strict,opts,isSelection(pos)):(c===test.def||c===opts.skipOptionalPartCharacter)&&test.def!==""?{c:getPlaceholder(position,test,true)||test.def,pos:position}:false;if(rslt!==false){var elem=rslt.c!==undefined?rslt.c:c,validatedPos=position;elem=elem===opts.skipOptionalPartCharacter&&test.fn===null?getPlaceholder(position,test,true)||test.def:elem;if(rslt.remove!==undefined){if(!$.isArray(rslt.remove))rslt.remove=[rslt.remove];$.each(rslt.remove.sort(function(a,b){return b-a}),function(ndx,lmnt){revalidateMask({begin:lmnt,end:lmnt+1})})}if(rslt.insert!==undefined){if(!$.isArray(rslt.insert))rslt.insert=[rslt.insert];$.each(rslt.insert.sort(function(a,b){return a-b}),function(ndx,lmnt){isValid(lmnt.pos,lmnt.c,true,fromSetValid)})}if(rslt!==true&&rslt.pos!==undefined&&rslt.pos!==position){validatedPos=rslt.pos}if(rslt!==true&&rslt.pos===undefined&&rslt.c===undefined){return false}if(!revalidateMask(pos,$.extend({},tst,{input:casing(elem,test,validatedPos)}),fromSetValid,validatedPos)){rslt=false}return false}});return rslt}var result=true,positionsClone=$.extend(true,{},getMaskSet().validPositions);if($.isFunction(opts.preValidation)&&!strict&&fromSetValid!==true&&validateOnly!==true){result=opts.preValidation(getBuffer(),maskPos,c,isSelection(pos),opts,getMaskSet())}if(result===true){trackbackPositions(undefined,maskPos,true);if(maxLength===undefined||maskPos0;originalPos--){if(getMaskSet().validPositions[originalPos])break}}for(var ps=originalPos;pspos+1?valids[pos+1]&&valids[pos+1].match.fn===null&&valids[pos+1]:valids[pos+1];return prevMatch&&nextMatch}return false}var begin=pos.begin!==undefined?pos.begin:pos,end=pos.end!==undefined?pos.end:pos;if(pos.begin>pos.end){begin=pos.end;end=pos.begin}validatedPos=validatedPos!==undefined?validatedPos:begin;if(begin!==end||opts.insertMode&&getMaskSet().validPositions[validatedPos]!==undefined&&fromSetValid===undefined){var positionsClone=$.extend(true,{},getMaskSet().validPositions),lvp=getLastValidPosition(undefined,true),i;getMaskSet().p=begin;for(i=lvp;i>=begin;i--){if(getMaskSet().validPositions[i]&&getMaskSet().validPositions[i].match.nativeDef==="+"){opts.isNegative=false}delete getMaskSet().validPositions[i]}var valid=true,j=validatedPos,vps=getMaskSet().validPositions,needsValidation=false,posMatch=j,i=j;if(validTest){getMaskSet().validPositions[validatedPos]=$.extend(true,{},validTest);posMatch++;j++;if(begin=end||i>=begin&&t.generatedInput!==true&&IsEnclosedStatic(i,positionsClone,{begin:begin,end:end}))){while(getTest(posMatch).match.def!==""){if(needsValidation===false&&positionsClone[posMatch]&&positionsClone[posMatch].match.nativeDef===t.match.nativeDef){getMaskSet().validPositions[posMatch]=$.extend(true,{},positionsClone[posMatch]);getMaskSet().validPositions[posMatch].input=t.input;trackbackPositions(undefined,posMatch,true);j=posMatch+1;valid=true}else if(opts.shiftPositions&&positionCanMatchDefinition(posMatch,t.match.def)){var result=isValid(posMatch,t.input,true,true);valid=result!==false;j=result.caret||result.insert?getLastValidPosition():posMatch+1;needsValidation=true}else{valid=t.generatedInput===true||t.input===opts.radixPoint&&opts.numericInput===true}if(valid)break;if(!valid&&posMatch>end&&isMask(posMatch,true)&&(t.match.fn!==null||posMatch>getMaskSet().maskLength)){break}posMatch++}if(getTest(posMatch).match.def=="")valid=false;posMatch=j}if(!valid)break}if(!valid){getMaskSet().validPositions=$.extend(true,{},positionsClone);resetMaskSet(true);return false}}else if(validTest){getMaskSet().validPositions[validatedPos]=$.extend(true,{},validTest)}resetMaskSet(true);return true}function isMask(pos,strict){var test=getTestTemplate(pos).match;if(test.def==="")test=getTest(pos).match;if(test.fn!=null){return test.fn}if(strict!==true&&pos>-1){var tests=getTests(pos);return tests.length>1+(tests[tests.length-1].match.def===""?1:0)}return false}function seekNext(pos,newBlock){var position=pos+1;while(getTest(position).match.def!==""&&(newBlock===true&&(getTest(position).match.newBlockMarker!==true||!isMask(position))||newBlock!==true&&!isMask(position))){position++}return position}function seekPrevious(pos,newBlock){var position=pos,tests;if(position<=0)return 0;while(--position>0&&(newBlock===true&&getTest(position).match.newBlockMarker!==true||newBlock!==true&&!isMask(position)&&(tests=getTests(position),tests.length<2||tests.length===2&&tests[1].match.def===""))){}return position}function writeBuffer(input,buffer,caretPos,event,triggerEvents){if(event&&$.isFunction(opts.onBeforeWrite)){var result=opts.onBeforeWrite.call(inputmask,event,buffer,caretPos,opts);if(result){if(result.refreshFromBuffer){var refresh=result.refreshFromBuffer;refreshFromBuffer(refresh===true?refresh:refresh.start,refresh.end,result.buffer||buffer);buffer=getBuffer(true)}if(caretPos!==undefined)caretPos=result.caret!==undefined?result.caret:caretPos}}if(input!==undefined){input.inputmask._valueSet(buffer.join(""));if(caretPos!==undefined&&(event===undefined||event.type!=="blur")){caret(input,caretPos)}else renderColorMask(input,caretPos,buffer.length===0);if(triggerEvents===true){var $input=$(input),nptVal=input.inputmask._valueGet();skipInputEvent=true;$input.trigger("input");setTimeout(function(){if(nptVal===getBufferTemplate().join("")){$input.trigger("cleared")}else if(isComplete(buffer)===true){$input.trigger("complete")}},0)}}}function getPlaceholder(pos,test,returnPL){test=test||getTest(pos).match;if(test.placeholder!==undefined||returnPL===true){return $.isFunction(test.placeholder)?test.placeholder(opts):test.placeholder}else if(test.fn===null){if(pos>-1&&getMaskSet().validPositions[pos]===undefined){var tests=getTests(pos),staticAlternations=[],prevTest;if(tests.length>1+(tests[tests.length-1].match.def===""?1:0)){for(var i=0;i1){if(/[0-9a-bA-Z]/.test(staticAlternations[0].match.def)){return opts.placeholder.charAt(pos%opts.placeholder.length)}}}}}}return test.def}return opts.placeholder.charAt(pos%opts.placeholder.length)}function HandleNativePlaceholder(npt,value){if(ie){if(npt.inputmask._valueGet()!==value&&(npt.placeholder!==value||npt.placeholder==="")){var buffer=getBuffer().slice(),nptValue=npt.inputmask._valueGet();if(nptValue!==value){var lvp=getLastValidPosition();if(lvp===-1&&nptValue===getBufferTemplate().join("")){buffer=[]}else if(lvp!==-1){clearOptionalTail(buffer)}writeBuffer(npt,buffer)}}}else if(npt.placeholder!==value){npt.placeholder=value;if(npt.placeholder==="")npt.removeAttribute("placeholder")}}var EventRuler={on:function on(input,eventName,eventHandler){var ev=function ev(e){var that=this;if(that.inputmask===undefined&&this.nodeName!=="FORM"){var imOpts=$.data(that,"_inputmask_opts");if(imOpts)new Inputmask(imOpts).mask(that);else EventRuler.off(that)}else if(e.type!=="setvalue"&&this.nodeName!=="FORM"&&(that.disabled||that.readOnly&&!(e.type==="keydown"&&e.ctrlKey&&e.keyCode===67||opts.tabThrough===false&&e.keyCode===Inputmask.keyCode.TAB))){e.preventDefault()}else{switch(e.type){case"input":if(skipInputEvent===true){skipInputEvent=false;return e.preventDefault()}if(mobile){var args=arguments;setTimeout(function(){eventHandler.apply(that,args);caret(that,that.inputmask.caretPos,undefined,true)},0);return false}break;case"keydown":skipKeyPressEvent=false;skipInputEvent=false;break;case"keypress":if(skipKeyPressEvent===true){return e.preventDefault()}skipKeyPressEvent=true;break;case"click":if(iemobile||iphone){var args=arguments;setTimeout(function(){eventHandler.apply(that,args)},0);return false}break}var returnVal=eventHandler.apply(that,arguments);if(returnVal===false){e.preventDefault();e.stopPropagation()}return returnVal}};input.inputmask.events[eventName]=input.inputmask.events[eventName]||[];input.inputmask.events[eventName].push(ev);if($.inArray(eventName,["submit","reset"])!==-1){if(input.form!==null)$(input.form).on(eventName,ev)}else{$(input).on(eventName,ev)}},off:function off(input,event){if(input.inputmask&&input.inputmask.events){var events;if(event){events=[];events[event]=input.inputmask.events[event]}else{events=input.inputmask.events}$.each(events,function(eventName,evArr){while(evArr.length>0){var ev=evArr.pop();if($.inArray(eventName,["submit","reset"])!==-1){if(input.form!==null)$(input.form).off(eventName,ev)}else{$(input).off(eventName,ev)}}delete input.inputmask.events[eventName]})}}};var EventHandlers={keydownEvent:function keydownEvent(e){var input=this,$input=$(input),k=e.keyCode,pos=caret(input);if(k===Inputmask.keyCode.BACKSPACE||k===Inputmask.keyCode.DELETE||iphone&&k===Inputmask.keyCode.BACKSPACE_SAFARI||e.ctrlKey&&k===Inputmask.keyCode.X&&!isInputEventSupported("cut")){e.preventDefault();handleRemove(input,k,pos);writeBuffer(input,getBuffer(true),getMaskSet().p,e,input.inputmask._valueGet()!==getBuffer().join(""))}else if(k===Inputmask.keyCode.END||k===Inputmask.keyCode.PAGE_DOWN){e.preventDefault();var caretPos=seekNext(getLastValidPosition());caret(input,e.shiftKey?pos.begin:caretPos,caretPos,true)}else if(k===Inputmask.keyCode.HOME&&!e.shiftKey||k===Inputmask.keyCode.PAGE_UP){e.preventDefault();caret(input,0,e.shiftKey?pos.begin:0,true)}else if((opts.undoOnEscape&&k===Inputmask.keyCode.ESCAPE||k===90&&e.ctrlKey)&&e.altKey!==true){checkVal(input,true,false,undoValue.split(""));$input.trigger("click")}else if(k===Inputmask.keyCode.INSERT&&!(e.shiftKey||e.ctrlKey)){opts.insertMode=!opts.insertMode;input.setAttribute("im-insert",opts.insertMode)}else if(opts.tabThrough===true&&k===Inputmask.keyCode.TAB){if(e.shiftKey===true){if(getTest(pos.begin).match.fn===null){pos.begin=seekNext(pos.begin)}pos.end=seekPrevious(pos.begin,true);pos.begin=seekPrevious(pos.end,true)}else{pos.begin=seekNext(pos.begin,true);pos.end=seekNext(pos.begin,true);if(pos.endbuffer.length?-1:0,frontPart=inputValue.substr(0,caretPos.begin),backPart=inputValue.substr(caretPos.begin),frontBufferPart=buffer.substr(0,caretPos.begin+offset),backBufferPart=buffer.substr(caretPos.begin+offset);var selection=caretPos,entries="",isEntry=false;if(frontPart!==frontBufferPart){var fpl=(isEntry=frontPart.length>=frontBufferPart.length)?frontPart.length:frontBufferPart.length,i;for(i=0;frontPart.charAt(i)===frontBufferPart.charAt(i)&&ibackBufferPart.length){entries+=backPart.slice(0,1)}else{if(backPart.length0){$.each(entries.split(""),function(ndx,entry){var keypress=new $.Event("keypress");keypress.which=entry.charCodeAt(0);ignorable=false;EventHandlers.keypressEvent.call(input,keypress)})}else{if(selection.begin===selection.end-1){selection.begin=seekPrevious(selection.begin+1);if(selection.begin===selection.end-1){caret(input,selection.begin)}else{caret(input,selection.begin,selection.end)}}var keydown=new $.Event("keydown");keydown.keyCode=opts.numericInput?Inputmask.keyCode.BACKSPACE:Inputmask.keyCode.DELETE;EventHandlers.keydownEvent.call(input,keydown)}e.preventDefault()}}},beforeInputEvent:function beforeInputEvent(e){if(e.cancelable){var input=this;switch(e.inputType){case"insertText":$.each(e.data.split(""),function(ndx,entry){var keypress=new $.Event("keypress");keypress.which=entry.charCodeAt(0);ignorable=false;EventHandlers.keypressEvent.call(input,keypress)});return e.preventDefault();case"deleteContentBackward":var keydown=new $.Event("keydown");keydown.keyCode=Inputmask.keyCode.BACKSPACE;EventHandlers.keydownEvent.call(input,keydown);return e.preventDefault();case"deleteContentForward":var keydown=new $.Event("keydown");keydown.keyCode=Inputmask.keyCode.DELETE;EventHandlers.keydownEvent.call(input,keydown);return e.preventDefault()}}},setValueEvent:function setValueEvent(e){this.inputmask.refreshValue=false;var input=this,value=e&&e.detail?e.detail[0]:arguments[1],value=value||input.inputmask._valueGet(true);if($.isFunction(opts.onBeforeMask))value=opts.onBeforeMask.call(inputmask,value,opts)||value;value=value.toString().split("");checkVal(input,true,false,value);undoValue=getBuffer().join("");if((opts.clearMaskOnLostFocus||opts.clearIncomplete)&&input.inputmask._valueGet()===getBufferTemplate().join("")){input.inputmask._valueSet("")}},focusEvent:function focusEvent(e){var input=this,nptValue=input.inputmask._valueGet();if(opts.showMaskOnFocus){if(nptValue!==getBuffer().join("")){writeBuffer(input,getBuffer(),seekNext(getLastValidPosition()))}else if(mouseEnter===false){caret(input,seekNext(getLastValidPosition()))}}if(opts.positionCaretOnTab===true&&mouseEnter===false){EventHandlers.clickEvent.apply(input,[e,true])}undoValue=getBuffer().join("")},mouseleaveEvent:function mouseleaveEvent(e){var input=this;mouseEnter=false;if(opts.clearMaskOnLostFocus&&document.activeElement!==input){HandleNativePlaceholder(input,originalPlaceholder)}},clickEvent:function clickEvent(e,tabbed){function doRadixFocus(clickPos){if(opts.radixPoint!==""){var vps=getMaskSet().validPositions;if(vps[clickPos]===undefined||vps[clickPos].input===getPlaceholder(clickPos)){if(clickPos=newPos||clickPosition===lastPosition){lastPosition=newPos}}caret(input,lastPosition)}break}}}},0)},cutEvent:function cutEvent(e){var input=this,$input=$(input),pos=caret(input),ev=e.originalEvent||e;var clipboardData=window.clipboardData||ev.clipboardData,clipData=isRTL?getBuffer().slice(pos.end,pos.begin):getBuffer().slice(pos.begin,pos.end);clipboardData.setData("text",isRTL?clipData.reverse().join(""):clipData.join(""));if(document.execCommand)document.execCommand("copy");handleRemove(input,Inputmask.keyCode.DELETE,pos);writeBuffer(input,getBuffer(),getMaskSet().p,e,undoValue!==getBuffer().join(""))},blurEvent:function blurEvent(e){var $input=$(this),input=this;if(input.inputmask){HandleNativePlaceholder(input,originalPlaceholder);var nptValue=input.inputmask._valueGet(),buffer=getBuffer().slice();if(nptValue!==""||colorMask!==undefined){if(opts.clearMaskOnLostFocus){if(getLastValidPosition()===-1&&nptValue===getBufferTemplate().join("")){buffer=[]}else{clearOptionalTail(buffer)}}if(isComplete(buffer)===false){setTimeout(function(){$input.trigger("incomplete")},0);if(opts.clearIncomplete){resetMaskSet();if(opts.clearMaskOnLostFocus){buffer=[]}else{buffer=getBufferTemplate().slice()}}}writeBuffer(input,buffer,undefined,e)}if(undoValue!==getBuffer().join("")){undoValue=buffer.join("");$input.trigger("change")}}},mouseenterEvent:function mouseenterEvent(e){var input=this;mouseEnter=true;if(document.activeElement!==input&&opts.showMaskOnHover){HandleNativePlaceholder(input,(isRTL?getBuffer().slice().reverse():getBuffer()).join(""))}},submitEvent:function submitEvent(e){if(undoValue!==getBuffer().join("")){$el.trigger("change")}if(opts.clearMaskOnLostFocus&&getLastValidPosition()===-1&&el.inputmask._valueGet&&el.inputmask._valueGet()===getBufferTemplate().join("")){el.inputmask._valueSet("")}if(opts.clearIncomplete&&isComplete(getBuffer())===false){el.inputmask._valueSet("")}if(opts.removeMaskOnSubmit){el.inputmask._valueSet(el.inputmask.unmaskedvalue(),true);setTimeout(function(){writeBuffer(el,getBuffer())},0)}},resetEvent:function resetEvent(e){el.inputmask.refreshValue=true;setTimeout(function(){$el.trigger("setvalue")},0)}};function checkVal(input,writeOut,strict,nptvl,initiatingEvent){var inputmask=this||input.inputmask,inputValue=nptvl.slice(),charCodes="",initialNdx=-1,result=undefined;function isTemplateMatch(ndx,charCodes){var charCodeNdx=getMaskTemplate(true,0,false).slice(ndx,seekNext(ndx)).join("").replace(/'/g,"").indexOf(charCodes);return charCodeNdx!==-1&&!isMask(ndx)&&(getTest(ndx).match.nativeDef===charCodes.charAt(0)||getTest(ndx).match.fn===null&&getTest(ndx).match.nativeDef==="'"+charCodes.charAt(0)||getTest(ndx).match.nativeDef===" "&&(getTest(ndx+1).match.nativeDef===charCodes.charAt(0)||getTest(ndx+1).match.fn===null&&getTest(ndx+1).match.nativeDef==="'"+charCodes.charAt(0)))}resetMaskSet();if(!strict&&opts.autoUnmask!==true){var staticInput=getBufferTemplate().slice(0,seekNext(-1)).join(""),matches=inputValue.join("").match(new RegExp("^"+Inputmask.escapeRegex(staticInput),"g"));if(matches&&matches.length>0){inputValue.splice(0,matches.length*staticInput.length);initialNdx=seekNext(initialNdx)}}else{initialNdx=seekNext(initialNdx)}if(initialNdx===-1){getMaskSet().p=seekNext(initialNdx);initialNdx=0}else getMaskSet().p=initialNdx;inputmask.caretPos={begin:initialNdx};$.each(inputValue,function(ndx,charCode){if(charCode!==undefined){if(getMaskSet().validPositions[ndx]===undefined&&inputValue[ndx]===getPlaceholder(ndx)&&isMask(ndx,true)&&isValid(ndx,inputValue[ndx],true,undefined,undefined,true)===false){getMaskSet().p++}else{var keypress=new $.Event("_checkval");keypress.which=charCode.charCodeAt(0);charCodes+=charCode;var lvp=getLastValidPosition(undefined,true);if(!isTemplateMatch(initialNdx,charCodes)){result=EventHandlers.keypressEvent.call(input,keypress,true,false,strict,inputmask.caretPos.begin);if(result){initialNdx=inputmask.caretPos.begin+1;charCodes=""}}else{result=EventHandlers.keypressEvent.call(input,keypress,true,false,strict,lvp+1)}if(result){writeBuffer(undefined,getBuffer(),result.forwardPosition,keypress,false);inputmask.caretPos={begin:result.forwardPosition,end:result.forwardPosition}}}}});if(writeOut)writeBuffer(input,getBuffer(),result?result.forwardPosition:undefined,initiatingEvent||new $.Event("checkval"),initiatingEvent&&initiatingEvent.type==="input")}function unmaskedvalue(input){if(input){if(input.inputmask===undefined){return input.value}if(input.inputmask&&input.inputmask.refreshValue){EventHandlers.setValueEvent.call(input)}}var umValue=[],vps=getMaskSet().validPositions;for(var pndx in vps){if(vps[pndx].match&&vps[pndx].match.fn!=null){umValue.push(vps[pndx].input)}}var unmaskedValue=umValue.length===0?"":(isRTL?umValue.reverse():umValue).join("");if($.isFunction(opts.onUnMask)){var bufferValue=(isRTL?getBuffer().slice().reverse():getBuffer()).join("");unmaskedValue=opts.onUnMask.call(inputmask,bufferValue,unmaskedValue,opts)}return unmaskedValue}function caret(input,begin,end,notranslate){function translatePosition(pos){if(isRTL&&typeof pos==="number"&&(!opts.greedy||opts.placeholder!=="")&&el){pos=el.inputmask._valueGet().length-pos}return pos}var range;if(begin!==undefined){if($.isArray(begin)){end=isRTL?begin[0]:begin[1];begin=isRTL?begin[1]:begin[0]}if(begin.begin!==undefined){end=isRTL?begin.begin:begin.end;begin=isRTL?begin.end:begin.begin}if(typeof begin==="number"){begin=notranslate?begin:translatePosition(begin);end=notranslate?end:translatePosition(end);end=typeof end=="number"?end:begin;var scrollCalc=parseInt(((input.ownerDocument.defaultView||window).getComputedStyle?(input.ownerDocument.defaultView||window).getComputedStyle(input,null):input.currentStyle).fontSize)*end;input.scrollLeft=scrollCalc>input.scrollWidth?scrollCalc:0;input.inputmask.caretPos={begin:begin,end:end};if(input===document.activeElement){if("selectionStart"in input){input.selectionStart=begin;input.selectionEnd=end}else if(window.getSelection){range=document.createRange();if(input.firstChild===undefined||input.firstChild===null){var textNode=document.createTextNode("");input.appendChild(textNode)}range.setStart(input.firstChild,beginlvp;pos--){testPos=positions[pos];if((testPos.match.optionality||testPos.match.optionalQuantifier&&testPos.match.newBlockMarker||lvTestAlt&&(lvTestAlt!==positions[pos].locator[lvTest.alternation]&&testPos.match.fn!=null||testPos.match.fn===null&&testPos.locator[lvTest.alternation]&&checkAlternationMatch(testPos.locator[lvTest.alternation].toString().split(","),lvTestAlt.toString().split(","))&&getTests(pos)[0].def!==""))&&buffer[pos]===getPlaceholder(pos,testPos.match)){bl--}else break}return returnDefinition?{l:bl,def:positions[bl]?positions[bl].match:undefined}:bl}function clearOptionalTail(buffer){buffer.length=0;var template=getMaskTemplate(true,0,true,undefined,true),lmnt,validPos;while(lmnt=template.shift(),lmnt!==undefined){buffer.push(lmnt)}return buffer}function isComplete(buffer){if($.isFunction(opts.isComplete))return opts.isComplete(buffer,opts);if(opts.repeat==="*")return undefined;var complete=false,lrp=determineLastRequiredPosition(true),aml=seekPrevious(lrp.l);if(lrp.def===undefined||lrp.def.newBlockMarker||lrp.def.optionality||lrp.def.optionalQuantifier){complete=true;for(var i=0;i<=aml;i++){var test=getTestTemplate(i).match;if(test.fn!==null&&getMaskSet().validPositions[i]===undefined&&test.optionality!==true&&test.optionalQuantifier!==true||test.fn===null&&buffer[i]!==getPlaceholder(i,test)){complete=false;break}}}return complete}function handleRemove(input,k,pos,strict,fromIsValid){if(opts.numericInput||isRTL){if(k===Inputmask.keyCode.BACKSPACE){k=Inputmask.keyCode.DELETE}else if(k===Inputmask.keyCode.DELETE){k=Inputmask.keyCode.BACKSPACE}if(isRTL){var pend=pos.end;pos.end=pos.begin;pos.begin=pend}}if(k===Inputmask.keyCode.BACKSPACE&&pos.end-pos.begin<1){pos.begin=seekPrevious(pos.begin);if(getMaskSet().validPositions[pos.begin]!==undefined&&getMaskSet().validPositions[pos.begin].input===opts.groupSeparator){pos.begin--}}else if(k===Inputmask.keyCode.DELETE&&pos.begin===pos.end){pos.end=isMask(pos.end,true)&&getMaskSet().validPositions[pos.end]&&getMaskSet().validPositions[pos.end].input!==opts.radixPoint?pos.end+1:seekNext(pos.end)+1;if(getMaskSet().validPositions[pos.begin]!==undefined&&getMaskSet().validPositions[pos.begin].input===opts.groupSeparator){pos.end++}}revalidateMask(pos);if(strict!==true&&opts.keepStatic!==false||opts.regex!==null){var result=alternate(true);if(result){var newPos=result.caret!==undefined?result.caret:result.pos?seekNext(result.pos.begin?result.pos.begin:result.pos):getLastValidPosition(-1,true);if(k!==Inputmask.keyCode.DELETE||pos.begin>newPos){pos.begin==newPos}}}var lvp=getLastValidPosition(pos.begin,true);if(lvp=clientx){var offset1=clientx-previousWidth;var offset2=e.offsetWidth-clientx;e.innerHTML=inputText.charAt(caretPos);offset1-=e.offsetWidth/3;caretPos=offset1"+entry)}else if(isStatic&&(test.fn!==null&&testPos.input!==undefined||test.def==="")){isStatic=false;var mtl=maskTemplate.length;maskTemplate[mtl-1]=maskTemplate[mtl-1]+"";maskTemplate.push(entry)}else maskTemplate.push(entry)}function setCaret(){if(document.activeElement===input){maskTemplate.splice(caretPos.begin,0,caretPos.begin===caretPos.end||caretPos.end>getMaskSet().maskLength?'':'');maskTemplate.splice(caretPos.end+1,0,"")}}if(colorMask!==undefined){var buffer=getBuffer();if(caretPos===undefined){caretPos=caret(input)}else if(caretPos.begin===undefined){caretPos={begin:caretPos,end:caretPos}}if(clear!==true){var lvp=getLastValidPosition();do{if(getMaskSet().validPositions[pos]){testPos=getMaskSet().validPositions[pos];test=testPos.match;ndxIntlzr=testPos.locator.slice();setEntry(buffer[pos])}else{testPos=getTestTemplate(pos,ndxIntlzr,pos-1);test=testPos.match;ndxIntlzr=testPos.locator.slice();if(opts.jitMasking===false||pospos){setEntry(getPlaceholder(pos,test))}else isStatic=false}pos++}while((maxLength===undefined||pospos||isStatic);if(isStatic)setEntry();setCaret()}var template=colorMask.getElementsByTagName("div")[0];template.innerHTML=maskTemplate.join("");input.inputmask.positionColorMask(input,template)}}function mask(elem){function isElementTypeSupported(input,opts){function patchValueProperty(npt){var valueGet;var valueSet;function patchValhook(type){if($.valHooks&&($.valHooks[type]===undefined||$.valHooks[type].inputmaskpatch!==true)){var valhookGet=$.valHooks[type]&&$.valHooks[type].get?$.valHooks[type].get:function(elem){return elem.value};var valhookSet=$.valHooks[type]&&$.valHooks[type].set?$.valHooks[type].set:function(elem,value){elem.value=value;return elem};$.valHooks[type]={get:function get(elem){if(elem.inputmask){if(elem.inputmask.opts.autoUnmask){return elem.inputmask.unmaskedvalue()}else{var result=valhookGet(elem);return getLastValidPosition(undefined,undefined,elem.inputmask.maskset.validPositions)!==-1||opts.nullable!==true?result:""}}else return valhookGet(elem)},set:function set(elem,value){var $elem=$(elem),result;result=valhookSet(elem,value);if(elem.inputmask){$elem.trigger("setvalue",[value])}return result},inputmaskpatch:true}}}function getter(){if(this.inputmask){return this.inputmask.opts.autoUnmask?this.inputmask.unmaskedvalue():getLastValidPosition()!==-1||opts.nullable!==true?document.activeElement===this&&opts.clearMaskOnLostFocus?(isRTL?clearOptionalTail(getBuffer().slice()).reverse():clearOptionalTail(getBuffer().slice())).join(""):valueGet.call(this):""}else return valueGet.call(this)}function setter(value){valueSet.call(this,value);if(this.inputmask){$(this).trigger("setvalue",[value])}}function installNativeValueSetFallback(npt){EventRuler.on(npt,"mouseenter",function(event){var $input=$(this),input=this,value=input.inputmask._valueGet();if(value!==getBuffer().join("")){$input.trigger("setvalue")}})}if(!npt.inputmask.__valueGet){if(opts.noValuePatching!==true){if(Object.getOwnPropertyDescriptor){if(typeof Object.getPrototypeOf!=="function"){Object.getPrototypeOf=_typeof("test".__proto__)==="object"?function(object){return object.__proto__}:function(object){return object.constructor.prototype}}var valueProperty=Object.getPrototypeOf?Object.getOwnPropertyDescriptor(Object.getPrototypeOf(npt),"value"):undefined;if(valueProperty&&valueProperty.get&&valueProperty.set){valueGet=valueProperty.get;valueSet=valueProperty.set;Object.defineProperty(npt,"value",{get:getter,set:setter,configurable:true})}else if(npt.tagName!=="INPUT"){valueGet=function valueGet(){return this.textContent};valueSet=function valueSet(value){this.textContent=value};Object.defineProperty(npt,"value",{get:getter,set:setter,configurable:true})}}else if(document.__lookupGetter__&&npt.__lookupGetter__("value")){valueGet=npt.__lookupGetter__("value");valueSet=npt.__lookupSetter__("value");npt.__defineGetter__("value",getter);npt.__defineSetter__("value",setter)}npt.inputmask.__valueGet=valueGet;npt.inputmask.__valueSet=valueSet}npt.inputmask._valueGet=function(overruleRTL){return isRTL&&overruleRTL!==true?valueGet.call(this.el).split("").reverse().join(""):valueGet.call(this.el)};npt.inputmask._valueSet=function(value,overruleRTL){valueSet.call(this.el,value===null||value===undefined?"":overruleRTL!==true&&isRTL?value.split("").reverse().join(""):value)};if(valueGet===undefined){valueGet=function valueGet(){return this.value};valueSet=function valueSet(value){this.value=value};patchValhook(npt.type);installNativeValueSetFallback(npt)}}}var elementType=input.getAttribute("type");var isSupported=input.tagName==="INPUT"&&$.inArray(elementType,opts.supportsInputType)!==-1||input.isContentEditable||input.tagName==="TEXTAREA";if(!isSupported){if(input.tagName==="INPUT"){var el=document.createElement("input");el.setAttribute("type",elementType);isSupported=el.type==="text";el=null}else isSupported="partial"}if(isSupported!==false){patchValueProperty(input)}else input.inputmask=undefined;return isSupported}EventRuler.off(elem);var isSupported=isElementTypeSupported(elem,opts);if(isSupported!==false){el=elem;$el=$(el);originalPlaceholder=el.placeholder;maxLength=el!==undefined?el.maxLength:undefined;if(maxLength===-1)maxLength=undefined;if(opts.colorMask===true){initializeColorMask(el)}if(mobile){if("inputMode"in el){el.inputmode=opts.inputmode;el.setAttribute("inputmode",opts.inputmode)}if(opts.disablePredictiveText===true){if("autocorrect"in el){el.autocorrect=false}else{if(opts.colorMask!==true){initializeColorMask(el)}el.type="password"}}}if(isSupported===true){el.setAttribute("im-insert",opts.insertMode);EventRuler.on(el,"submit",EventHandlers.submitEvent);EventRuler.on(el,"reset",EventHandlers.resetEvent);EventRuler.on(el,"blur",EventHandlers.blurEvent);EventRuler.on(el,"focus",EventHandlers.focusEvent);if(opts.colorMask!==true){EventRuler.on(el,"click",EventHandlers.clickEvent);EventRuler.on(el,"mouseleave",EventHandlers.mouseleaveEvent);EventRuler.on(el,"mouseenter",EventHandlers.mouseenterEvent)}EventRuler.on(el,"paste",EventHandlers.pasteEvent);EventRuler.on(el,"cut",EventHandlers.cutEvent);EventRuler.on(el,"complete",opts.oncomplete);EventRuler.on(el,"incomplete",opts.onincomplete);EventRuler.on(el,"cleared",opts.oncleared);if(!mobile&&opts.inputEventOnly!==true){EventRuler.on(el,"keydown",EventHandlers.keydownEvent);EventRuler.on(el,"keypress",EventHandlers.keypressEvent)}else{el.removeAttribute("maxLength")}EventRuler.on(el,"input",EventHandlers.inputFallBackEvent);EventRuler.on(el,"beforeinput",EventHandlers.beforeInputEvent)}EventRuler.on(el,"setvalue",EventHandlers.setValueEvent);undoValue=getBufferTemplate().join("");if(el.inputmask._valueGet(true)!==""||opts.clearMaskOnLostFocus===false||document.activeElement===el){var initialValue=$.isFunction(opts.onBeforeMask)?opts.onBeforeMask.call(inputmask,el.inputmask._valueGet(true),opts)||el.inputmask._valueGet(true):el.inputmask._valueGet(true);if(initialValue!=="")checkVal(el,true,false,initialValue.split(""));var buffer=getBuffer().slice();undoValue=buffer.join("");if(isComplete(buffer)===false){if(opts.clearIncomplete){resetMaskSet()}}if(opts.clearMaskOnLostFocus&&document.activeElement!==el){if(getLastValidPosition()===-1){buffer=[]}else{clearOptionalTail(buffer)}}if(opts.clearMaskOnLostFocus===false||opts.showMaskOnFocus&&document.activeElement===el||el.inputmask._valueGet(true)!=="")writeBuffer(el,buffer);if(document.activeElement===el){caret(el,seekNext(getLastValidPosition()))}}}}var valueBuffer;if(actionObj!==undefined){switch(actionObj.action){case"isComplete":el=actionObj.el;return isComplete(getBuffer());case"unmaskedvalue":if(el===undefined||actionObj.value!==undefined){valueBuffer=actionObj.value;valueBuffer=($.isFunction(opts.onBeforeMask)?opts.onBeforeMask.call(inputmask,valueBuffer,opts)||valueBuffer:valueBuffer).split("");checkVal.call(this,undefined,false,false,valueBuffer);if($.isFunction(opts.onBeforeWrite))opts.onBeforeWrite.call(inputmask,undefined,getBuffer(),0,opts)}return unmaskedvalue(el);case"mask":mask(el);break;case"format":valueBuffer=($.isFunction(opts.onBeforeMask)?opts.onBeforeMask.call(inputmask,actionObj.value,opts)||actionObj.value:actionObj.value).split("");checkVal.call(this,undefined,true,false,valueBuffer);if(actionObj.metadata){return{value:isRTL?getBuffer().slice().reverse().join(""):getBuffer().join(""),metadata:maskScope.call(this,{action:"getmetadata"},maskset,opts)}}return isRTL?getBuffer().slice().reverse().join(""):getBuffer().join("");case"isValid":if(actionObj.value){valueBuffer=actionObj.value.split("");checkVal.call(this,undefined,true,true,valueBuffer)}else{actionObj.value=getBuffer().join("")}var buffer=getBuffer();var rl=determineLastRequiredPosition(),lmib=buffer.length-1;for(;lmib>rl;lmib--){if(isMask(lmib))break}buffer.splice(rl,lmib+1-rl);return isComplete(buffer)&&actionObj.value===getBuffer().join("");case"getemptymask":return getBufferTemplate().join("");case"remove":if(el&&el.inputmask){$.data(el,"_inputmask_opts",null);$el=$(el);el.inputmask._valueSet(opts.autoUnmask?unmaskedvalue(el):el.inputmask._valueGet(true));EventRuler.off(el);if(el.inputmask.colorMask){colorMask=el.inputmask.colorMask;colorMask.removeChild(el);colorMask.parentNode.insertBefore(el,colorMask);colorMask.parentNode.removeChild(colorMask)}var valueProperty;if(Object.getOwnPropertyDescriptor&&Object.getPrototypeOf){valueProperty=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el),"value");if(valueProperty){if(el.inputmask.__valueGet){Object.defineProperty(el,"value",{get:el.inputmask.__valueGet,set:el.inputmask.__valueSet,configurable:true})}}}else if(document.__lookupGetter__&&el.__lookupGetter__("value")){if(el.inputmask.__valueGet){el.__defineGetter__("value",el.inputmask.__valueGet);el.__defineSetter__("value",el.inputmask.__valueSet)}}el.inputmask=undefined}return el;break;case"getmetadata":if($.isArray(maskset.metadata)){var maskTarget=getMaskTemplate(true,0,false).join("");$.each(maskset.metadata,function(ndx,mtdt){if(mtdt.mask===maskTarget){maskTarget=mtdt;return false}});return maskTarget}return maskset.metadata}}}return Inputmask})},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(4)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function($){return $})},function(module,exports){module.exports=jQuery},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};if(true)!(__WEBPACK_AMD_DEFINE_RESULT__=function(){return typeof window!=="undefined"?window:new(eval("require('jsdom').JSDOM"))("").window}.call(exports,__webpack_require__,exports,module),__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__));else{}},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(2)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function(Inputmask){var $=Inputmask.dependencyLib;var formatCode={d:["[1-9]|[12][0-9]|3[01]",Date.prototype.setDate,"day",Date.prototype.getDate],dd:["0[1-9]|[12][0-9]|3[01]",Date.prototype.setDate,"day",function(){return pad(Date.prototype.getDate.call(this),2)}],ddd:[""],dddd:[""],m:["[1-9]|1[012]",Date.prototype.setMonth,"month",function(){return Date.prototype.getMonth.call(this)+1}],mm:["0[1-9]|1[012]",Date.prototype.setMonth,"month",function(){return pad(Date.prototype.getMonth.call(this)+1,2)}],mmm:[""],mmmm:[""],yy:["[0-9]{2}",Date.prototype.setFullYear,"year",function(){return pad(Date.prototype.getFullYear.call(this),2)}],yyyy:["[0-9]{4}",Date.prototype.setFullYear,"year",function(){return pad(Date.prototype.getFullYear.call(this),4)}],h:["[1-9]|1[0-2]",Date.prototype.setHours,"hours",Date.prototype.getHours],hh:["0[1-9]|1[0-2]",Date.prototype.setHours,"hours",function(){return pad(Date.prototype.getHours.call(this),2)}],hhh:["[0-9]+",Date.prototype.setHours,"hours",Date.prototype.getHours],H:["1?[0-9]|2[0-3]",Date.prototype.setHours,"hours",Date.prototype.getHours],HH:["0[0-9]|1[0-9]|2[0-3]",Date.prototype.setHours,"hours",function(){return pad(Date.prototype.getHours.call(this),2)}],HHH:["[0-9]+",Date.prototype.setHours,"hours",Date.prototype.getHours],M:["[1-5]?[0-9]",Date.prototype.setMinutes,"minutes",Date.prototype.getMinutes],MM:["0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]",Date.prototype.setMinutes,"minutes",function(){return pad(Date.prototype.getMinutes.call(this),2)}],ss:["[0-5][0-9]",Date.prototype.setSeconds,"seconds",function(){return pad(Date.prototype.getSeconds.call(this),2)}],l:["[0-9]{3}",Date.prototype.setMilliseconds,"milliseconds",function(){return pad(Date.prototype.getMilliseconds.call(this),3)}],L:["[0-9]{2}",Date.prototype.setMilliseconds,"milliseconds",function(){return pad(Date.prototype.getMilliseconds.call(this),2)}],t:["[ap]"],tt:["[ap]m"],T:["[AP]"],TT:["[AP]M"],Z:[""],o:[""],S:[""]},formatAlias={isoDate:"yyyy-mm-dd",isoTime:"HH:MM:ss",isoDateTime:"yyyy-mm-dd'T'HH:MM:ss",isoUtcDateTime:"UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"};function getTokenizer(opts){if(!opts.tokenizer){var tokens=[];for(var ndx in formatCode){if(tokens.indexOf(ndx[0])===-1)tokens.push(ndx[0])}opts.tokenizer="("+tokens.join("+|")+")+?|.";opts.tokenizer=new RegExp(opts.tokenizer,"g")}return opts.tokenizer}function isValidDate(dateParts,currentResult){return!isFinite(dateParts.rawday)||dateParts.day=="29"&&!isFinite(dateParts.rawyear)||new Date(dateParts.date.getFullYear(),isFinite(dateParts.rawmonth)?dateParts.month:dateParts.date.getMonth()+1,0).getDate()>=dateParts.day?currentResult:false}function isDateInRange(dateParts,opts){var result=true;if(opts.min){if(dateParts["rawyear"]){var rawYear=dateParts["rawyear"].replace(/[^0-9]/g,""),minYear=opts.min.year.substr(0,rawYear.length);result=minYear<=rawYear}if(dateParts["year"]===dateParts["rawyear"]){if(opts.min.date.getTime()===opts.min.date.getTime()){result=opts.min.date.getTime()<=dateParts.date.getTime()}}}if(result&&opts.max&&opts.max.date.getTime()===opts.max.date.getTime()){result=opts.max.date.getTime()>=dateParts.date.getTime()}return result}function parse(format,dateObjValue,opts,raw){var mask="",match;while(match=getTokenizer(opts).exec(format)){if(dateObjValue===undefined){if(formatCode[match[0]]){mask+="("+formatCode[match[0]][0]+")"}else{switch(match[0]){case"[":mask+="(";break;case"]":mask+=")?";break;default:mask+=Inputmask.escapeRegex(match[0])}}}else{if(formatCode[match[0]]){if(raw!==true&&formatCode[match[0]][3]){var getFn=formatCode[match[0]][3];mask+=getFn.call(dateObjValue.date)}else if(formatCode[match[0]][2])mask+=dateObjValue["raw"+formatCode[match[0]][2]];else mask+=match[0]}else mask+=match[0]}}return mask}function pad(val,len){val=String(val);len=len||2;while(val.lengthmax.slice(0,enteredPart.length)?max.slice(enteredPart.length):correctedValue.toString().slice(enteredPart.length))}return correctedValue}function setValue(dateObj,value,opts){dateObj[targetProp]=extendProperty(value);dateObj["raw"+targetProp]=value;if(dateOperation!==undefined)dateOperation.call(dateObj.date,targetProp=="month"?parseInt(dateObj[targetProp])-1:dateObj[targetProp])}if(typeof mask==="string"){while(match=getTokenizer(opts).exec(format)){var value=mask.slice(0,match[0].length);if(formatCode.hasOwnProperty(match[0])){targetValidator=formatCode[match[0]][0];targetProp=formatCode[match[0]][2];dateOperation=formatCode[match[0]][1];setValue(dateObj,value,opts)}mask=mask.slice(value.length)}return dateObj}else if(mask&&(typeof mask==="undefined"?"undefined":_typeof(mask))==="object"&&mask.hasOwnProperty("date")){return mask}return undefined}Inputmask.extendAliases({datetime:{mask:function mask(opts){formatCode.S=opts.i18n.ordinalSuffix.join("|");opts.inputFormat=formatAlias[opts.inputFormat]||opts.inputFormat;opts.displayFormat=formatAlias[opts.displayFormat]||opts.displayFormat||opts.inputFormat;opts.outputFormat=formatAlias[opts.outputFormat]||opts.outputFormat||opts.inputFormat;opts.placeholder=opts.placeholder!==""?opts.placeholder:opts.inputFormat.replace(/[\[\]]/,"");opts.regex=parse(opts.inputFormat,undefined,opts);return null},placeholder:"",inputFormat:"isoDateTime",displayFormat:undefined,outputFormat:undefined,min:null,max:null,i18n:{dayNames:["Mon","Tue","Wed","Thu","Fri","Sat","Sun","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],monthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","January","February","March","April","May","June","July","August","September","October","November","December"],ordinalSuffix:["st","nd","rd","th"]},postValidation:function postValidation(buffer,pos,currentResult,opts){opts.min=analyseMask(opts.min,opts.inputFormat,opts);opts.max=analyseMask(opts.max,opts.inputFormat,opts);var result=currentResult,dateParts=analyseMask(buffer.join(""),opts.inputFormat,opts);if(result&&dateParts.date.getTime()===dateParts.date.getTime()){result=isValidDate(dateParts,result);result=result&&isDateInRange(dateParts,opts)}if(pos&&result&¤tResult.pos!==pos){return{buffer:parse(opts.inputFormat,dateParts,opts),refreshFromBuffer:{start:pos,end:currentResult.pos}}}return result},onKeyDown:function onKeyDown(e,buffer,caretPos,opts){var input=this;if(e.ctrlKey&&e.keyCode===Inputmask.keyCode.RIGHT){var today=new Date,match,date="";while(match=getTokenizer(opts).exec(opts.inputFormat)){if(match[0].charAt(0)==="d"){date+=pad(today.getDate(),match[0].length)}else if(match[0].charAt(0)==="m"){date+=pad(today.getMonth()+1,match[0].length)}else if(match[0]==="yyyy"){date+=today.getFullYear().toString()}else if(match[0].charAt(0)==="y"){date+=pad(today.getYear(),match[0].length)}}input.inputmask._valueSet(date);$(input).trigger("setvalue")}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return parse(opts.outputFormat,analyseMask(maskedValue,opts.inputFormat,opts),opts,true)},casing:function casing(elem,test,pos,validPositions){if(test.nativeDef.indexOf("[ap]")==0)return elem.toLowerCase();if(test.nativeDef.indexOf("[AP]")==0)return elem.toUpperCase();return elem},insertMode:false,shiftPositions:false}});return Inputmask})},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(2)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function(Inputmask){var $=Inputmask.dependencyLib;function autoEscape(txt,opts){var escapedTxt="";for(var i=0;i0){var radixPosition=$.inArray(opts.radixPoint,buffer);if(radixPosition===-1){buffer.push(opts.radixPoint);radixPosition=buffer.length-1}for(var i=1;i<=digits;i++){buffer[radixPosition+i]=buffer[radixPosition+i]||"0"}}return buffer}Inputmask.extendAliases({numeric:{mask:function mask(opts){if(opts.repeat!==0&&isNaN(opts.integerDigits)){opts.integerDigits=opts.repeat}opts.repeat=0;if(opts.groupSeparator===opts.radixPoint&&opts.digits&&opts.digits!=="0"){if(opts.radixPoint==="."){opts.groupSeparator=","}else if(opts.radixPoint===","){opts.groupSeparator="."}else opts.groupSeparator=""}if(opts.groupSeparator===" "){opts.skipOptionalPartCharacter=undefined}opts.autoGroup=opts.autoGroup&&opts.groupSeparator!=="";if(opts.autoGroup){if(typeof opts.groupSize=="string"&&isFinite(opts.groupSize))opts.groupSize=parseInt(opts.groupSize);if(isFinite(opts.integerDigits)){var seps=Math.floor(opts.integerDigits/opts.groupSize);var mod=opts.integerDigits%opts.groupSize;opts.integerDigits=parseInt(opts.integerDigits)+(mod===0?seps-1:seps);if(opts.integerDigits<1){opts.integerDigits="*"}}}if(opts.placeholder.length>1){opts.placeholder=opts.placeholder.charAt(0)}if(opts.positionCaretOnClick==="radixFocus"&&opts.placeholder===""&&opts.integerOptional===false){opts.positionCaretOnClick="lvp"}opts.definitions[";"]=opts.definitions["~"];opts.definitions[";"].definitionSymbol="~";if(opts.numericInput===true){opts.positionCaretOnClick=opts.positionCaretOnClick==="radixFocus"?"lvp":opts.positionCaretOnClick;opts.digitsOptional=false;if(isNaN(opts.digits))opts.digits=2;opts.decimalProtect=false}var mask="[+]";mask+=autoEscape(opts.prefix,opts);if(opts.integerOptional===true){mask+="~{1,"+opts.integerDigits+"}"}else mask+="~{"+opts.integerDigits+"}";if(opts.digits!==undefined){var radixDef=opts.decimalProtect?":":opts.radixPoint;var dq=opts.digits.toString().split(",");if(isFinite(dq[0])&&dq[1]&&isFinite(dq[1])){mask+=radixDef+";{"+opts.digits+"}"}else if(isNaN(opts.digits)||parseInt(opts.digits)>0){if(opts.digitsOptional){mask+="["+radixDef+";{1,"+opts.digits+"}]"}else mask+=radixDef+";{"+opts.digits+"}"}}mask+=autoEscape(opts.suffix,opts);mask+="[-]";opts.greedy=false;return mask},placeholder:"",greedy:false,digits:"*",digitsOptional:true,enforceDigitsOnBlur:false,radixPoint:".",positionCaretOnClick:"radixFocus",groupSize:3,groupSeparator:"",autoGroup:false,allowMinus:true,negationSymbol:{front:"-",back:""},integerDigits:"+",integerOptional:true,prefix:"",suffix:"",rightAlign:true,decimalProtect:true,min:null,max:null,step:1,insertMode:true,autoUnmask:false,unmaskAsNumber:false,inputType:"text",inputmode:"numeric",preValidation:function preValidation(buffer,pos,c,isSelection,opts,maskset){if(c==="-"||c===opts.negationSymbol.front){if(opts.allowMinus!==true)return false;opts.isNegative=opts.isNegative===undefined?true:!opts.isNegative;if(buffer.join("")==="")return true;return{caret:maskset.validPositions[pos]?pos:undefined,dopost:true}}if(isSelection===false&&c===opts.radixPoint&&opts.digits!==undefined&&(isNaN(opts.digits)||parseInt(opts.digits)>0)){var radixPos=$.inArray(opts.radixPoint,buffer);if(radixPos!==-1&&maskset.validPositions[radixPos]!==undefined){if(opts.numericInput===true){return pos===radixPos}return{caret:radixPos+1}}}return true},postValidation:function postValidation(buffer,pos,currentResult,opts){function buildPostMask(buffer,opts){var postMask="";postMask+="("+opts.groupSeparator+"*{"+opts.groupSize+"}){*}";if(opts.radixPoint!==""){var radixSplit=buffer.join("").split(opts.radixPoint);if(radixSplit[1]){postMask+=opts.radixPoint+"*{"+radixSplit[1].match(/^\d*\??\d*/)[0].length+"}"}}return postMask}var suffix=opts.suffix.split(""),prefix=opts.prefix.split("");if(currentResult.pos===undefined&¤tResult.caret!==undefined&¤tResult.dopost!==true)return currentResult;var caretPos=currentResult.caret!==undefined?currentResult.caret:currentResult.pos;var maskedValue=buffer.slice();if(opts.numericInput){caretPos=maskedValue.length-caretPos-1;maskedValue=maskedValue.reverse()}var charAtPos=maskedValue[caretPos];if(charAtPos===opts.groupSeparator){caretPos+=1;charAtPos=maskedValue[caretPos]}if(caretPos===maskedValue.length-opts.suffix.length-1&&charAtPos===opts.radixPoint)return currentResult;if(charAtPos!==undefined){if(charAtPos!==opts.radixPoint&&charAtPos!==opts.negationSymbol.front&&charAtPos!==opts.negationSymbol.back){maskedValue[caretPos]="?";if(opts.prefix.length>0&&caretPos>=(opts.isNegative===false?1:0)&&caretPos0&&caretPos>=maskedValue.length-opts.suffix.length-(opts.isNegative===false?1:0)){suffix[caretPos-(maskedValue.length-opts.suffix.length-(opts.isNegative===false?1:0))]="?"}}}prefix=prefix.join("");suffix=suffix.join("");var processValue=maskedValue.join("").replace(prefix,"");processValue=processValue.replace(suffix,"");processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),"");processValue=processValue.replace(new RegExp("[-"+Inputmask.escapeRegex(opts.negationSymbol.front)+"]","g"),"");processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.negationSymbol.back)+"$"),"");if(isNaN(opts.placeholder)){processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.placeholder),"g"),"")}if(processValue.length>1&&processValue.indexOf(opts.radixPoint)!==1){if(charAtPos==="0"){processValue=processValue.replace(/^\?/g,"")}processValue=processValue.replace(/^0/g,"")}if(processValue.charAt(0)===opts.radixPoint&&opts.radixPoint!==""&&opts.numericInput!==true){processValue="0"+processValue}if(processValue!==""){processValue=processValue.split("");if((!opts.digitsOptional||opts.enforceDigitsOnBlur&¤tResult.event==="blur")&&isFinite(opts.digits)){var radixPosition=$.inArray(opts.radixPoint,processValue);var rpb=$.inArray(opts.radixPoint,maskedValue);if(radixPosition===-1){processValue.push(opts.radixPoint);radixPosition=processValue.length-1}for(var i=1;i<=opts.digits;i++){if((!opts.digitsOptional||opts.enforceDigitsOnBlur&¤tResult.event==="blur")&&(processValue[radixPosition+i]===undefined||processValue[radixPosition+i]===opts.placeholder.charAt(0))){processValue[radixPosition+i]=currentResult.placeholder||opts.placeholder.charAt(0)}else if(rpb!==-1&&maskedValue[rpb+i]!==undefined){processValue[radixPosition+i]=processValue[radixPosition+i]||maskedValue[rpb+i]}}}if(opts.autoGroup===true&&opts.groupSeparator!==""&&(charAtPos!==opts.radixPoint||currentResult.pos!==undefined||currentResult.dopost)){var addRadix=processValue[processValue.length-1]===opts.radixPoint&¤tResult.c===opts.radixPoint;processValue=Inputmask(buildPostMask(processValue,opts),{numericInput:true,jitMasking:true,definitions:{"*":{validator:"[0-9?]",cardinality:1}}}).format(processValue.join(""));if(addRadix)processValue+=opts.radixPoint;if(processValue.charAt(0)===opts.groupSeparator){processValue.substr(1)}}else processValue=processValue.join("")}if(opts.isNegative&¤tResult.event==="blur"){opts.isNegative=processValue!=="0"}processValue=prefix+processValue;processValue+=suffix;if(opts.isNegative){processValue=opts.negationSymbol.front+processValue;processValue+=opts.negationSymbol.back}processValue=processValue.split("");if(charAtPos!==undefined){if(charAtPos!==opts.radixPoint&&charAtPos!==opts.negationSymbol.front&&charAtPos!==opts.negationSymbol.back){caretPos=$.inArray("?",processValue);if(caretPos>-1){processValue[caretPos]=charAtPos}else caretPos=currentResult.caret||0}else if(charAtPos===opts.radixPoint||charAtPos===opts.negationSymbol.front||charAtPos===opts.negationSymbol.back){var newCaretPos=$.inArray(charAtPos,processValue);if(newCaretPos!==-1)caretPos=newCaretPos}}if(opts.numericInput){caretPos=processValue.length-caretPos-1;processValue=processValue.reverse()}var rslt={caret:(charAtPos===undefined||currentResult.pos!==undefined)&&caretPos!==undefined?caretPos+(opts.numericInput?-1:1):caretPos,buffer:processValue,refreshFromBuffer:currentResult.dopost||buffer.join("")!==processValue.join("")};return rslt.refreshFromBuffer?rslt:currentResult},onBeforeWrite:function onBeforeWrite(e,buffer,caretPos,opts){function parseMinMaxOptions(opts){if(opts.parseMinMaxOptions===undefined){if(opts.min!==null){opts.min=opts.min.toString().replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),"");if(opts.radixPoint===",")opts.min=opts.min.replace(opts.radixPoint,".");opts.min=isFinite(opts.min)?parseFloat(opts.min):NaN;if(isNaN(opts.min))opts.min=Number.MIN_VALUE}if(opts.max!==null){opts.max=opts.max.toString().replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),"");if(opts.radixPoint===",")opts.max=opts.max.replace(opts.radixPoint,".");opts.max=isFinite(opts.max)?parseFloat(opts.max):NaN;if(isNaN(opts.max))opts.max=Number.MAX_VALUE}opts.parseMinMaxOptions="done"}}if(e){switch(e.type){case"keydown":return opts.postValidation(buffer,caretPos,{caret:caretPos,dopost:true},opts);case"blur":case"checkval":var unmasked;parseMinMaxOptions(opts);if(opts.min!==null||opts.max!==null){unmasked=opts.onUnMask(buffer.join(""),undefined,$.extend({},opts,{unmaskAsNumber:true}));if(opts.min!==null&&unmaskedopts.max){opts.isNegative=opts.max<0;return opts.postValidation(opts.max.toString().replace(".",opts.radixPoint).split(""),caretPos,{caret:caretPos,dopost:true,placeholder:"0"},opts)}}return opts.postValidation(buffer,caretPos,{caret:caretPos,placeholder:"0",event:"blur"},opts);case"_checkval":return{caret:caretPos};default:break}}},regex:{integerPart:function integerPart(opts,emptyCheck){return emptyCheck?new RegExp("["+Inputmask.escapeRegex(opts.negationSymbol.front)+"+]?"):new RegExp("["+Inputmask.escapeRegex(opts.negationSymbol.front)+"+]?\\d+")},integerNPart:function integerNPart(opts){return new RegExp("[\\d"+Inputmask.escapeRegex(opts.groupSeparator)+Inputmask.escapeRegex(opts.placeholder.charAt(0))+"]+")}},definitions:{"~":{validator:function validator(chrs,maskset,pos,strict,opts,isSelection){var isValid,l;if(chrs==="k"||chrs==="m"){isValid={insert:[],c:0};for(var i=0,l=chrs==="k"?2:5;i1){pvRadixSplit[1]=pvRadixSplit[1].replace(/0/g,opts.placeholder.charAt(0))}if(pvRadixSplit[0]==="0"){pvRadixSplit[0]=pvRadixSplit[0].replace(/0/g,opts.placeholder.charAt(0))}processValue=pvRadixSplit[0]+opts.radixPoint+pvRadixSplit[1]||"";var bufferTemplate=maskset._buffer.join("");if(processValue===opts.radixPoint){processValue=bufferTemplate}while(processValue.match(Inputmask.escapeRegex(bufferTemplate)+"$")===null){bufferTemplate=bufferTemplate.slice(1)}processValue=processValue.replace(bufferTemplate,"");processValue=processValue.split("");if(processValue[pos]===undefined){isValid={pos:pos,remove:pos}}else{isValid={pos:pos}}}}else if(!strict&&chrs===opts.radixPoint&&maskset.validPositions[pos-1]===undefined){isValid={insert:{pos:pos,c:0},pos:pos+1}}return isValid},cardinality:1},"+":{validator:function validator(chrs,maskset,pos,strict,opts){return opts.allowMinus&&(chrs==="-"||chrs===opts.negationSymbol.front)},cardinality:1,placeholder:""},"-":{validator:function validator(chrs,maskset,pos,strict,opts){return opts.allowMinus&&chrs===opts.negationSymbol.back},cardinality:1,placeholder:""},":":{validator:function validator(chrs,maskset,pos,strict,opts){var radix="["+Inputmask.escapeRegex(opts.radixPoint)+"]";var isValid=new RegExp(radix).test(chrs);if(isValid&&maskset.validPositions[pos]&&maskset.validPositions[pos].match.placeholder===opts.radixPoint){isValid={caret:pos+1}}return isValid},cardinality:1,placeholder:function placeholder(opts){return opts.radixPoint}}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){if(unmaskedValue===""&&opts.nullable===true){return unmaskedValue}var processValue=maskedValue.replace(opts.prefix,"");processValue=processValue.replace(opts.suffix,"");processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),"");if(opts.placeholder.charAt(0)!==""){processValue=processValue.replace(new RegExp(opts.placeholder.charAt(0),"g"),"0")}if(opts.unmaskAsNumber){if(opts.radixPoint!==""&&processValue.indexOf(opts.radixPoint)!==-1)processValue=processValue.replace(Inputmask.escapeRegex.call(this,opts.radixPoint),".");processValue=processValue.replace(new RegExp("^"+Inputmask.escapeRegex(opts.negationSymbol.front)),"-");processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.negationSymbol.back)+"$"),"");return Number(processValue)}return processValue},isComplete:function isComplete(buffer,opts){var maskedValue=(opts.numericInput?buffer.slice().reverse():buffer).join("");maskedValue=maskedValue.replace(new RegExp("^"+Inputmask.escapeRegex(opts.negationSymbol.front)),"-");maskedValue=maskedValue.replace(new RegExp(Inputmask.escapeRegex(opts.negationSymbol.back)+"$"),"");maskedValue=maskedValue.replace(opts.prefix,"");maskedValue=maskedValue.replace(opts.suffix,"");maskedValue=maskedValue.replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator)+"([0-9]{3})","g"),"$1");if(opts.radixPoint===",")maskedValue=maskedValue.replace(Inputmask.escapeRegex(opts.radixPoint),".");return isFinite(maskedValue)},onBeforeMask:function onBeforeMask(initialValue,opts){opts.isNegative=undefined;var radixPoint=opts.radixPoint||",";if((typeof initialValue=="number"||opts.inputType==="number")&&radixPoint!==""){initialValue=initialValue.toString().replace(".",radixPoint)}var valueParts=initialValue.split(radixPoint),integerPart=valueParts[0].replace(/[^\-0-9]/g,""),decimalPart=valueParts.length>1?valueParts[1].replace(/[^0-9]/g,""):"";initialValue=integerPart+(decimalPart!==""?radixPoint+decimalPart:decimalPart);var digits=0;if(radixPoint!==""){digits=decimalPart.length;if(decimalPart!==""){var digitsFactor=Math.pow(10,digits||1);if(isFinite(opts.digits)){digits=parseInt(opts.digits);digitsFactor=Math.pow(10,digits)}initialValue=initialValue.replace(Inputmask.escapeRegex(radixPoint),".");if(isFinite(initialValue))initialValue=Math.round(parseFloat(initialValue)*digitsFactor)/digitsFactor;initialValue=initialValue.toString().replace(".",radixPoint)}}if(opts.digits===0&&initialValue.indexOf(Inputmask.escapeRegex(radixPoint))!==-1){initialValue=initialValue.substring(0,initialValue.indexOf(Inputmask.escapeRegex(radixPoint)))}return alignDigits(initialValue.toString().split(""),digits,opts).join("")},onKeyDown:function onKeyDown(e,buffer,caretPos,opts){var $input=$(this);if(e.ctrlKey){switch(e.keyCode){case Inputmask.keyCode.UP:$input.val(parseFloat(this.inputmask.unmaskedvalue())+parseInt(opts.step));$input.trigger("setvalue");break;case Inputmask.keyCode.DOWN:$input.val(parseFloat(this.inputmask.unmaskedvalue())-parseInt(opts.step));$input.trigger("setvalue");break}}}},currency:{prefix:"$ ",groupSeparator:",",alias:"numeric",placeholder:"0",autoGroup:true,digits:2,digitsOptional:false,clearMaskOnLostFocus:false},decimal:{alias:"numeric"},integer:{alias:"numeric",digits:0,radixPoint:""},percentage:{alias:"numeric",digits:2,digitsOptional:true,radixPoint:".",placeholder:"0",autoGroup:false,min:0,max:100,suffix:" %",allowMinus:false}});return Inputmask})},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};(function(factory){if(true){!(__WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(4),__webpack_require__(2)],__WEBPACK_AMD_DEFINE_FACTORY__=factory,__WEBPACK_AMD_DEFINE_RESULT__=typeof __WEBPACK_AMD_DEFINE_FACTORY__==="function"?__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__):__WEBPACK_AMD_DEFINE_FACTORY__,__WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__))}else{}})(function($,Inputmask){if($.fn.inputmask===undefined){$.fn.inputmask=function(fn,options){var nptmask,input=this[0];if(options===undefined)options={};if(typeof fn==="string"){switch(fn){case"unmaskedvalue":return input&&input.inputmask?input.inputmask.unmaskedvalue():$(input).val();case"remove":return this.each(function(){if(this.inputmask)this.inputmask.remove()});case"getemptymask":return input&&input.inputmask?input.inputmask.getemptymask():"";case"hasMaskedValue":return input&&input.inputmask?input.inputmask.hasMaskedValue():false;case"isComplete":return input&&input.inputmask?input.inputmask.isComplete():true;case"getmetadata":return input&&input.inputmask?input.inputmask.getmetadata():undefined;case"setvalue":Inputmask.setValue(input,options);break;case"option":if(typeof options==="string"){if(input&&input.inputmask!==undefined){return input.inputmask.option(options)}}else{return this.each(function(){if(this.inputmask!==undefined){return this.inputmask.option(options)}})}break;default:options.alias=fn;nptmask=new Inputmask(options);return this.each(function(){nptmask.mask(this)})}}else if(Array.isArray(fn)){options.alias=fn;nptmask=new Inputmask(options);return this.each(function(){nptmask.mask(this)})}else if((typeof fn==="undefined"?"undefined":_typeof(fn))=="object"){nptmask=new Inputmask(fn);if(fn.mask===undefined&&fn.alias===undefined){return this.each(function(){if(this.inputmask!==undefined){return this.inputmask.option(fn)}else nptmask.mask(this)})}else{return this.each(function(){nptmask.mask(this)})}}else if(fn===undefined){return this.each(function(){nptmask=new Inputmask(options);nptmask.mask(this)})}}}return $.fn.inputmask})}]); \ No newline at end of file diff --git a/public/js/vendor/jquery.min.js b/public/js/vendor/jquery.min.js new file mode 100644 index 0000000..a1c07fd --- /dev/null +++ b/public/js/vendor/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 01){for(var r=0;r=i&&o>=t};break;case"bottom":h=function(t,e,n,i,o){return n>=i&&o>=n};break;case"middle":h=function(t,e,n,i,o){return e>=i&&o>=e};break;case"top-only":h=function(t,e,n,i,o){return i>=t&&n>=i};break;case"bottom-only":h=function(t,e,n,i,o){return n>=o&&o>=t};break;default:case"default":h=function(t,e,n,i,o){return n>=i&&o>=t}}return c=function(t){var i,o,l,s,r,a,u=this.state,h=!1,c=this.$element.offset();i=n.height(),o=t+i/2,l=t+i,s=this.$element.outerHeight(),r=c.top+e(this.options.top,s,i),a=c.top+s-e(this.options.bottom,s,i),h=this.test(t,o,l,r,a),h!=u&&(this.state=h,h?this.options.enter&&this.options.enter.apply(this.element):this.options.leave&&this.options.leave.apply(this.element)),this.options.scroll&&this.options.scroll.apply(this.element,[(o-r)/(a-r)])},p={id:a,options:u,test:h,handler:c,state:null,element:this,$element:s,timeoutId:null},o[a]=p,s.data("_scrollexId",p.id),p.options.initialize&&p.options.initialize.apply(this),s},jQuery.fn.unscrollex=function(){var e=t(this);if(0==this.length)return e;if(this.length>1){for(var n=0;n1){for(o=0;o ul').dropotron({ + alignment: 'right', + hideDelay: 350 + }); + + // Nav. + + // Title Bar. + $( + '
' + + '' + + '' + $('#logo').html() + '' + + '
' + ) + .appendTo($body); + + // Panel. + $( + '' + ) + .appendTo($body) + .panel({ + delay: 500, + hideOnClick: true, + hideOnSwipe: true, + resetScroll: true, + resetForms: true, + side: 'left', + target: $body, + visibleClass: 'navPanel-visible' + }); + + // Parallax. + // Disabled on IE (choppy scrolling) and mobile platforms (poor performance). + if (browser.name == 'ie' + || browser.mobile) { + + $.fn._parallax = function() { + + return $(this); + + }; + + } + else { + + $.fn._parallax = function() { + + $(this).each(function() { + + var $this = $(this), + on, off; + + on = function() { + + $this + .css('background-position', 'center 0px'); + + $window + .on('scroll._parallax', function() { + + var pos = parseInt($window.scrollTop()) - parseInt($this.position().top); + + $this.css('background-position', 'center ' + (pos * -0.15) + 'px'); + + }); + + }; + + off = function() { + + $this + .css('background-position', ''); + + $window + .off('scroll._parallax'); + + }; + + breakpoints.on('<=medium', off); + breakpoints.on('>medium', on); + + }); + + return $(this); + + }; + + $window + .on('load resize', function() { + $window.trigger('scroll'); + }); + + } + + // Spotlights. + var $spotlights = $('.spotlight'); + + $spotlights + ._parallax() + .each(function() { + + var $this = $(this), + on, off; + + on = function() { + + var top, bottom, mode; + + // Use main 's src as this spotlight's background. + $this.css('background-image', 'url("' + $this.find('.image.main > img').attr('src') + '")'); + + // Side-specific scrollex tweaks. + if ($this.hasClass('top')) { + + mode = 'top'; + top = '-20%'; + bottom = 0; + + } + else if ($this.hasClass('bottom')) { + + mode = 'bottom-only'; + top = 0; + bottom = '20%'; + + } + else { + + mode = 'middle'; + top = 0; + bottom = 0; + + } + + // Add scrollex. + $this.scrollex({ + mode: mode, + top: top, + bottom: bottom, + initialize: function(t) { $this.addClass('inactive'); }, + terminate: function(t) { $this.removeClass('inactive'); }, + enter: function(t) { $this.removeClass('inactive'); }, + + // Uncomment the line below to "rewind" when this spotlight scrolls out of view. + + //leave: function(t) { $this.addClass('inactive'); }, + + }); + + }; + + off = function() { + + // Clear spotlight's background. + $this.css('background-image', ''); + + // Remove scrollex. + $this.unscrollex(); + + }; + + breakpoints.on('<=medium', off); + breakpoints.on('>medium', on); + + }); + + // Wrappers. + var $wrappers = $('.wrapper'); + + $wrappers + .each(function() { + + var $this = $(this), + on, off; + + on = function() { + + $this.scrollex({ + top: 250, + bottom: 0, + initialize: function(t) { $this.addClass('inactive'); }, + terminate: function(t) { $this.removeClass('inactive'); }, + enter: function(t) { $this.removeClass('inactive'); }, + + // Uncomment the line below to "rewind" when this wrapper scrolls out of view. + + //leave: function(t) { $this.addClass('inactive'); }, + + }); + + }; + + off = function() { + $this.unscrollex(); + }; + + breakpoints.on('<=medium', off); + breakpoints.on('>medium', on); + + }); + + // Banner. + // var $banner = $('#banner'); + + // $banner + // ._parallax(); + +})(jQuery); \ No newline at end of file diff --git a/public/js/vendor/util.js b/public/js/vendor/util.js new file mode 100644 index 0000000..ecf7b37 --- /dev/null +++ b/public/js/vendor/util.js @@ -0,0 +1,587 @@ +(function($) { + + /** + * Generate an indented list of links from a nav. Meant for use with panel(). + * @return {jQuery} jQuery object. + */ + $.fn.navList = function() { + + var $this = $(this); + $a = $this.find('a'), + b = []; + + $a.each(function() { + + var $this = $(this), + indent = Math.max(0, $this.parents('li').length - 1), + href = $this.attr('href'), + target = $this.attr('target'); + + b.push( + '' + + '' + + $this.text() + + '' + ); + + }); + + return b.join(''); + + }; + + /** + * Panel-ify an element. + * @param {object} userConfig User config. + * @return {jQuery} jQuery object. + */ + $.fn.panel = function(userConfig) { + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).panel(userConfig); + + return $this; + + } + + // Vars. + var $this = $(this), + $body = $('body'), + $window = $(window), + id = $this.attr('id'), + config; + + // Config. + config = $.extend({ + + // Delay. + delay: 0, + + // Hide panel on link click. + hideOnClick: false, + + // Hide panel on escape keypress. + hideOnEscape: false, + + // Hide panel on swipe. + hideOnSwipe: false, + + // Reset scroll position on hide. + resetScroll: false, + + // Reset forms on hide. + resetForms: false, + + // Side of viewport the panel will appear. + side: null, + + // Target element for "class". + target: $this, + + // Class to toggle. + visibleClass: 'visible' + + }, userConfig); + + // Expand "target" if it's not a jQuery object already. + if (typeof config.target != 'jQuery') + config.target = $(config.target); + + // Panel. + + // Methods. + $this._hide = function(event) { + + // Already hidden? Bail. + if (!config.target.hasClass(config.visibleClass)) + return; + + // If an event was provided, cancel it. + if (event) { + + event.preventDefault(); + event.stopPropagation(); + + } + + // Hide. + config.target.removeClass(config.visibleClass); + + // Post-hide stuff. + window.setTimeout(function() { + + // Reset scroll position. + if (config.resetScroll) + $this.scrollTop(0); + + // Reset forms. + if (config.resetForms) + $this.find('form').each(function() { + this.reset(); + }); + + }, config.delay); + + }; + + // Vendor fixes. + $this + .css('-ms-overflow-style', '-ms-autohiding-scrollbar') + .css('-webkit-overflow-scrolling', 'touch'); + + // Hide on click. + if (config.hideOnClick) { + + $this.find('a') + .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); + + $this + .on('click', 'a', function(event) { + + var $a = $(this), + href = $a.attr('href'), + target = $a.attr('target'); + + if (!href || href == '#' || href == '' || href == '#' + id) + return; + + // Cancel original event. + event.preventDefault(); + event.stopPropagation(); + + // Hide panel. + $this._hide(); + + // Redirect to href. + window.setTimeout(function() { + + if (target == '_blank') + window.open(href); + else + window.location.href = href; + + }, config.delay + 10); + + }); + + } + + // Event: Touch stuff. + $this.on('touchstart', function(event) { + + $this.touchPosX = event.originalEvent.touches[0].pageX; + $this.touchPosY = event.originalEvent.touches[0].pageY; + + }) + + $this.on('touchmove', function(event) { + + if ($this.touchPosX === null + || $this.touchPosY === null) + return; + + var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, + diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, + th = $this.outerHeight(), + ts = ($this.get(0).scrollHeight - $this.scrollTop()); + + // Hide on swipe? + if (config.hideOnSwipe) { + + var result = false, + boundary = 20, + delta = 50; + + switch (config.side) { + + case 'left': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); + break; + + case 'right': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); + break; + + case 'top': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); + break; + + case 'bottom': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); + break; + + default: + break; + + } + + if (result) { + + $this.touchPosX = null; + $this.touchPosY = null; + $this._hide(); + + return false; + + } + + } + + // Prevent vertical scrolling past the top or bottom. + if (($this.scrollTop() < 0 && diffY < 0) + || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { + + event.preventDefault(); + event.stopPropagation(); + + } + + }); + + // Event: Prevent certain events inside the panel from bubbling. + $this.on('click touchend touchstart touchmove', function(event) { + event.stopPropagation(); + }); + + // Event: Hide panel if a child anchor tag pointing to its ID is clicked. + $this.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.removeClass(config.visibleClass); + + }); + + // Body. + + // Event: Hide panel on body click/tap. + $body.on('click touchend', function(event) { + $this._hide(event); + }); + + // Event: Toggle. + $body.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.toggleClass(config.visibleClass); + + }); + + // Window. + + // Event: Hide on ESC. + if (config.hideOnEscape) + $window.on('keydown', function(event) { + + if (event.keyCode == 27) + $this._hide(event); + + }); + + return $this; + + }; + + /** + * Apply "placeholder" attribute polyfill to one or more forms. + * @return {jQuery} jQuery object. + */ + $.fn.placeholder = function() { + + // Browser natively supports placeholders? Bail. + if (typeof (document.createElement('input')).placeholder != 'undefined') + return $(this); + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).placeholder(); + + return $this; + + } + + // Vars. + var $this = $(this); + + // Text, TextArea. + $this.find('input[type=text],textarea') + .each(function() { + + var i = $(this); + + if (i.val() == '' + || i.val() == i.attr('placeholder')) + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('blur', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == '') + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('focus', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == i.attr('placeholder')) + i + .removeClass('polyfill-placeholder') + .val(''); + + }); + + // Password. + $this.find('input[type=password]') + .each(function() { + + var i = $(this); + var x = $( + $('
') + .append(i.clone()) + .remove() + .html() + .replace(/type="password"/i, 'type="text"') + .replace(/type=password/i, 'type=text') + ); + + if (i.attr('id') != '') + x.attr('id', i.attr('id') + '-polyfill-field'); + + if (i.attr('name') != '') + x.attr('name', i.attr('name') + '-polyfill-field'); + + x.addClass('polyfill-placeholder') + .val(x.attr('placeholder')).insertAfter(i); + + if (i.val() == '') + i.hide(); + else + x.hide(); + + i + .on('blur', function(event) { + + event.preventDefault(); + + var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + + i.hide(); + x.show(); + + } + + }); + + x + .on('focus', function(event) { + + event.preventDefault(); + + var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); + + x.hide(); + + i + .show() + .focus(); + + }) + .on('keypress', function(event) { + + event.preventDefault(); + x.val(''); + + }); + + }); + + // Events. + $this + .on('submit', function() { + + $this.find('input[type=text],input[type=password],textarea') + .each(function(event) { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + i.attr('name', ''); + + if (i.val() == i.attr('placeholder')) { + + i.removeClass('polyfill-placeholder'); + i.val(''); + + } + + }); + + }) + .on('reset', function(event) { + + event.preventDefault(); + + $this.find('select') + .val($('option:first').val()); + + $this.find('input,textarea') + .each(function() { + + var i = $(this), + x; + + i.removeClass('polyfill-placeholder'); + + switch (this.type) { + + case 'submit': + case 'reset': + break; + + case 'password': + i.val(i.attr('defaultValue')); + + x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + i.hide(); + x.show(); + } + else { + i.show(); + x.hide(); + } + + break; + + case 'checkbox': + case 'radio': + i.attr('checked', i.attr('defaultValue')); + break; + + case 'text': + case 'textarea': + i.val(i.attr('defaultValue')); + + if (i.val() == '') { + i.addClass('polyfill-placeholder'); + i.val(i.attr('placeholder')); + } + + break; + + default: + i.val(i.attr('defaultValue')); + break; + + } + }); + + }); + + return $this; + + }; + + /** + * Moves elements to/from the first positions of their respective parents. + * @param {jQuery} $elements Elements (or selector) to move. + * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. + */ + $.prioritize = function($elements, condition) { + + var key = '__prioritize'; + + // Expand $elements if it's not already a jQuery object. + if (typeof $elements != 'jQuery') + $elements = $($elements); + + // Step through elements. + $elements.each(function() { + + var $e = $(this), $p, + $parent = $e.parent(); + + // No parent? Bail. + if ($parent.length == 0) + return; + + // Not moved? Move it. + if (!$e.data(key)) { + + // Condition is false? Bail. + if (!condition) + return; + + // Get placeholder (which will serve as our point of reference for when this element needs to move back). + $p = $e.prev(); + + // Couldn't find anything? Means this element's already at the top, so bail. + if ($p.length == 0) + return; + + // Move element to top of parent. + $e.prependTo($parent); + + // Mark element as moved. + $e.data(key, $p); + + } + + // Moved already? + else { + + // Condition is true? Bail. + if (condition) + return; + + $p = $e.data(key); + + // Move element back to its original location (using our placeholder). + $e.insertAfter($p); + + // Unmark element as moved. + $e.removeData(key); + + } + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json new file mode 100644 index 0000000..7b3694e --- /dev/null +++ b/public/mix-manifest.json @@ -0,0 +1,6 @@ +{ + "/js/app.js": "/js/app.js", + "/css/app.css": "/css/app.css", + "/css/vendor/datatable.css": "/css/vendor/datatable.css", + "/vendor/industry/css/main.css": "/vendor/industry/css/main.css" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/vendor/datatables/de_DE.json b/public/vendor/datatables/de_DE.json new file mode 100644 index 0000000..d55e754 --- /dev/null +++ b/public/vendor/datatables/de_DE.json @@ -0,0 +1,41 @@ +{ + "sEmptyTable": "Keine Daten in der Tabelle vorhanden", + "sInfo": "_START_ bis _END_ von _TOTAL_ Einträgen", + "sInfoEmpty": "Keine Daten vorhanden", + "sInfoFiltered": "(gefiltert von _MAX_ Einträgen)", + "sInfoPostFix": "", + "sInfoThousands": ".", + "sLengthMenu": "_MENU_ Einträge anzeigen", + "sLoadingRecords": "Wird geladen ..", + "sProcessing": "Bitte warten ..", + "sSearch": "Suchen", + "sZeroRecords": "Keine Einträge vorhanden", + "oPaginate": { + "sFirst": "Erste", + "sPrevious": "Zurück", + "sNext": "Nächste", + "sLast": "Letzte" + }, + "oAria": { + "sSortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren", + "sSortDescending": ": aktivieren, um Spalte absteigend zu sortieren" + }, + "select": { + "rows": { + "_": "%d Zeilen ausgewählt", + "0": "", + "1": "1 Zeile ausgewählt" + } + }, + "buttons": { + "print": "Drucken", + "colvis": "Spalten", + "copy": "Kopieren", + "copyTitle": "In Zwischenablage kopieren", + "copyKeys": "Taste ctrl oder \u2318 + C um Tabelle
in Zwischenspeicher zu kopieren.

Um abzubrechen die Nachricht anklicken oder Escape drücken.", + "copySuccess": { + "_": "%d Spalten kopiert", + "1": "1 Spalte kopiert" + } + } +} \ No newline at end of file diff --git a/public/vendor/industry/css/.DS_Store b/public/vendor/industry/css/.DS_Store new file mode 100644 index 0000000..82bf491 Binary files /dev/null and b/public/vendor/industry/css/.DS_Store differ diff --git a/public/vendor/industry/css/animate.min.css b/public/vendor/industry/css/animate.min.css new file mode 100644 index 0000000..b6f6129 --- /dev/null +++ b/public/vendor/industry/css/animate.min.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +/*! + * animate.css -http://daneden.me/animate + * Version - 3.5.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2016 Daniel Eden + */ + +.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/public/vendor/industry/css/bootstrap.css b/public/vendor/industry/css/bootstrap.css new file mode 100644 index 0000000..d10bc18 --- /dev/null +++ b/public/vendor/industry/css/bootstrap.css @@ -0,0 +1,8070 @@ +/*! + * Bootstrap v4.0.0-beta (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + box-shadow: none !important; + } + a:not(.btn) { + text-decoration: underline; + } + abbr[title]::after { + content: " (" attr(title) ")"; + } + pre { + white-space: pre-wrap !important; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + @page { + size: a3; + } + body { + min-width: 992px !important; + } + .container { + min-width: 992px !important; + } + .navbar { + display: none; + } + .badge { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; +} + +@-ms-viewport { + width: device-width; +} + +article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +dfn { + font-style: italic; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; + -webkit-text-decoration-skip: objects; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]):not([tabindex]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg:not(:root) { + overflow: hidden; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: .5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html [type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-family: inherit; + font-weight: 500; + line-height: 1.2; + color: inherit; +} + +h1, .h1 { + font-size: 2.5rem; +} + +h2, .h2 { + font-size: 2rem; +} + +h3, .h3 { + font-size: 1.75rem; +} + +h4, .h4 { + font-size: 1.5rem; +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #6c757d; +} + +.blockquote-footer::before { + content: "\2014 \00A0"; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #6c757d; +} + +code, +kbd, +pre, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +code { + font-size: 87.5%; + color: #e83e8c; + word-break: break-word; +} + +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + font-size: 87.5%; + color: #212529; +} + +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; + max-width: none; +} + +.col-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; +} + +.col-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; +} + +.col-3 { + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; +} + +.col-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; +} + +.col-6 { + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; +} + +.col-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; +} + +.col-9 { + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; +} + +.col-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; +} + +.col-12 { + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + order: -1; +} + +.order-last { + order: 13; +} + +.order-0 { + order: 0; +} + +.order-1 { + order: 1; +} + +.order-2 { + order: 2; +} + +.order-3 { + order: 3; +} + +.order-4 { + order: 4; +} + +.order-5 { + order: 5; +} + +.order-6 { + order: 6; +} + +.order-7 { + order: 7; +} + +.order-8 { + order: 8; +} + +.order-9 { + order: 9; +} + +.order-10 { + order: 10; +} + +.order-11 { + order: 11; +} + +.order-12 { + order: 12; +} + +.offset-1 { + margin-left: 8.33333%; +} + +.offset-2 { + margin-left: 16.66667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333%; +} + +.offset-5 { + margin-left: 41.66667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333%; +} + +.offset-8 { + margin-left: 66.66667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333%; +} + +.offset-11 { + margin-left: 91.66667%; +} + +@media (min-width: 576px) { + .col-sm { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-sm-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-sm-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-sm-3 { + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-sm-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-sm-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-sm-9 { + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-sm-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + order: -1; + } + .order-sm-last { + order: 13; + } + .order-sm-0 { + order: 0; + } + .order-sm-1 { + order: 1; + } + .order-sm-2 { + order: 2; + } + .order-sm-3 { + order: 3; + } + .order-sm-4 { + order: 4; + } + .order-sm-5 { + order: 5; + } + .order-sm-6 { + order: 6; + } + .order-sm-7 { + order: 7; + } + .order-sm-8 { + order: 8; + } + .order-sm-9 { + order: 9; + } + .order-sm-10 { + order: 10; + } + .order-sm-11 { + order: 11; + } + .order-sm-12 { + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.33333%; + } + .offset-sm-2 { + margin-left: 16.66667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333%; + } + .offset-sm-5 { + margin-left: 41.66667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333%; + } + .offset-sm-8 { + margin-left: 66.66667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333%; + } + .offset-sm-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 768px) { + .col-md { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-md-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-md-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-md-3 { + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-md-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-md-6 { + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-md-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-md-9 { + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-md-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + order: -1; + } + .order-md-last { + order: 13; + } + .order-md-0 { + order: 0; + } + .order-md-1 { + order: 1; + } + .order-md-2 { + order: 2; + } + .order-md-3 { + order: 3; + } + .order-md-4 { + order: 4; + } + .order-md-5 { + order: 5; + } + .order-md-6 { + order: 6; + } + .order-md-7 { + order: 7; + } + .order-md-8 { + order: 8; + } + .order-md-9 { + order: 9; + } + .order-md-10 { + order: 10; + } + .order-md-11 { + order: 11; + } + .order-md-12 { + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.33333%; + } + .offset-md-2 { + margin-left: 16.66667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333%; + } + .offset-md-5 { + margin-left: 41.66667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333%; + } + .offset-md-8 { + margin-left: 66.66667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333%; + } + .offset-md-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 992px) { + .col-lg { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-lg-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-lg-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-lg-3 { + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-lg-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-lg-6 { + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-lg-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-lg-9 { + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-lg-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-lg-12 { + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + order: -1; + } + .order-lg-last { + order: 13; + } + .order-lg-0 { + order: 0; + } + .order-lg-1 { + order: 1; + } + .order-lg-2 { + order: 2; + } + .order-lg-3 { + order: 3; + } + .order-lg-4 { + order: 4; + } + .order-lg-5 { + order: 5; + } + .order-lg-6 { + order: 6; + } + .order-lg-7 { + order: 7; + } + .order-lg-8 { + order: 8; + } + .order-lg-9 { + order: 9; + } + .order-lg-10 { + order: 10; + } + .order-lg-11 { + order: 11; + } + .order-lg-12 { + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.33333%; + } + .offset-lg-2 { + margin-left: 16.66667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333%; + } + .offset-lg-5 { + margin-left: 41.66667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333%; + } + .offset-lg-8 { + margin-left: 66.66667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333%; + } + .offset-lg-11 { + margin-left: 91.66667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-xl-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + .col-xl-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + .col-xl-3 { + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + .col-xl-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-xl-6 { + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-xl-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + .col-xl-9 { + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + .col-xl-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + .col-xl-12 { + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + order: -1; + } + .order-xl-last { + order: 13; + } + .order-xl-0 { + order: 0; + } + .order-xl-1 { + order: 1; + } + .order-xl-2 { + order: 2; + } + .order-xl-3 { + order: 3; + } + .order-xl-4 { + order: 4; + } + .order-xl-5 { + order: 5; + } + .order-xl-6 { + order: 6; + } + .order-xl-7 { + order: 7; + } + .order-xl-8 { + order: 8; + } + .order-xl-9 { + order: 9; + } + .order-xl-10 { + order: 10; + } + .order-xl-11 { + order: 11; + } + .order-xl-12 { + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.33333%; + } + .offset-xl-2 { + margin-left: 16.66667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333%; + } + .offset-xl-5 { + margin-left: 41.66667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333%; + } + .offset-xl-8 { + margin-left: 66.66667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333%; + } + .offset-xl-11 { + margin-left: 91.66667%; + } +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +.table tbody + tbody { + border-top: 2px solid #dee2e6; +} + +.table .table { + background-color: #fff; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-hover .table-primary:hover { + background-color: #9fcdff; +} + +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #9fcdff; +} + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-hover .table-secondary:hover { + background-color: #c8cbcf; +} + +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #c8cbcf; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-hover .table-success:hover { + background-color: #b1dfbb; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #b1dfbb; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-hover .table-info:hover { + background-color: #abdde5; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #abdde5; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-hover .table-warning:hover { + background-color: #ffe8a1; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #ffe8a1; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-hover .table-danger:hover { + background-color: #f1b0b7; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #f1b0b7; +} + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-hover .table-light:hover { + background-color: #ececf6; +} + +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #ececf6; +} + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-hover .table-dark:hover { + background-color: #b9bbbe; +} + +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #b9bbbe; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); +} + +.table .thead-dark th { + color: #fff; + background-color: #212529; + border-color: #32383e; +} + +.table .thead-light th { + color: #495057; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.table-dark { + color: #fff; + background-color: #212529; +} + +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #32383e; +} + +.table-dark.table-bordered { + border: 0; +} + +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + background-color: rgba(255, 255, 255, 0.075); +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-sm > .table-bordered { + border: 0; + } +} + +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-md > .table-bordered { + border: 0; + } +} + +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-lg > .table-bordered { + border: 0; + } +} + +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-xl > .table-bordered { + border: 0; + } +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; +} + +.table-responsive > .table-bordered { + border: 0; +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control::placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; +} + +select.form-control:not([size]):not([multiple]) { + height: calc(2.25rem + 2px); +} + +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding-top: 0.375rem; + padding-bottom: 0.375rem; + margin-bottom: 0; + line-height: 1.5; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} + +.form-control-plaintext.form-control-sm, .input-group-sm > .form-control-plaintext.form-control, +.input-group-sm > .input-group-prepend > .form-control-plaintext.input-group-text, +.input-group-sm > .input-group-append > .form-control-plaintext.input-group-text, +.input-group-sm > .input-group-prepend > .form-control-plaintext.btn, +.input-group-sm > .input-group-append > .form-control-plaintext.btn, .form-control-plaintext.form-control-lg, .input-group-lg > .form-control-plaintext.form-control, +.input-group-lg > .input-group-prepend > .form-control-plaintext.input-group-text, +.input-group-lg > .input-group-append > .form-control-plaintext.input-group-text, +.input-group-lg > .input-group-prepend > .form-control-plaintext.btn, +.input-group-lg > .input-group-append > .form-control-plaintext.btn { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm, .input-group-sm > .form-control, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +select.form-control-sm:not([size]):not([multiple]), .input-group-sm > select.form-control:not([size]):not([multiple]), +.input-group-sm > .input-group-prepend > select.input-group-text:not([size]):not([multiple]), +.input-group-sm > .input-group-append > select.input-group-text:not([size]):not([multiple]), +.input-group-sm > .input-group-prepend > select.btn:not([size]):not([multiple]), +.input-group-sm > .input-group-append > select.btn:not([size]):not([multiple]) { + height: calc(1.8125rem + 2px); +} + +.form-control-lg, .input-group-lg > .form-control, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.form-control:not([size]):not([multiple]), +.input-group-lg > .input-group-prepend > select.input-group-text:not([size]):not([multiple]), +.input-group-lg > .input-group-append > select.input-group-text:not([size]):not([multiple]), +.input-group-lg > .input-group-prepend > select.btn:not([size]):not([multiple]), +.input-group-lg > .input-group-append > select.btn:not([size]):not([multiple]) { + height: calc(2.875rem + 2px); +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.25rem; +} + +.form-row { + display: flex; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; +} + +.form-row > .col, +.form-row > [class*="col-"] { + padding-right: 5px; + padding-left: 5px; +} + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} + +.form-check-input:disabled ~ .form-check-label { + color: #6c757d; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-inline { + display: inline-flex; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: .5rem; + margin-top: .1rem; + font-size: .875rem; + line-height: 1; + color: #fff; + background-color: rgba(40, 167, 69, 0.8); + border-radius: .2rem; +} + +.was-validated .form-control:valid, .form-control.is-valid, .was-validated +.custom-select:valid, +.custom-select.is-valid { + border-color: #28a745; +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus, .was-validated +.custom-select:valid:focus, +.custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-control:valid ~ .valid-feedback, +.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback, +.form-control.is-valid ~ .valid-tooltip, .was-validated +.custom-select:valid ~ .valid-feedback, +.was-validated +.custom-select:valid ~ .valid-tooltip, +.custom-select.is-valid ~ .valid-feedback, +.custom-select.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + background-color: #71dd8a; +} + +.was-validated .custom-control-input:valid ~ .valid-feedback, +.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback, +.custom-control-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label::before, .custom-file-input.is-valid ~ .custom-file-label::before { + border-color: inherit; +} + +.was-validated .custom-file-input:valid ~ .valid-feedback, +.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback, +.custom-file-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: .5rem; + margin-top: .1rem; + font-size: .875rem; + line-height: 1; + color: #fff; + background-color: rgba(220, 53, 69, 0.8); + border-radius: .2rem; +} + +.was-validated .form-control:invalid, .form-control.is-invalid, .was-validated +.custom-select:invalid, +.custom-select.is-invalid { + border-color: #dc3545; +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus, .was-validated +.custom-select:invalid:focus, +.custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-control:invalid ~ .invalid-feedback, +.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback, +.form-control.is-invalid ~ .invalid-tooltip, .was-validated +.custom-select:invalid ~ .invalid-feedback, +.was-validated +.custom-select:invalid ~ .invalid-tooltip, +.custom-select.is-invalid ~ .invalid-feedback, +.custom-select.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + background-color: #efa2a9; +} + +.was-validated .custom-control-input:invalid ~ .invalid-feedback, +.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback, +.custom-control-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label::before, .custom-file-input.is-invalid ~ .custom-file-label::before { + border-color: inherit; +} + +.was-validated .custom-file-input:invalid ~ .invalid-feedback, +.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback, +.custom-file-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; +} + +.form-inline .form-check { + width: 100%; +} + +@media (min-width: 576px) { + .form-inline label { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: flex; + flex: 0 0 auto; + flex-flow: row wrap; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group { + width: auto; + } + .form-inline .form-check { + display: flex; + align-items: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + align-items: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.btn:hover, .btn:focus { + text-decoration: none; +} + +.btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.65; +} + +.btn:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active { + background-image: none; +} + +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; +} + +.btn-primary:focus, .btn-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, +.show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0062cc; + border-color: #005cbf; +} + +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + color: #fff; + background-color: #5a6268; + border-color: #545b62; +} + +.btn-secondary:focus, .btn-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, +.show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #545b62; + border-color: #4e555b; +} + +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-success { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + color: #fff; + background-color: #218838; + border-color: #1e7e34; +} + +.btn-success:focus, .btn-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #1e7e34; + border-color: #1c7430; +} + +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; +} + +.btn-info:focus, .btn-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, +.show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #117a8b; + border-color: #10707f; +} + +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; +} + +.btn-warning:focus, .btn-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-warning.disabled, .btn-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, +.show > .btn-warning.dropdown-toggle { + color: #212529; + background-color: #d39e00; + border-color: #c69500; +} + +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + color: #fff; + background-color: #c82333; + border-color: #bd2130; +} + +.btn-danger:focus, .btn-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, +.show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #bd2130; + border-color: #b21f2d; +} + +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-light { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:hover { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; +} + +.btn-light:focus, .btn-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, +.show > .btn-light.dropdown-toggle { + color: #212529; + background-color: #dae0e5; + border-color: #d3d9df; +} + +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:hover { + color: #fff; + background-color: #23272b; + border-color: #1d2124; +} + +.btn-dark:focus, .btn-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, +.show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1d2124; + border-color: #171a1d; +} + +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-outline-primary { + color: #007bff; + background-color: transparent; + background-image: none; + border-color: #007bff; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #007bff; + background-color: transparent; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-secondary { + color: #6c757d; + background-color: transparent; + background-image: none; + border-color: #6c757d; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-success { + color: #28a745; + background-color: transparent; + background-image: none; + border-color: #28a745; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #28a745; + background-color: transparent; +} + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-info { + color: #17a2b8; + background-color: transparent; + background-image: none; + border-color: #17a2b8; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; +} + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-warning { + color: #ffc107; + background-color: transparent; + background-image: none; + border-color: #ffc107; +} + +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-danger { + color: #dc3545; + background-color: transparent; + background-image: none; + border-color: #dc3545; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #dc3545; + background-color: transparent; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-light { + color: #f8f9fa; + background-color: transparent; + background-image: none; + border-color: #f8f9fa; +} + +.btn-outline-light:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f8f9fa; + background-color: transparent; +} + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-dark { + color: #343a40; + background-color: transparent; + background-image: none; + border-color: #343a40; +} + +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-link { + font-weight: 400; + color: #007bff; + background-color: transparent; +} + +.btn-link:hover { + color: #0056b3; + text-decoration: underline; + background-color: transparent; + border-color: transparent; +} + +.btn-link:focus, .btn-link.focus { + text-decoration: underline; + border-color: transparent; + box-shadow: none; +} + +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + transition: opacity 0.15s linear; +} + +.fade.show { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.show { + display: block; +} + +tr.collapse.show { + display: table-row; +} + +tbody.collapse.show { + display: table-row-group; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropup .dropdown-menu { + margin-top: 0; + margin-bottom: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} + +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-menu { + margin-top: 0; + margin-left: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} + +.dropright .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-toggle::after { + vertical-align: 0; +} + +.dropleft .dropdown-menu { + margin-top: 0; + margin-right: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + width: 0; + height: 0; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + color: #16181b; + text-decoration: none; + background-color: #f8f9fa; +} + +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #007bff; +} + +.dropdown-item.disabled, .dropdown-item:disabled { + color: #6c757d; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 0 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group, +.btn-group-vertical .btn + .btn, +.btn-group-vertical .btn + .btn-group, +.btn-group-vertical .btn-group + .btn, +.btn-group-vertical .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after { + margin-left: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} + +.btn-group-vertical .btn, +.btn-group-vertical .btn-group { + width: 100%; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; +} + +.btn-group-toggle > .btn input[type="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} + +.input-group > .form-control, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + flex: 1 1 auto; + width: 1%; + margin-bottom: 0; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file:focus { + z-index: 3; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group > .custom-file { + display: flex; + align-items: center; +} + +.input-group > .custom-file:not(:last-child) .custom-file-label, +.input-group > .custom-file:not(:last-child) .custom-file-label::before { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .custom-file:not(:first-child) .custom-file-label, +.input-group > .custom-file:not(:first-child) .custom-file-label::before { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group-prepend, +.input-group-append { + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; +} + +.input-group-prepend { + margin-right: -1px; +} + +.input-group-append { + margin-left: -1px; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.custom-control { + position: relative; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; +} + +.custom-control-inline { + display: inline-flex; + margin-right: 1rem; +} + +.custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + background-color: #007bff; +} + +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-control-input:active ~ .custom-control-label::before { + color: #fff; + background-color: #b3d7ff; +} + +.custom-control-input:disabled ~ .custom-control-label { + color: #6c757d; +} + +.custom-control-input:disabled ~ .custom-control-label::before { + background-color: #e9ecef; +} + +.custom-control-label { + margin-bottom: 0; +} + +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: 0; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + user-select: none; + background-color: #dee2e6; +} + +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: 0; + display: block; + width: 1rem; + height: 1rem; + content: ""; + background-repeat: no-repeat; + background-position: center center; + background-size: 50% 50%; +} + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before { + background-color: #007bff; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + background-color: #007bff; +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); +} + +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-radio .custom-control-label::before { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::before { + background-color: #007bff; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); +} + +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(2.25rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + vertical-align: middle; + background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center; + background-size: 8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(128, 189, 255, 0.5); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.75rem; + background-image: none; +} + +.custom-select:disabled { + color: #6c757d; + background-color: #e9ecef; +} + +.custom-select::-ms-expand { + opacity: 0; +} + +.custom-select-sm { + height: calc(1.8125rem + 2px); + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 75%; +} + +.custom-select-lg { + height: calc(2.875rem + 2px); + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 125%; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(2.25rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(2.25rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-control { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-file-input:focus ~ .custom-file-control::before { + border-color: #80bdff; +} + +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; +} + +.custom-file-label { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: calc(2.25rem + 2px); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(calc(2.25rem + 2px) - 1px * 2); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: 1px solid #ced4da; + border-radius: 0 0.25rem 0.25rem 0; +} + +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + text-decoration: none; +} + +.nav-link.disabled { + color: #6c757d; +} + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-item { + margin-bottom: -1px; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #007bff; +} + +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; +} + +.navbar > .container, +.navbar > .container-fluid { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} + +.navbar-brand { + display: inline-block; + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; +} + +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} + +.navbar-nav .dropdown-menu { + position: static; + float: none; +} + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + text-decoration: none; +} + +.navbar-toggler:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .dropdown-menu-right { + right: 0; + left: auto; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } + .navbar-expand-sm .dropup .dropdown-menu { + top: auto; + bottom: 100%; + } +} + +@media (max-width: 767.98px) { + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 768px) { + .navbar-expand-md { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .dropdown-menu-right { + right: 0; + left: auto; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } + .navbar-expand-md .dropup .dropdown-menu { + top: auto; + bottom: 100%; + } +} + +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 992px) { + .navbar-expand-lg { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .dropdown-menu-right { + right: 0; + left: auto; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } + .navbar-expand-lg .dropup .dropdown-menu { + top: auto; + bottom: 100%; + } +} + +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .dropdown-menu-right { + right: 0; + left: auto; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } + .navbar-expand-xl .dropup .dropdown-menu { + top: auto; + bottom: 100%; + } +} + +.navbar-expand { + flex-flow: row nowrap; + justify-content: flex-start; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid { + padding-right: 0; + padding-left: 0; +} + +.navbar-expand .navbar-nav { + flex-direction: row; +} + +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} + +.navbar-expand .navbar-nav .dropdown-menu-right { + right: 0; + left: auto; +} + +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid { + flex-wrap: nowrap; +} + +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} + +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-expand .dropup .dropdown-menu { + top: auto; + bottom: 100%; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); +} + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + flex: 1 1 auto; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img { + width: 100%; + border-radius: calc(0.25rem - 1px); +} + +.card-img-top { + width: 100%; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img-bottom { + width: 100%; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-deck { + display: flex; + flex-direction: column; +} + +.card-deck .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-deck { + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + display: flex; + flex: 1 0 0%; + flex-direction: column; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } +} + +.card-group { + display: flex; + flex-direction: column; +} + +.card-group > .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-group { + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:first-child .card-img-top, + .card-group > .card:first-child .card-header { + border-top-right-radius: 0; + } + .card-group > .card:first-child .card-img-bottom, + .card-group > .card:first-child .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:last-child .card-img-top, + .card-group > .card:last-child .card-header { + border-top-left-radius: 0; + } + .card-group > .card:last-child .card-img-bottom, + .card-group > .card:last-child .card-footer { + border-bottom-left-radius: 0; + } + .card-group > .card:only-child { + border-radius: 0.25rem; + } + .card-group > .card:only-child .card-img-top, + .card-group > .card:only-child .card-header { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + } + .card-group > .card:only-child .card-img-bottom, + .card-group > .card:only-child .card-footer { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; + } + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) { + border-radius: 0; + } + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer { + border-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} + +@media (min-width: 576px) { + .card-columns { + column-count: 3; + column-gap: 1.25rem; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.breadcrumb { + display: flex; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + padding-left: 0.5rem; + color: #6c757d; + content: "/"; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #007bff; + background-color: #fff; + border: 1px solid #dee2e6; +} + +.page-link:hover { + color: #0056b3; + text-decoration: none; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.page-link:focus { + z-index: 2; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-link:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.page-item.active .page-link { + z-index: 1; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + cursor: auto; + background-color: #fff; + border-color: #dee2e6; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +.badge-primary[href]:hover, .badge-primary[href]:focus { + color: #fff; + text-decoration: none; + background-color: #0062cc; +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +.badge-secondary[href]:hover, .badge-secondary[href]:focus { + color: #fff; + text-decoration: none; + background-color: #545b62; +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +.badge-success[href]:hover, .badge-success[href]:focus { + color: #fff; + text-decoration: none; + background-color: #1e7e34; +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +.badge-info[href]:hover, .badge-info[href]:focus { + color: #fff; + text-decoration: none; + background-color: #117a8b; +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +.badge-warning[href]:hover, .badge-warning[href]:focus { + color: #212529; + text-decoration: none; + background-color: #d39e00; +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +.badge-danger[href]:hover, .badge-danger[href]:focus { + color: #fff; + text-decoration: none; + background-color: #bd2130; +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +.badge-light[href]:hover, .badge-light[href]:focus { + color: #212529; + text-decoration: none; + background-color: #dae0e5; +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +.badge-dark[href]:hover, .badge-dark[href]:focus { + color: #fff; + text-decoration: none; + background-color: #1d2124; +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.alert-primary { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} + +.alert-primary hr { + border-top-color: #9fcdff; +} + +.alert-primary .alert-link { + color: #002752; +} + +.alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; +} + +.alert-secondary hr { + border-top-color: #c8cbcf; +} + +.alert-secondary .alert-link { + color: #202326; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-success hr { + border-top-color: #b1dfbb; +} + +.alert-success .alert-link { + color: #0b2e13; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} + +.alert-info hr { + border-top-color: #abdde5; +} + +.alert-info .alert-link { + color: #062c33; +} + +.alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} + +.alert-warning hr { + border-top-color: #ffe8a1; +} + +.alert-warning .alert-link { + color: #533f03; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-danger hr { + border-top-color: #f1b0b7; +} + +.alert-danger .alert-link { + color: #491217; +} + +.alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; +} + +.alert-light hr { + border-top-color: #ececf6; +} + +.alert-light .alert-link { + color: #686868; +} + +.alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; +} + +.alert-dark hr { + border-top-color: #b9bbbe; +} + +.alert-dark .alert-link { + color: #040505; +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + color: #fff; + text-align: center; + background-color: #007bff; + transition: width 0.6s ease; +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + animation: progress-bar-stripes 1s linear infinite; +} + +.media { + display: flex; + align-items: flex-start; +} + +.media-body { + flex: 1; +} + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} + +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item:hover, .list-group-item:focus { + z-index: 1; + text-decoration: none; +} + +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + background-color: #fff; +} + +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.list-group-flush .list-group-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} + +.list-group-flush:first-child .list-group-item:first-child { + border-top: 0; +} + +.list-group-flush:last-child .list-group-item:last-child { + border-bottom: 0; +} + +.list-group-item-primary { + color: #004085; + background-color: #b8daff; +} + +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #004085; + background-color: #9fcdff; +} + +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #004085; + border-color: #004085; +} + +.list-group-item-secondary { + color: #383d41; + background-color: #d6d8db; +} + +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #383d41; + background-color: #c8cbcf; +} + +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #383d41; + border-color: #383d41; +} + +.list-group-item-success { + color: #155724; + background-color: #c3e6cb; +} + +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #155724; + background-color: #b1dfbb; +} + +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #155724; + border-color: #155724; +} + +.list-group-item-info { + color: #0c5460; + background-color: #bee5eb; +} + +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #0c5460; + background-color: #abdde5; +} + +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #0c5460; + border-color: #0c5460; +} + +.list-group-item-warning { + color: #856404; + background-color: #ffeeba; +} + +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #856404; + background-color: #ffe8a1; +} + +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #856404; + border-color: #856404; +} + +.list-group-item-danger { + color: #721c24; + background-color: #f5c6cb; +} + +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #721c24; + background-color: #f1b0b7; +} + +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #721c24; + border-color: #721c24; +} + +.list-group-item-light { + color: #818182; + background-color: #fdfdfe; +} + +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #818182; + background-color: #ececf6; +} + +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #818182; + border-color: #818182; +} + +.list-group-item-dark { + color: #1b1e21; + background-color: #c6c8ca; +} + +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1b1e21; + background-color: #b9bbbe; +} + +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1b1e21; + border-color: #1b1e21; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; +} + +.close:hover, .close:focus { + color: #000; + text-decoration: none; + opacity: .75; +} + +.close:not(:disabled):not(.disabled) { + cursor: pointer; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + outline: 0; +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -25%); +} + +.modal.show .modal-dialog { + transform: translate(0, 0); +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - (0.5rem * 2)); +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid #e9ecef; + border-top-left-radius: 0.3rem; + border-top-right-radius: 0.3rem; +} + +.modal-header .close { + padding: 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 1rem; + border-top: 1px solid #e9ecef; +} + +.modal-footer > :not(:first-child) { + margin-left: .25rem; +} + +.modal-footer > :not(:last-child) { + margin-right: .25rem; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + .modal-dialog-centered { + min-height: calc(100% - (1.75rem * 2)); + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg { + max-width: 800px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top .arrow, .bs-popover-auto[x-placement^="top"] .arrow { + bottom: calc((0.5rem + 1px) * -1); +} + +.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before, +.bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { + border-width: 0.5rem 0.5rem 0; +} + +.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before { + bottom: 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { + bottom: 1px; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right .arrow, .bs-popover-auto[x-placement^="right"] .arrow { + left: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before, +.bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { + border-width: 0.5rem 0.5rem 0.5rem 0; +} + +.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before { + left: 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { + left: 1px; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^="bottom"] .arrow { + top: calc((0.5rem + 1px) * -1); +} + +.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before, +.bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { + border-width: 0 0.5rem 0.5rem 0.5rem; +} + +.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before { + top: 0; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { + top: 1px; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left .arrow, .bs-popover-auto[x-placement^="left"] .arrow { + right: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before, +.bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { + border-width: 0.5rem 0 0.5rem 0.5rem; +} + +.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before { + right: 0; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { + right: 1px; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + color: inherit; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.75rem; + color: #212529; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-item { + position: relative; + display: none; + align-items: center; + width: 100%; + transition: transform 0.6s ease; + backface-visibility: hidden; + perspective: 1000px; +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next, +.carousel-item-prev { + position: absolute; + top: 0; +} + +.carousel-item-next.carousel-item-left, +.carousel-item-prev.carousel-item-right { + transform: translateX(0); +} + +@supports (transform-style: preserve-3d) { + .carousel-item-next.carousel-item-left, + .carousel-item-prev.carousel-item-right { + transform: translate3d(0, 0, 0); + } +} + +.carousel-item-next, +.active.carousel-item-right { + transform: translateX(100%); +} + +@supports (transform-style: preserve-3d) { + .carousel-item-next, + .active.carousel-item-right { + transform: translate3d(100%, 0, 0); + } +} + +.carousel-item-prev, +.active.carousel-item-left { + transform: translateX(-100%); +} + +@supports (transform-style: preserve-3d) { + .carousel-item-prev, + .active.carousel-item-left { + transform: translate3d(-100%, 0, 0); + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: .9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: transparent no-repeat center center; + background-size: 100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 10px; + left: 0; + z-index: 15; + display: flex; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; +} + +.carousel-indicators li { + position: relative; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + background-color: rgba(255, 255, 255, 0.5); +} + +.carousel-indicators li::before { + position: absolute; + top: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; +} + +.carousel-indicators li::after { + position: absolute; + bottom: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.bg-primary { + background-color: #007bff !important; +} + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.border-top { + border-top: 1px solid #dee2e6 !important; +} + +.border-right { + border-right: 1px solid #dee2e6 !important; +} + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-left { + border-left: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-left-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #007bff !important; +} + +.border-secondary { + border-color: #6c757d !important; +} + +.border-success { + border-color: #28a745 !important; +} + +.border-info { + border-color: #17a2b8 !important; +} + +.border-warning { + border-color: #ffc107 !important; +} + +.border-danger { + border-color: #dc3545 !important; +} + +.border-light { + border-color: #f8f9fa !important; +} + +.border-dark { + border-color: #343a40 !important; +} + +.border-white { + border-color: #fff !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } +} + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} + +.embed-responsive::before { + display: block; + content: ""; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9::before { + padding-top: 42.85714%; +} + +.embed-responsive-16by9::before { + padding-top: 56.25%; +} + +.embed-responsive-4by3::before { + padding-top: 75%; +} + +.embed-responsive-1by1::before { + padding-top: 100%; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } +} + +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + .float-sm-right { + float: right !important; + } + .float-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + .float-md-right { + float: right !important; + } + .float-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + .float-lg-right { + float: right !important; + } + .float-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + .float-xl-right { + float: right !important; + } + .float-xl-none { + float: none !important; + } +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports (position: sticky) { + .sticky-top { + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + clip-path: inset(50%); + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; + clip-path: none; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} + +.text-justify { + text-align: justify !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-light { + font-weight: 300 !important; +} + +.font-weight-normal { + font-weight: 400 !important; +} + +.font-weight-bold { + font-weight: 700 !important; +} + +.font-italic { + font-style: italic !important; +} + +.text-white { + color: #fff !important; +} + +.text-primary { + color: #007bff !important; +} + +a.text-primary:hover, a.text-primary:focus { + color: #0062cc !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #545b62 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #1e7e34 !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #117a8b !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #d39e00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #bd2130 !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #dae0e5 !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #1d2124 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} diff --git a/public/vendor/industry/css/bootstrap.map b/public/vendor/industry/css/bootstrap.map new file mode 100644 index 0000000..40c58e0 --- /dev/null +++ b/public/vendor/industry/css/bootstrap.map @@ -0,0 +1,166 @@ +{ + "version": 3, + "file": "../scss/bootstrap.css", + "sources": [ + "../scss/bootstrap.scss", + "../scss/bootstrap/_functions.scss", + "../scss/bootstrap/_variables.scss", + "../scss/bootstrap/_mixins.scss", + "../scss/bootstrap/mixins/_breakpoints.scss", + "../scss/bootstrap/mixins/_hover.scss", + "../scss/bootstrap/mixins/_image.scss", + "../scss/bootstrap/mixins/_badge.scss", + "../scss/bootstrap/mixins/_resize.scss", + "../scss/bootstrap/mixins/_screen-reader.scss", + "../scss/bootstrap/mixins/_size.scss", + "../scss/bootstrap/mixins/_reset-text.scss", + "../scss/bootstrap/mixins/_text-emphasis.scss", + "../scss/bootstrap/mixins/_text-hide.scss", + "../scss/bootstrap/mixins/_text-truncate.scss", + "../scss/bootstrap/mixins/_visibility.scss", + "../scss/bootstrap/mixins/_alert.scss", + "../scss/bootstrap/mixins/_buttons.scss", + "../scss/bootstrap/mixins/_pagination.scss", + "../scss/bootstrap/mixins/_lists.scss", + "../scss/bootstrap/mixins/_list-group.scss", + "../scss/bootstrap/mixins/_nav-divider.scss", + "../scss/bootstrap/mixins/_forms.scss", + "../scss/bootstrap/mixins/_table-row.scss", + "../scss/bootstrap/mixins/_background-variant.scss", + "../scss/bootstrap/mixins/_border-radius.scss", + "../scss/bootstrap/mixins/_box-shadow.scss", + "../scss/bootstrap/mixins/_gradients.scss", + "../scss/bootstrap/mixins/_transition.scss", + "../scss/bootstrap/mixins/_clearfix.scss", + "../scss/bootstrap/mixins/_grid-framework.scss", + "../scss/bootstrap/mixins/_grid.scss", + "../scss/bootstrap/mixins/_float.scss", + "../scss/bootstrap/_print.scss", + "../scss/bootstrap/_reboot.scss", + "../scss/bootstrap/_type.scss", + "../scss/bootstrap/_images.scss", + "../scss/bootstrap/_code.scss", + "../scss/bootstrap/_grid.scss", + "../scss/bootstrap/_tables.scss", + "../scss/bootstrap/_forms.scss", + "../scss/bootstrap/_buttons.scss", + "../scss/bootstrap/_transitions.scss", + "../scss/bootstrap/_dropdown.scss", + "../scss/bootstrap/_button-group.scss", + "../scss/bootstrap/_input-group.scss", + "../scss/bootstrap/_custom-forms.scss", + "../scss/bootstrap/_nav.scss", + "../scss/bootstrap/_navbar.scss", + "../scss/bootstrap/_card.scss", + "../scss/bootstrap/_breadcrumb.scss", + "../scss/bootstrap/_pagination.scss", + "../scss/bootstrap/_badge.scss", + "../scss/bootstrap/_jumbotron.scss", + "../scss/bootstrap/_alert.scss", + "../scss/bootstrap/_progress.scss", + "../scss/bootstrap/_media.scss", + "../scss/bootstrap/_list-group.scss", + "../scss/bootstrap/_close.scss", + "../scss/bootstrap/_modal.scss", + "../scss/bootstrap/_tooltip.scss", + "../scss/bootstrap/_popover.scss", + "../scss/bootstrap/_carousel.scss", + "../scss/bootstrap/_utilities.scss", + "../scss/bootstrap/utilities/_align.scss", + "../scss/bootstrap/utilities/_background.scss", + "../scss/bootstrap/utilities/_borders.scss", + "../scss/bootstrap/utilities/_clearfix.scss", + "../scss/bootstrap/utilities/_display.scss", + "../scss/bootstrap/utilities/_embed.scss", + "../scss/bootstrap/utilities/_flex.scss", + "../scss/bootstrap/utilities/_float.scss", + "../scss/bootstrap/utilities/_position.scss", + "../scss/bootstrap/utilities/_screenreaders.scss", + "../scss/bootstrap/utilities/_sizing.scss", + "../scss/bootstrap/utilities/_spacing.scss", + "../scss/bootstrap/utilities/_text.scss", + "../scss/bootstrap/utilities/_visibility.scss" + ], + "sourcesContent": [ + "/*!\n * Bootstrap v4.0.0-beta (https://getbootstrap.com)\n * Copyright 2011-2017 The Bootstrap Authors\n * Copyright 2011-2017 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"bootstrap/functions\";\n@import \"bootstrap/variables\";\n@import \"bootstrap/mixins\";\n@import \"bootstrap/print\";\n@import \"bootstrap/reboot\";\n@import \"bootstrap/type\";\n@import \"bootstrap/images\";\n@import \"bootstrap/code\";\n@import \"bootstrap/grid\";\n@import \"bootstrap/tables\";\n@import \"bootstrap/forms\";\n@import \"bootstrap/buttons\";\n@import \"bootstrap/transitions\";\n@import \"bootstrap/dropdown\";\n@import \"bootstrap/button-group\";\n@import \"bootstrap/input-group\";\n@import \"bootstrap/custom-forms\";\n@import \"bootstrap/nav\";\n@import \"bootstrap/navbar\";\n@import \"bootstrap/card\";\n@import \"bootstrap/breadcrumb\";\n@import \"bootstrap/pagination\";\n@import \"bootstrap/badge\";\n@import \"bootstrap/jumbotron\";\n@import \"bootstrap/alert\";\n@import \"bootstrap/progress\";\n@import \"bootstrap/media\";\n@import \"bootstrap/list-group\";\n@import \"bootstrap/close\";\n@import \"bootstrap/modal\";\n@import \"bootstrap/tooltip\";\n@import \"bootstrap/popover\";\n@import \"bootstrap/carousel\";\n@import \"bootstrap/utilities\";\n", + "// Bootstrap functions\n//\n// Utility mixins and functions for evalutating source code across our variables, maps, and mixins.\n\n// Ascending\n// Used to evaluate Sass maps like our grid breakpoints.\n@mixin _assert-ascending($map, $map-name) {\n $prev-key: null;\n $prev-num: null;\n @each $key, $num in $map {\n @if $prev-num == null {\n // Do nothing\n } @else if not comparable($prev-num, $num) {\n @warn \"Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n } @else if $prev-num >= $num {\n @warn \"Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n }\n $prev-key: $key;\n $prev-num: $num;\n }\n}\n\n// Starts at zero\n// Another grid mixin that ensures the min-width of the lowest breakpoint starts at 0.\n@mixin _assert-starts-at-zero($map) {\n $values: map-values($map);\n $first-value: nth($values, 1);\n @if $first-value != 0 {\n @warn \"First breakpoint in `$grid-breakpoints` must start at 0, but starts at #{$first-value}.\";\n }\n}\n\n// Replace `$search` with `$replace` in `$string`\n// Used on our SVG icon backgrounds for custom forms.\n//\n// @author Hugo Giraudel\n// @param {String} $string - Initial string\n// @param {String} $search - Substring to replace\n// @param {String} $replace ('') - New value\n// @return {String} - Updated string\n@function str-replace($string, $search, $replace: \"\") {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n// Color contrast\n@mixin color-yiq($color) {\n $r: red($color);\n $g: green($color);\n $b: blue($color);\n\n $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;\n\n @if ($yiq >= 150) {\n color: #111;\n } @else {\n color: #fff;\n }\n}\n\n// Retreive color Sass maps\n@function color($key: \"blue\") {\n @return map-get($colors, $key);\n}\n\n@function theme-color($key: \"primary\") {\n @return map-get($theme-colors, $key);\n}\n\n@function grayscale($key: \"100\") {\n @return map-get($grays, $key);\n}\n\n// Request a theme color level\n@function theme-color-level($color-name: \"primary\", $level: 0) {\n $color: theme-color($color-name);\n $color-base: if($level > 0, #000, #fff);\n\n @if $level < 0 {\n // Lighter values need a quick double negative for the Sass math to work\n @return mix($color-base, $color, $level * -1 * $theme-color-interval);\n } @else {\n @return mix($color-base, $color, $level * $theme-color-interval);\n }\n}\n", + "// Variables\n//\n// Copy settings from this file into the provided `_custom.scss` to override\n// the Bootstrap defaults without modifying key, versioned files.\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Table of Contents\n//\n// Color system\n// Options\n// Spacing\n// Body\n// Links\n// Grid breakpoints\n// Grid containers\n// Grid columns\n// Fonts\n// Components\n// Tables\n// Buttons\n// Forms\n// Dropdowns\n// Z-index master list\n// Navs\n// Navbar\n// Pagination\n// Jumbotron\n// Form states and alerts\n// Cards\n// Tooltips\n// Popovers\n// Badges\n// Modals\n// Alerts\n// Progress bars\n// List group\n// Image thumbnails\n// Figures\n// Breadcrumbs\n// Carousel\n// Close\n// Code\n\n\n//\n// Color system\n//\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #868e96 !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: (\n 100: $gray-100,\n 200: $gray-200,\n 300: $gray-300,\n 400: $gray-400,\n 500: $gray-500,\n 600: $gray-600,\n 700: $gray-700,\n 800: $gray-800,\n 900: $gray-900\n) !default;\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: (\n blue: $blue,\n indigo: $indigo,\n purple: $purple,\n pink: $pink,\n red: $red,\n orange: $orange,\n yellow: $yellow,\n green: $green,\n teal: $teal,\n cyan: $cyan,\n white: $white,\n gray: $gray-600,\n gray-dark: $gray-800\n) !default;\n\n$theme-colors: (\n primary: $blue,\n secondary: $gray-600,\n success: $green,\n info: $cyan,\n warning: $yellow,\n danger: $red,\n light: $gray-100,\n dark: $gray-800\n) !default;\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-hover-media-query: false !default;\n$enable-grid-classes: true !default;\n$enable-print-styles: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n) !default;\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%\n) !default;\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints);\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n\n// Fonts\n//\n// Font, line-height, and color for body text, headings, and more.\n\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif !default;\n$font-family-monospace: Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: 1.25rem !default;\n$font-size-sm: .875rem !default;\n\n$font-weight-normal: normal !default;\n$font-weight-bold: bold !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: 2.5rem !default;\n$h2-font-size: 2rem !default;\n$h3-font-size: 1.75rem !default;\n$h4-font-size: 1.5rem !default;\n$h5-font-size: 1.25rem !default;\n$h6-font-size: 1rem !default;\n\n$headings-margin-bottom: ($spacer / 2) !default;\n$headings-font-family: inherit !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.1 !default;\n$headings-color: inherit !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: 1.25rem !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-font-size: ($font-size-base * 1.25) !default;\n\n$hr-border-color: rgba($black,.1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black,.25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: 5px !default;\n\n$mark-bg: #fcf8e3 !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-bg: transparent !default;\n$table-accent-bg: rgba($black,.05) !default;\n$table-hover-bg: rgba($black,.075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $gray-200 !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-inverse-bg: $gray-900 !default;\n$table-inverse-accent-bg: rgba($white, .05) !default;\n$table-inverse-hover-bg: rgba($white, .075) !default;\n$table-inverse-border-color: lighten($gray-900, 7.5%) !default;\n$table-inverse-color: $body-bg !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background and border color.\n\n$input-btn-padding-y: .5rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-line-height: 1.25 !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-line-height-sm: 1.5 !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-line-height-lg: 1.5 !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white,.15), 0 1px 1px rgba($black,.075) !default;\n$btn-focus-box-shadow: 0 0 0 3px rgba(theme-color(\"primary\"), .25) !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black,.125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: all .15s ease-in-out !default;\n\n\n// Forms\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: rgba($black,.15) !default;\n$input-btn-border-width: $border-width !default; // For form controls and buttons\n$input-box-shadow: inset 0 1px 1px rgba($black,.075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten(theme-color(\"primary\"), 25%) !default;\n$input-focus-box-shadow: $input-box-shadow, $btn-focus-box-shadow !default;\n$input-focus-color: $input-color !default;\n\n$input-placeholder-color: $gray-600 !default;\n\n$input-height-border: $input-btn-border-width * 2 !default;\n\n$input-height-inner: ($font-size-base * $input-btn-line-height) + ($input-btn-padding-y * 2) !default;\n$input-height: calc(#{$input-height-inner} + #{$input-height-border}) !default;\n\n$input-height-inner-sm: ($font-size-sm * $input-btn-line-height-sm) + ($input-btn-padding-y-sm * 2) !default;\n$input-height-sm: calc(#{$input-height-inner-sm} + #{$input-height-border}) !default;\n\n$input-height-inner-lg: ($font-size-sm * $input-btn-line-height-lg) + ($input-btn-padding-y-lg * 2) !default;\n$input-height-lg: calc(#{$input-height-inner-lg} + #{$input-height-border}) !default;\n\n$input-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-margin-bottom: .5rem !default;\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .25rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-control-gutter: 1.5rem !default;\n$custom-control-spacer-y: .25rem !default;\n$custom-control-spacer-x: 1rem !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: #ddd !default;\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: inset 0 .25rem .25rem rgba($black,.1) !default;\n\n$custom-control-indicator-disabled-bg: $gray-200 !default;\n$custom-control-description-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $white !default;\n$custom-control-indicator-checked-bg: theme-color(\"primary\") !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n\n$custom-control-indicator-focus-box-shadow: 0 0 0 1px $body-bg, 0 0 0 3px theme-color(\"primary\") !default;\n\n$custom-control-indicator-active-color: $white !default;\n$custom-control-indicator-active-bg: lighten(theme-color(\"primary\"), 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$custom-control-indicator-checked-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: theme-color(\"primary\") !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='#{$custom-checkbox-indicator-indeterminate-color}' d='M0 2h4'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$custom-control-indicator-checked-color}'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n\n$custom-select-padding-y: .375rem !default;\n$custom-select-padding-x: .75rem !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-line-height: $input-btn-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $white !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: #333 !default;\n$custom-select-indicator: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n$custom-select-border-width: $input-btn-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n\n$custom-select-focus-border-color: lighten(theme-color(\"primary\"), 25%) !default;\n$custom-select-focus-box-shadow: inset 0 1px 2px rgba($black, .075), 0 0 5px rgba($custom-select-focus-border-color, .5) !default;\n\n$custom-select-font-size-sm: 75% !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-file-height: 2.5rem !default;\n$custom-file-width: 14rem !default;\n$custom-file-focus-box-shadow: 0 0 0 .075rem $white, 0 0 0 .2rem theme-color(\"primary\") !default;\n\n$custom-file-padding-y: 1rem !default;\n$custom-file-padding-x: .5rem !default;\n$custom-file-line-height: 1.5 !default;\n$custom-file-color: $gray-700 !default;\n$custom-file-bg: $white !default;\n$custom-file-border-width: $border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $border-radius !default;\n$custom-file-box-shadow: inset 0 .2rem .4rem rgba($black,.05) !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $gray-200 !default;\n$custom-file-text: (\n placeholder: (\n en: \"Choose file...\"\n ),\n button-label: (\n en: \"Browse\"\n )\n) !default;\n\n\n// Form validation\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black,.15) !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: #ddd !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: #ddd !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n// Navbar\n\n$navbar-padding-y: ($spacer / 2) !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-height: ($font-size-base * $line-height-base + $nav-link-padding-y * 2) !default;\n$navbar-brand-padding-y: ($navbar-brand-height - $nav-link-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white,.5) !default;\n$navbar-dark-hover-color: rgba($white,.75) !default;\n$navbar-dark-active-color: rgba($white,1) !default;\n$navbar-dark-disabled-color: rgba($white,.25) !default;\n$navbar-dark-toggler-icon-bg: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-dark-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n$navbar-dark-toggler-border-color: rgba($white,.1) !default;\n\n$navbar-light-color: rgba($black,.5) !default;\n$navbar-light-hover-color: rgba($black,.7) !default;\n$navbar-light-active-color: rgba($black,.9) !default;\n$navbar-light-disabled-color: rgba($black,.3) !default;\n$navbar-light-toggler-icon-bg: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-light-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n$navbar-light-toggler-border-color: rgba($black,.1) !default;\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: #ddd !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: #ddd !default;\n\n$pagination-active-color: $white !default;\n$pagination-active-bg: theme-color(\"primary\") !default;\n$pagination-active-border-color: theme-color(\"primary\") !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: #ddd !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: 1px !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black,.125) !default;\n$card-inner-border-radius: calc(#{$card-border-radius} - #{$card-border-width}) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-deck-margin: ($grid-gutter-width / 2) !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: 3px !default;\n$tooltip-padding-x: 8px !default;\n$tooltip-margin: 0 !default;\n\n\n$tooltip-arrow-width: 5px !default;\n$tooltip-arrow-height: 5px !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n\n// Popovers\n\n$popover-inner-padding: 1px !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black,.2) !default;\n$popover-box-shadow: 0 5px 10px rgba($black,.2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: 8px !default;\n$popover-header-padding-x: 14px !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: 9px !default;\n$popover-body-padding-x: 14px !default;\n\n$popover-arrow-width: 10px !default;\n$popover-arrow-height: 5px !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-width: ($popover-arrow-width + 1px) !default;\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Badges\n\n$badge-color: $white !default;\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 15px !default;\n\n$modal-dialog-margin: 10px !default;\n$modal-dialog-margin-y-sm-up: 30px !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black,.2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-box-shadow-xs: 0 3px 9px rgba($black,.5) !default;\n$modal-content-box-shadow-sm-up: 0 5px 15px rgba($black,.5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $gray-200 !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding: 15px !default;\n\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-transition: transform .3s ease-out !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: .75rem !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black,.1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n// List group\n\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black,.125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: #ddd !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black,.075) !default;\n$thumbnail-transition: all .2s ease-in-out !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: \"/\" !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n$carousel-control-next-icon-bg: str-replace(url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E\"), \"#\", \"%23\") !default;\n\n$carousel-transition: transform .6s ease !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n// Code\n\n$code-font-size: 90% !default;\n$code-padding-y: .2rem !default;\n$code-padding-x: .4rem !default;\n$code-color: #bd4147 !default;\n$code-bg: $gray-100 !default;\n\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n", + "// Toggles\n//\n// Used in conjunction with global variables to enable certain theme features.\n\n// Utilities\n@import \"mixins/breakpoints\";\n@import \"mixins/hover\";\n@import \"mixins/image\";\n@import \"mixins/badge\";\n@import \"mixins/resize\";\n@import \"mixins/screen-reader\";\n@import \"mixins/size\";\n@import \"mixins/reset-text\";\n@import \"mixins/text-emphasis\";\n@import \"mixins/text-hide\";\n@import \"mixins/text-truncate\";\n@import \"mixins/visibility\";\n\n// // Components\n@import \"mixins/alert\";\n@import \"mixins/buttons\";\n@import \"mixins/pagination\";\n@import \"mixins/lists\";\n@import \"mixins/list-group\";\n@import \"mixins/nav-divider\";\n@import \"mixins/forms\";\n@import \"mixins/table-row\";\n\n// // Skins\n@import \"mixins/background-variant\";\n@import \"mixins/border-radius\";\n@import \"mixins/box-shadow\";\n@import \"mixins/gradients\";\n@import \"mixins/transition\";\n\n// // Layout\n@import \"mixins/clearfix\";\n// @import \"mixins/navbar-align\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n@import \"mixins/float\";\n", + "// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.1.\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - 1px, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash infront.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name)\n } @else if $min == null {\n @include media-breakpoint-down($name)\n }\n}\n", + "@mixin hover {\n // TODO: re-enable along with mq4-hover-shim\n// @if $enable-hover-media-query {\n// // See Media Queries Level 4: https://drafts.csswg.org/mediaqueries/#hover\n// // Currently shimmed by https://github.com/twbs/mq4-hover-shim\n// @media (hover: hover) {\n// &:hover { @content }\n// }\n// }\n// @else {\n// scss-lint:disable Indentation\n &:hover { @content }\n// scss-lint:enable Indentation\n// }\n}\n\n\n@mixin hover-focus {\n @if $enable-hover-media-query {\n &:focus { @content }\n @include hover { @content }\n } @else {\n &:focus,\n &:hover {\n @content\n }\n }\n}\n\n@mixin plain-hover-focus {\n @if $enable-hover-media-query {\n &,\n &:focus {\n @content\n }\n @include hover { @content }\n } @else {\n &,\n &:focus,\n &:hover {\n @content\n }\n }\n}\n\n@mixin hover-focus-active {\n @if $enable-hover-media-query {\n &:focus,\n &:active {\n @content\n }\n @include hover { @content }\n } @else {\n &:focus,\n &:active,\n &:hover {\n @content\n }\n }\n}\n", + "// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: http://caniuse.com/#feat=css-media-resolution\n @media\n only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n}\n", + "@mixin badge-variant($bg) {\n @include color-yiq($bg);\n background-color: $bg;\n\n &[href] {\n @include hover-focus {\n @include color-yiq($bg);\n text-decoration: none;\n background-color: darken($bg, 10%);\n }\n }\n}\n", + "// Resize anything\n\n@mixin resizable($direction) {\n overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible`\n resize: $direction; // Options: horizontal, vertical, both\n}\n", + "// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content\n// See: http://hugogiraudel.com/2016/10/13/css-hide-and-seek/\n\n@mixin sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n white-space: nowrap;\n clip-path: inset(50%);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n//\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n//\n// Credit: HTML5 Boilerplate\n\n@mixin sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n clip-path: none;\n }\n}\n", + "// Sizing shortcuts\n\n@mixin size($width, $height: $width) {\n width: $width;\n height: $height;\n}\n", + "// scss-lint:disable DuplicateProperty\n@mixin reset-text {\n font-family: $font-family-base;\n // We deliberately do NOT reset font-size or word-wrap.\n font-style: normal;\n font-weight: $font-weight-normal;\n line-height: $line-height-base;\n text-align: left; // Fallback for where `start` is not supported\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n}\n", + "// Typography\n\n@mixin text-emphasis-variant($parent, $color) {\n #{$parent} {\n color: $color !important;\n }\n a#{$parent} {\n @include hover-focus {\n color: darken($color, 10%) !important;\n }\n }\n}\n", + "// CSS image replacement\n@mixin text-hide() {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n", + "// Text truncate\n// Requires inline-block or block for proper styling\n\n@mixin text-truncate() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n", + "// Visibility\n\n@mixin invisible($visibility) {\n visibility: $visibility !important;\n}\n", + "@mixin alert-variant($background, $border, $color) {\n color: $color;\n background-color: $background;\n border-color: $border;\n\n hr {\n border-top-color: darken($border, 5%);\n }\n\n .alert-link {\n color: darken($color, 10%);\n }\n}\n", + "// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n@mixin button-variant($background, $border, $active-background: darken($background, 7.5%), $active-border: darken($border, 10%)) {\n @include color-yiq($background);\n background-color: $background;\n border-color: $border;\n @include box-shadow($btn-box-shadow);\n\n &:hover {\n @include color-yiq($background);\n background-color: $active-background;\n border-color: $active-border;\n }\n\n &:focus,\n &.focus {\n // Avoid using mixin so we can pass custom focus shadow properly\n @if $enable-shadows {\n box-shadow: $btn-box-shadow, 0 0 0 3px rgba($border, .5);\n } @else {\n box-shadow: 0 0 0 3px rgba($border, .5);\n }\n }\n\n // Disabled comes first so active can properly restyle\n &.disabled,\n &:disabled {\n background-color: $background;\n border-color: $border;\n }\n\n &:active,\n &.active,\n .show > &.dropdown-toggle {\n background-color: $active-background;\n background-image: none; // Remove the gradient for the pressed/active state\n border-color: $active-border;\n @include box-shadow($btn-active-box-shadow);\n }\n}\n\n@mixin button-outline-variant($color, $color-hover: #fff) {\n color: $color;\n background-color: transparent;\n background-image: none;\n border-color: $color;\n\n @include hover {\n color: $color-hover;\n background-color: $color;\n border-color: $color;\n }\n\n &:focus,\n &.focus {\n box-shadow: 0 0 0 3px rgba($color, .5);\n }\n\n &.disabled,\n &:disabled {\n color: $color;\n background-color: transparent;\n }\n\n &:active,\n &.active,\n .show > &.dropdown-toggle {\n color: $color-hover;\n background-color: $color;\n border-color: $color;\n }\n}\n\n// Button sizes\n@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n padding: $padding-y $padding-x;\n font-size: $font-size;\n line-height: $line-height;\n @include border-radius($border-radius);\n}\n", + "// Pagination\n\n@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n .page-link {\n padding: $padding-y $padding-x;\n font-size: $font-size;\n line-height: $line-height;\n }\n\n .page-item {\n &:first-child {\n .page-link {\n @include border-left-radius($border-radius);\n }\n }\n &:last-child {\n .page-link {\n @include border-right-radius($border-radius);\n }\n }\n }\n}\n", + "// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n", + "// List Groups\n\n@mixin list-group-item-variant($state, $background, $color) {\n .list-group-item-#{$state} {\n color: $color;\n background-color: $background;\n }\n\n //scss-lint:disable QualifyingElement\n a.list-group-item-#{$state},\n button.list-group-item-#{$state} {\n color: $color;\n\n @include hover-focus {\n color: $color;\n background-color: darken($background, 5%);\n }\n\n &.active {\n color: #fff;\n background-color: $color;\n border-color: $color;\n }\n }\n // scss-lint:enable QualifyingElement\n}\n", + "// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n@mixin nav-divider($color: #e5e5e5) {\n height: 0;\n margin: ($spacer / 2) 0;\n overflow: hidden;\n border-top: 1px solid $color;\n}\n", + "// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-border-color-focus` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n@mixin form-control-focus() {\n &:focus {\n color: $input-focus-color;\n background-color: $input-focus-bg;\n border-color: $input-focus-border-color;\n outline: none;\n @include box-shadow($input-focus-box-shadow);\n }\n}\n\n\n@mixin form-validation-state($state, $color) {\n\n .form-control,\n .custom-select {\n .was-validated &:#{$state},\n &.is-#{$state} {\n border-color: $color;\n\n &:focus {\n box-shadow: 0 0 0 .2rem rgba($color,.25);\n }\n\n ~ .invalid-feedback,\n ~ .invalid-tooltip {\n display: block;\n }\n }\n }\n\n\n // TODO: redo check markup lol crap\n .form-check-input {\n .was-validated &:#{$state},\n &.is-#{$state} {\n + .form-check-label {\n color: $color;\n }\n }\n }\n\n // custom radios and checks\n .custom-control-input {\n .was-validated &:#{$state},\n &.is-#{$state} {\n ~ .custom-control-indicator {\n background-color: rgba($color, .25);\n }\n ~ .custom-control-description {\n color: $color;\n }\n }\n }\n\n // custom file\n .custom-file-input {\n .was-validated &:#{$state},\n &.is-#{$state} {\n ~ .custom-file-control {\n border-color: $color;\n\n &::before { border-color: inherit; }\n }\n &:focus {\n box-shadow: 0 0 0 .2rem rgba($color,.25);\n }\n }\n }\n}\n", + "// Tables\n\n@mixin table-row-variant($state, $background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n", + "// Contextual backgrounds\n\n@mixin bg-variant($parent, $color) {\n #{$parent} {\n background-color: $color !important;\n }\n a#{$parent} {\n @include hover-focus {\n background-color: darken($color, 10%) !important;\n }\n }\n}\n", + "// Single side border-radius\n\n@mixin border-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n", + "@mixin box-shadow($shadow...) {\n @if $enable-shadows {\n box-shadow: $shadow;\n }\n}\n", + "// Gradients\n\n// Horizontal gradient, from left to right\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-x($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n// Vertical gradient, from top to bottom\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-y($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n@mixin gradient-directional($start-color: #555, $end-color: #333, $deg: 45deg) {\n background-image: linear-gradient($deg, $start-color, $end-color);\n background-repeat: repeat-x;\n}\n@mixin gradient-x-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {\n background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-y-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {\n background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-radial($inner-color: #555, $outer-color: #333) {\n background-image: radial-gradient(circle, $inner-color, $outer-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-striped($color: rgba(255,255,255,.15), $angle: 45deg) {\n background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);\n}\n", + "@mixin transition($transition...) {\n @if $enable-transitions {\n @if length($transition) == 0 {\n transition: $transition-base;\n } @else {\n transition: $transition;\n }\n }\n}\n", + "@mixin clearfix() {\n &::after {\n display: block;\n clear: both;\n content: \"\";\n }\n}\n", + "// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n min-height: 1px; // Prevent columns from collapsing when empty\n padding-right: ($gutter / 2);\n padding-left: ($gutter / 2);\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col#{$infix}-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none; // Reset earlier grid tiers\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n @for $i from 1 through $columns {\n .order#{$infix}-#{$i} {\n order: $i;\n }\n }\n }\n }\n}\n", + "/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container() {\n margin-right: auto;\n margin-left: auto;\n padding-right: ($grid-gutter-width / 2);\n padding-left: ($grid-gutter-width / 2);\n width: 100%;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row() {\n display: flex;\n flex-wrap: wrap;\n margin-right: ($grid-gutter-width / -2);\n margin-left: ($grid-gutter-width / -2);\n}\n\n@mixin make-col-ready() {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n min-height: 1px; // Prevent collapsing\n padding-right: ($grid-gutter-width / 2);\n padding-left: ($grid-gutter-width / 2);\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n", + "@mixin float-left {\n float: left !important;\n}\n@mixin float-right {\n float: right !important;\n}\n@mixin float-none {\n float: none !important;\n}\n", + "// scss-lint:disable QualifyingElement\n\n// Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request:\n// http://www.phpied.com/delay-loading-your-print-css/\n// ==========================================================================\n\n@if $enable-print-styles {\n @media print {\n *,\n *::before,\n *::after {\n // Bootstrap specific; comment out `color` and `background`\n //color: #000 !important; // Black prints faster:\n // http://www.sanbeiji.com/archives/953\n text-shadow: none !important;\n //background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n // Bootstrap specific; comment the following selector out\n //a[href]::after {\n // content: \" (\" attr(href) \")\";\n //}\n\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n\n // Bootstrap specific; comment the following selector out\n //\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n //\n\n //a[href^=\"#\"]::after,\n //a[href^=\"javascript:\"]::after {\n // content: \"\";\n //}\n\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: $border-width solid #999; // Bootstrap custom code; using `$border-width` instead of 1px\n page-break-inside: avoid;\n }\n\n //\n // Printing Tables:\n // http://css-discuss.incutio.com/wiki/Printing_Tables\n //\n\n thead {\n display: table-header-group;\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .badge {\n border: $border-width solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n }\n}\n", + "// scss-lint:disable QualifyingElement, DuplicateProperty, VendorPrefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Setting @viewport causes scrollbars to overlap content in IE11 and Edge, so\n// we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n// 6. Change the default tap highlight to be completely transparent in iOS.\n\nhtml {\n box-sizing: border-box; // 1\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -ms-text-size-adjust: 100%; // 4\n -ms-overflow-style: scrollbar; // 5\n -webkit-tap-highlight-color: rgba(0,0,0,0); // 6\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit; // 1\n}\n\n// IE10+ doesn't honor `` in some cases.\n@at-root {\n @-ms-viewport { width: device-width; }\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n font-size: $font-size-base;\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: none !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: .5rem;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n// Avoid 300ms click delay on touch devices that support the `touch-action` CSS property.\n//\n// In particular, unlike most other browsers, IE11+Edge on Windows 10 on touch devices and IE Mobile 10-11\n// DON'T remove the click delay when `` is present.\n// However, they DO support removing the click delay via `touch-action: manipulation`.\n// See:\n// * https://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch\n// * http://caniuse.com/#feat=css-touch-action\n// * https://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay\n\na,\narea,\nbutton,\n[role=\"button\"],\ninput,\nlabel,\nselect,\nsummary,\ntextarea {\n touch-action: manipulation;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment\n text-align: left;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: .5rem;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\nhtml [type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n", + "//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { font-size: $h1-font-size; }\nh2, .h2 { font-size: $h2-font-size; }\nh3, .h3 { font-size: $h3-font-size; }\nh4, .h4 { font-size: $h4-font-size; }\nh5, .h5 { font-size: $h5-font-size; }\nh6, .h6 { font-size: $h6-font-size; }\n\n.lead {\n font-size: $lead-font-size;\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n font-size: $display1-size;\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n font-size: $display2-size;\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n font-size: $display3-size;\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n font-size: $display4-size;\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n font-size: $small-font-size;\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled;\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n font-size: $blockquote-font-size;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%; // back to default font-size\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n}\n", + "// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid;\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include transition($thumbnail-transition);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid;\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: ($spacer / 2);\n line-height: 1;\n}\n\n.figure-caption {\n font-size: $figure-caption-font-size;\n color: $figure-caption-color;\n}\n", + "// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: $font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: $code-padding-y $code-padding-x;\n font-size: $code-font-size;\n color: $code-color;\n background-color: $code-bg;\n @include border-radius($border-radius);\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n padding: 0;\n color: inherit;\n background-color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $code-padding-y $code-padding-x;\n font-size: $code-font-size;\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n font-size: $code-font-size;\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n", + "// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n}\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but with 100% width for\n// fluid, full width layouts.\n\n@if $enable-grid-classes {\n .container-fluid {\n width: 100%;\n @include make-container();\n }\n}\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n", + "//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: $spacer;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n\n .table {\n background-color: $body-bg;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: (2 * $table-border-width);\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(odd) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, -9));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Inverse styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n.thead-inverse {\n th {\n color: $table-inverse-color;\n background-color: $table-inverse-bg;\n }\n}\n\n.thead-default {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n }\n}\n\n.table-inverse {\n color: $table-inverse-color;\n background-color: $table-inverse-bg;\n\n th,\n td,\n thead th {\n border-color: $table-inverse-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(odd) {\n background-color: $table-inverse-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-inverse-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Add `.table-responsive` to `.table`s and we'll make them mobile friendly by\n// enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n @include media-breakpoint-down(md) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -ms-overflow-style: -ms-autohiding-scrollbar; // See https://github.com/twbs/bootstrap/pull/10057\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n &.table-bordered {\n border: 0;\n }\n }\n}\n", + "// scss-lint:disable QualifyingElement, VendorPrefix\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n // // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n // height: $input-height;\n padding: $input-btn-padding-y $input-btn-padding-x;\n font-size: $font-size-base;\n line-height: $input-btn-line-height;\n color: $input-color;\n background-color: $input-bg;\n // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214.\n background-image: none;\n background-clip: padding-box;\n border: $input-btn-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @if $enable-rounded {\n // Manually use the if/else instead of the mixin to account for iOS override\n border-radius: $input-border-radius;\n } @else {\n // Otherwise undo the iOS default\n border-radius: 0;\n }\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label text to\n// align with the form controls.\n.col-form-label {\n padding-top: calc(#{$input-btn-padding-y} - #{$input-btn-border-width} * 2);\n padding-bottom: calc(#{$input-btn-padding-y} - #{$input-btn-border-width} * 2);\n margin-bottom: 0; // Override the `