Compare commits

...

140 Commits

Author SHA1 Message Date
Spythere ea5c9e0028 Merge do wersji 1.17.0
Wersja 1.17.0
2023-09-06 15:43:52 +02:00
Spythere eb7c2d7132 revamp postoju i strzałek w RJ scenerii 2023-09-05 16:48:26 +02:00
Spythere ee7c50f59b szybkie filtry (wip) 2023-09-05 16:10:38 +02:00
Spythere 439f59fedc poprawki filtrów scenerii 2023-09-05 15:40:32 +02:00
Spythere c47d839ce3 poprawki kolorów 2023-09-04 18:50:12 +02:00
Spythere f77c13cbcf srjp: poprawki dostępności modalu 2023-09-04 18:37:52 +02:00
Spythere dbbbd33100 poprawki dzienników 2023-09-03 19:24:32 +02:00
Spythere 14d13360a8 dziennik dr: animacje 2023-09-03 18:47:01 +02:00
Spythere dc862252ba dziennik dr: kolumna regionów 2023-09-03 18:34:45 +02:00
Spythere e5fe727ccd aktualizacja URLi 2023-09-03 17:58:55 +02:00
Spythere e836bbed0c szybkie filtry (wip) 2023-09-02 19:36:49 +02:00
Spythere d4438fd215 station filters active indicator 2023-09-02 18:53:23 +02:00
Spythere 1550849360 dzienniki 2023-09-02 18:47:01 +02:00
Spythere 9d1dc4ffca bump 1.17 2023-08-31 22:46:41 +02:00
Spythere 0397fa788d zmiana wyglądu listy dzienników 2023-08-31 22:45:14 +02:00
Spythere 6e5696b0a6 daily stats hotfix 2023-08-31 21:46:05 +02:00
Spythere 4537341a57 odświeżony wygląd dziennika RJ 2023-08-30 20:31:40 +02:00
Spythere c35c74bd4a Merge pull request #52
fix: data wcześniejszego przyjazdu dla 0pt
2023-08-27 23:07:01 +02:00
Spythere 25735c5e6e fix: data wcześniejszego przyjazdu dla 0pt 2023-08-27 22:45:36 +02:00
Spythere 41e60bc69e Merge do wersji 1.16.3 2023-07-06 01:54:21 +02:00
Spythere 933bdecb3c bump: 1.16.3 2023-07-02 14:50:55 +02:00
Spythere 10e183d96b zamiana infinite scrolla na przyciski 2023-07-02 14:50:18 +02:00
Spythere 5429d39f5e tłumaczenia historii dr scenerii 2023-07-01 23:39:08 +02:00
Spythere ff31e7f903 bump: 1.16.2.1 2023-07-01 23:29:48 +02:00
Spythere 91f4c6bc57 Merge do wersji 1.16.2 2023-07-01 01:25:10 +02:00
Spythere c133eb060b bump: 1.16.2 2023-07-01 01:20:04 +02:00
Spythere 7ffc169d8a hotfix 2023-07-01 01:19:50 +02:00
Spythere 1b85cc5f58 poprawki dziennika rj / dr 2023-06-30 20:50:03 +02:00
Spythere 72ff857fff dodatkowe info o postojach w dzienniku RJ 2023-06-27 02:52:41 +02:00
Spythere 96d64e77fc feature: nieskończona lista historii dr/rj scenerii 2023-06-24 13:46:37 +02:00
Spythere 6ceae3f161 revamp tabeli historii dyżurów 2023-06-23 14:19:28 +02:00
Spythere 8e8e27658c Merge do wersji 1.16.1
Wersja 1.16.1
2023-06-22 19:27:39 +02:00
Spythere 9b6ace394a bump v.1.16.1 2023-06-21 18:36:21 +02:00
Spythere 6cfeaa91bf responsywność dailyStats 2023-06-21 18:35:23 +02:00
Spythere 08b208aeaa fix tłumaczeń 2023-06-21 18:31:16 +02:00
Spythere a089b5275b hotfix daily stats 2023-06-21 18:30:02 +02:00
Spythere 8425cd4371 przyjazdy/odjazdy stacji pośrednich RJ w dzienniku 2023-06-21 18:26:16 +02:00
Spythere dbdc517b87 fix tłumaczeń 2023-06-21 17:32:36 +02:00
Spythere e271358a27 fix timetable id 2023-06-21 17:19:31 +02:00
Spythere 66262e3fcd dodatkowe statystyki dnia 2023-06-21 17:16:02 +02:00
Spythere 5b2b6bdea2 bump 1.16.0.1 2023-06-16 01:17:33 +02:00
Spythere c8587de6d9 npm update 2023-06-15 15:28:32 +02:00
Spythere 1f376085f2 feature: info o elektryfikacji spawnu na scenerii 2023-06-15 15:28:12 +02:00
Spythere f28600a7fa Merge do wersji 1.16.0
Wersja 1.16.0
2023-06-12 01:33:45 +02:00
Spythere d59ead87e6 bump v1.16.0 2023-06-12 01:21:49 +02:00
Spythere 34d91bc800 poprawki w pokazywaniu statystyk 2023-06-12 01:19:31 +02:00
Spythere cf9991d8a0 layout filtrów dzienników 2023-06-12 00:51:17 +02:00
Spythere 4ffb79d62b poprawki pobierania danych 2023-06-11 21:47:50 +02:00
Spythere d9f5edb4fe poprawki tłumaczeń 2023-06-11 21:47:22 +02:00
Spythere 1b2112430a feature: długości szlaków po kliknięciu 2023-06-08 23:35:57 +02:00
Spythere 0a972a23ef fix: asynchroniczność pobierania danych z API 2023-06-04 13:35:53 +02:00
Spythere 6d52724d06 zapamiętywanie stanu statystyk dnia 2023-06-04 12:19:46 +02:00
Spythere 99415c35d3 rozbudowane filtry dziennika RJ 2023-06-04 12:06:15 +02:00
Spythere c3f687d439 hotfixy dzienników 2023-06-04 01:45:58 +02:00
Spythere 266edfd6e6 reorder typów danych 2023-06-04 00:33:43 +02:00
Spythere d32d5ad91b poprawki dzienników 2023-06-03 18:55:44 +02:00
Spythere c3481470cb optymalizacja zapytań; filtr scenerii pocz. 2023-06-03 15:49:15 +02:00
Spythere 57e88b9abc Merge do wersji 1.15.1
Wersja 1.15.1
2023-06-02 20:09:43 +02:00
Spythere 44ebf53798 poprawki pwa 2023-06-02 20:06:25 +02:00
Spythere 145dc72b6b pwa: zmiana na autoUpdate 2023-06-02 19:41:33 +02:00
Spythere b7f3761940 Merge do wersji 1.15.0
Wersja 1.15.0
2023-06-02 01:16:37 +02:00
Spythere ea7c49dfb3 bump: 1.15.0 2023-06-02 01:10:01 +02:00
Spythere 5d6785813a fix: odznaki TWR/SKR; dodano do dziennika 2023-06-02 01:07:44 +02:00
Spythere a0054aed14 fix: optymalizacja zapytań historii RJ scenerii 2023-06-02 00:51:03 +02:00
Spythere 471e6f5216 Wersja 1.14.3
Wersja 1.14.3
2023-05-18 02:42:27 +02:00
Spythere a617eef00e bump 1.14.3 2023-05-18 02:38:13 +02:00
Spythere 38e700ecd6 migracja z route na routeNames 2023-05-18 02:37:27 +02:00
Spythere da1be0e10a Wersja 1.14.2 2023-05-17 02:24:05 +02:00
Spythere f49bb12948 bump 1.14.2 2023-05-17 02:21:13 +02:00
Spythere 02673a3d70 poprawki filtrów scenerii 2023-05-16 14:27:31 +02:00
Spythere 4ddc7345df poprawki filtrów RJ 2023-05-16 02:40:08 +02:00
Spythere 5d822684c0 feature: rozszerzone filtry RJ 2023-05-14 15:05:51 +02:00
Spythere 69fa15c70a v1.14.1
v1.14.1
2023-04-13 19:25:30 +02:00
Spythere 9192067388 1.14.1 2023-04-13 19:21:36 +02:00
Spythere 2b41e5b857 hotfix 2023-04-13 19:21:24 +02:00
Spythere 674680ff14 1.14: hotfixy
1.14 hotfixy
2023-04-13 19:15:38 +02:00
Spythere 475bd2ff10 przeniesienie skrótów do widoku scenerii 2023-04-13 19:11:44 +02:00
Spythere 074d1eb155 Hotfix tłumaczeń 2023-04-13 18:50:00 +02:00
Spythere 378393de89 Wersja 1.14.0
Wersja 1.14.0
2023-04-13 15:45:39 +02:00
Spythere 03e61083a7 tłumaczenie stopki 2023-04-13 15:37:34 +02:00
Spythere 0b746fce8c hotfix headera 2023-04-13 15:32:41 +02:00
Spythere 5883e710be bump wersji: 1.14 2023-04-13 15:26:22 +02:00
Spythere 3d0695a17b Poprawki w headerze i stopce 2023-04-13 15:25:39 +02:00
Spythere 4adb76eeb0 feature: podpisy status indicatorów dla RJ 2023-04-11 00:33:14 +02:00
Spythere 4c41076519 feature: skrót posterunku w tabelce 2023-04-09 01:37:11 +02:00
Spythere 77f61d17fd hotfix: wygląd badge'a 2023-04-09 00:19:53 +02:00
Spythere 032a84cbcf feature: historia zmian w zestawieniu pociągów 2023-03-20 22:39:23 +01:00
Spythere de9851ebcc Wersja 1.13.0
Wersja 1.13.0
2023-03-16 01:31:18 +01:00
Spythere ff78eba927 hotfixy filtrów 2023-03-15 18:15:01 +01:00
Spythere e4c5f6a322 filtry pociągów 2023-03-15 17:56:27 +01:00
Spythere 0a78761928 hotfix tłumaczeń 2023-03-15 15:34:36 +01:00
Spythere 4843043c29 bump wersji: 1.13.0 2023-03-15 15:20:28 +01:00
Spythere 9e1df1fb61 filtry pociągów 2023-03-15 15:19:50 +01:00
Spythere 021474cfb0 fix: tłumaczenia; hotfixy 2023-03-14 23:13:37 +01:00
Spythere 7d0e68862c filtry scenerii 2023-03-14 21:42:39 +01:00
Spythere 653d45dfc6 scenerie: remake filtrów 2023-03-13 22:26:00 +01:00
Spythere 4a4e1240a4 widok scenerii: odnośniki do tablic przy aktywnych RJ 2023-03-10 16:59:16 +01:00
Spythere 14ca48a90d hotfix 2023-03-10 16:30:36 +01:00
Spythere a02f9804b1 dziennik RJ: dodatkowe info 2023-03-10 16:30:31 +01:00
Spythere c5efc6fbac bump: wersja 1.12.2 2023-03-10 02:03:37 +01:00
Spythere cacd0a1e4e fix: data stworzenia RJ 2023-03-10 01:53:19 +01:00
Spythere 50375099ab update url api 2023-03-10 01:42:10 +01:00
Spythere 6af67ec741 fix: tłumaczenia filtrów 2023-03-09 14:35:03 +01:00
Spythere c64112c86a dziennik RJ: dodatkowe informacje 2023-03-09 14:28:16 +01:00
Spythere 0434702d3b update: URL api 2023-03-09 13:41:59 +01:00
Spythere dd7d1b0bb0 Wersja 1.12.1
Wersja 1.12.1
2023-02-26 14:20:40 +01:00
Spythere 68934a89a4 bump: wersja 1.12.1 2023-02-26 14:16:13 +01:00
Spythere b88a240ec1 feature: like count historii dyżurnych 2023-02-26 14:14:32 +01:00
Spythere eaa34f3359 hotfix: dostępność grubości czcionki 2023-02-26 13:50:47 +01:00
Spythere febb22e1bc Wersja 1.12
Merge produkcyjny do wersji 1.12.0
2023-02-14 21:32:46 +01:00
Spythere 500f3c1223 dziennik RJ: wyświetlanie statów 2023-02-14 16:57:22 +01:00
Spythere 221e0c7e82 dzienniki: fix ładowania 2023-02-14 16:50:12 +01:00
Spythere ca19f7e397 hotfix: websocket 2023-02-14 16:40:15 +01:00
Spythere a71ccd3e1a bump: wersja 1.12 2023-02-14 13:52:20 +01:00
Spythere d496c70fa8 aktualizacja tłumaczenia 2023-02-14 13:51:52 +01:00
Spythere b9868ba52e dzienniki: stylistyka 2023-02-12 16:12:48 +01:00
Spythere 59bd3fa2ef design: badge poziomów 2023-02-12 12:58:23 +01:00
Spythere e14d328ed9 fix: wielkość scrollbaru 2023-02-12 00:48:18 +01:00
Spythere 36d71292bc feature: url projektów 2023-02-12 00:42:37 +01:00
Spythere 2f6e2e7402 fix: responsywność 2023-02-12 00:30:05 +01:00
Spythere e959eac6c5 hotfix 2023-02-11 03:14:43 +01:00
Spythere 8bedc4dfc6 feature: vmax szlaków 2023-02-11 03:08:24 +01:00
Spythere 73563d5db7 Wersja 1.11.2
Wersja 1.11.2
2023-01-05 22:05:34 +01:00
Spythere 3f818069cd hotfix: podświetlenie sponsorów w dzienniku RJ 2023-01-05 16:09:27 +01:00
Spythere cdf0b2a426 feature: nasłuchiwanie aktualizacji 2023-01-05 16:05:34 +01:00
Spythere c29ddeb78c fix: poziom 0 w dzienniku RJ 2023-01-05 15:59:14 +01:00
Spythere b81d98cab7 fix: filtrowanie pociągów offline 2023-01-05 15:58:17 +01:00
Spythere 0e45bca5da feature: przycisk odświeżania dzienników 2023-01-05 14:49:44 +01:00
Spythere 715e66879f feature: przejścia pomiędzy statusami ładowań 2023-01-04 14:01:25 +01:00
Spythere 1747e15dc8 bump: 1.11.2 2023-01-03 14:58:04 +01:00
Spythere 6a923a8e1d feature: sygnatura dev w stopce 2023-01-03 14:57:36 +01:00
Spythere 25a248e95e feature: animacje list 2023-01-03 14:51:19 +01:00
Spythere aa7a6b220e feature: lvl maszynisty przy dzienniku i pociągach 2023-01-02 18:30:09 +01:00
Spythere deb7b68985 Merge branch 'development' 2023-01-01 03:02:11 +01:00
Spythere 633f05f690 fix: wyświetlanie poprawnych id RJ 2023-01-01 02:57:11 +01:00
Spythere 73828867da Merge wersji dev do produkcji (1.11.1)
Wersja 1.11.1
2022-12-31 18:30:08 +01:00
Spythere 75685c1e0e bump: 1.11.1 2022-12-31 18:22:39 +01:00
Spythere 496ff95236 fix: sortowanie RJ wg id z API 2022-12-31 18:21:32 +01:00
Spythere 7e25327832 feature: lvl dyżurnego w dzienniku 2022-12-30 17:39:21 +01:00
Spythere 272c9f50f8 fix: SW cache 2022-12-30 15:45:17 +01:00
95 changed files with 18022 additions and 16824 deletions
+4 -3
View File
@@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2" /> <meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl" />
<meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" /> <meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<title>Stacjownik</title> <title>Stacjownik</title>
@@ -24,7 +24,7 @@
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" /> <link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
@@ -32,3 +32,4 @@
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+11390 -11386
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.11.0", "version": "1.17.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+10 -2
View File
@@ -1,7 +1,6 @@
@import './styles/responsive.scss'; @import './styles/responsive.scss';
@import './styles/variables.scss'; @import './styles/variables.scss';
@import './styles/global.scss'; @import './styles/global.scss';
@import './styles/scenery_status.scss';
// VUE ROUTE CHANGE ANIMATION // VUE ROUTE CHANGE ANIMATION
.view-anim { .view-anim {
@@ -46,7 +45,11 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.5rem + 1.3vw); font-size: calc(0.55rem + 1.1vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
} }
} }
@@ -87,6 +90,11 @@ footer.app_footer {
max-width: 100%; max-width: 100%;
padding: 0.5em; padding: 0.5em;
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10; z-index: 10;
background: #111; background: #111;
+5 -1
View File
@@ -21,7 +21,10 @@
<footer class="app_footer"> <footer class="app_footer">
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | <a :href="releaseURL" target="_blank">v{{ VERSION }}</a> {{ new Date().getUTCFullYear() }} |
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
<br />
<a href="https://discord.gg/x2mpNN3svk"><img :src="getIcon('discord', 'png')" alt="">&nbsp;<b>{{ $t('footer.discord') }}</b></a>
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div> <div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer> </footer>
@@ -86,6 +89,7 @@ export default defineComponent({
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', releaseURL: '',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
}), }),
created() { created() {
+20 -18
View File
@@ -1,18 +1,20 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" fill="#898989"/> <rect y="-0.00012207" width="60" height="60" fill="#898989"/>
<path d="M30.5 6.04878H35.2195" stroke="#BFBFBF"/> <path d="M29.0126 32.4897V10.2511V9.52028H30.4337V10.2511V57.234H29.0126V32.4897Z" fill="#BFBFBF"/>
<path d="M27.9024 4.00303C25.2115 4.10008 24.2403 6.24494 24 7.41767H32.0488C31.8486 6.16406 30.5934 3.90598 27.9024 4.00303Z" fill="black"/> <path d="M26.955 29.3992V32.9949L29.7672 36.9105" stroke="black" stroke-width="0.61183"/>
<path d="M33.0244 29.6688V5.47793V4.68292H34.4878V5.47793V56.5854H33.0244V32.5H27.5V28.5V28.0163L28.5 28V31.5L31.9268 31.5447H33.0244V29.6688Z" fill="#BFBFBF"/> <rect x="29.0051" y="34.0686" width="1.42857" height="22.8196" fill="white"/>
<path d="M28.1463 29.2683C30.8373 29.1712 31.8085 27.0264 32.0488 25.8537H24C24.2002 27.1073 25.4554 29.3654 28.1463 29.2683Z" fill="black"/> <rect x="29.0051" y="34.0686" width="1.42857" height="5.18627" fill="#FF0000"/>
<path d="M32.0488 25.8537V7.86993V7.41464H24V25.8537H32.0488Z" fill="black"/> <rect x="29.0051" y="54.8137" width="1.42857" height="5.18627" fill="#FF0000"/>
<path d="M25 26V29.5L33.8781 44.9756" stroke="black"/> <rect x="29.0051" y="44.4412" width="1.42857" height="5.18627" fill="#FF0000"/>
<rect x="33.0244" y="31.5447" width="1.46341" height="25.0407" fill="white"/> <rect x="27.8749" y="31.8649" width="3.75" height="2.17823" fill="white"/>
<rect x="33.0244" y="31.5447" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M33.5 28.5111V8.61545V8.11176H26V28.5111H33.5Z" fill="black"/>
<rect x="33.0244" y="42.9268" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M29.6364 5.00276C27.1289 5.09112 26.2239 7.044 26 8.11176H33.5C33.3134 6.97036 32.1438 4.91439 29.6364 5.00276Z" fill="black"/>
<rect x="33.0244" y="54.3089" width="1.46341" height="5.69106" fill="#FF0000"/> <path d="M29.8636 31.6201C32.3711 31.5317 33.2761 29.5789 33.5 28.5111H26C26.1865 29.6525 27.3561 31.7085 29.8636 31.6201Z" fill="black"/>
<ellipse cx="27.9024" cy="7.40022" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="11.8168" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="11.8343" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="8.0135" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="16.2683" rx="1.46341" ry="1.40022" fill="#FF0000"/> <ellipse cx="29.887" cy="15.6151" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="20.7023" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="19.6834" rx="1.38696" ry="1.28474" fill="#212121"/>
<ellipse cx="27.9024" cy="25.1364" rx="1.46341" ry="1.40022" fill="#212121"/> <ellipse cx="29.887" cy="23.7518" rx="1.38696" ry="1.28474" fill="#212121"/>
</svg> <ellipse cx="29.887" cy="27.8201" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
<ellipse cx="29.887" cy="19.769" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

+18
View File
@@ -0,0 +1,18 @@
<svg width="144" height="147" viewBox="0 0 144 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1343_19)">
<path d="M115.039 101.247C116.397 98.6665 115.405 95.4739 112.824 94.1167C110.243 92.7594 107.05 93.7514 105.693 96.3323L115.039 101.247ZM89.4447 44.0402L94.1179 46.4977L99.0329 37.1513L94.3597 34.6938L89.4447 44.0402ZM105.693 96.3323C95.7398 115.259 72.3278 122.534 53.4008 112.581L48.4858 121.927C72.5746 134.595 102.372 125.336 115.039 101.247L105.693 96.3323ZM53.4008 112.581C34.4739 102.627 27.1993 79.2155 37.1525 60.2885L27.8061 55.3735C15.1383 79.4623 24.397 109.259 48.4858 121.927L53.4008 112.581ZM37.1525 60.2885C47.1057 41.3616 70.5177 34.087 89.4447 44.0402L94.3597 34.6938C70.2709 22.026 40.4738 31.2846 27.8061 55.3735L37.1525 60.2885Z" fill="white"/>
<path d="M91.2258 38.7627L101.056 20.0698L116.15 51.8695L81.3956 57.4555L91.2258 38.7627Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_1343_19" x="18.1328" y="20.0698" width="102.017" height="115.531" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1343_19"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1343_19" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#F2E147"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#66FF6C"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#898989"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

+27 -65
View File
@@ -6,16 +6,6 @@
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" /> <img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else /> <img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
</span> </span>
<span class="icons-bottom">
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
<img :src="getIcon('dollar')" alt="icon paypal" />
</a>
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div> </div>
<div class="header_body"> <div class="header_body">
@@ -33,6 +23,12 @@
<div class="info_counter"> <div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" /> <img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span> <span class="text--primary">{{ onlineDispatchersCount }}</span>
<!-- <span class="g-tooltip">
<b class="text--primary">{{ factorU }}U</b>
<div class="content">Test</div>
</span> -->
<span class="text--grayed"> / </span> <span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span> <span class="text--primary">{{ onlineTrainsCount }}</span>
<img :src="getIcon('train')" alt="icon train" /> <img :src="getIcon('train')" alt="icon train" />
@@ -98,11 +94,17 @@ export default defineComponent({
onlineTrainsCount() { onlineTrainsCount() {
return this.store.trainList.filter((train) => train.online).length; return this.store.trainList.filter((train) => train.online).length;
}, },
onlineDispatchersCount() { onlineDispatchersCount() {
return this.store.stationList.filter( return this.store.stationList.filter(
(station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id (station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id
).length; ).length;
}, },
factorU() {
return this.onlineDispatchersCount == 0 ? '-' : (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
},
computedRegions() { computedRegions() {
return options.regions.map((region) => { return options.regions.map((region) => {
const regionStationCount = const regionStationCount =
@@ -135,22 +137,20 @@ export default defineComponent({
.header { .header {
&_body { &_body {
max-width: 21em;
position: relative; position: relative;
max-width: 20em;
@include smallScreen {
max-width: 18em;
}
} }
&_container { &_container {
display: flex; display: flex;
justify-content: center; justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em; border-radius: 0 0 1em 1em;
@include smallScreen {
position: relative;
margin-top: 0.5em;
}
} }
&_brand { &_brand {
@@ -158,6 +158,7 @@ export default defineComponent({
img { img {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
} }
@@ -165,9 +166,7 @@ export default defineComponent({
&_info { &_info {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
max-width: 100%; font-size: 1.15em;
font-size: 1.2em;
} }
&_links { &_links {
@@ -184,57 +183,20 @@ export default defineComponent({
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
height: 100%;
display: flex; padding: 0.5em;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() { @include smallScreen {
right: auto; transform: translateX(85%);
left: 0.75em;
padding: 0;
align-items: center;
} }
} }
} }
// ICONS // ICONS
.icons { .icons-top {
position: relative; img {
width: 2.5em;
&-top { cursor: pointer;
img {
width: 2.5em;
cursor: pointer;
}
margin-bottom: 0.5em;
}
&-bottom {
display: flex;
a {
margin-left: 0.6em;
user-select: none;
}
img {
width: 1.9em;
}
@include smallScreen() {
flex-direction: column;
a {
margin: 0.25em 0;
}
}
} }
} }
+12 -15
View File
@@ -164,7 +164,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import { StoreState } from '../../store/storeTypes'; import { StoreState } from '../../scripts/interfaces/store/storeTypes';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -303,9 +303,11 @@ export default defineComponent({
.status-indicator { .status-indicator {
position: absolute; position: absolute;
left: 110%;
bottom: 0; bottom: 0;
right: 0;
z-index: 100; z-index: 100;
transform: translateX(1.5em);
} }
.indicator { .indicator {
@@ -330,7 +332,7 @@ export default defineComponent({
background-color: #171717; background-color: #171717;
border-radius: 0.75em; border-radius: 0.75em;
min-width: 13em; width: 13em;
text-align: center; text-align: center;
overflow: none; overflow: none;
@@ -354,22 +356,16 @@ export default defineComponent({
} }
@include midScreen() { @include midScreen() {
left: 50%; left: auto;
top: 100%; right: 200%;
transform: translate(-50%, 0);
margin-left: 0;
margin-top: 0.75em;
&::before { &::before {
border-left: 10px solid transparent;
border-right: 10px solid transparent; border-right: 10px solid transparent;
border-bottom: 10px solid #171717; border-left: 12px solid #171717;
right: 0;
left: auto;
top: 0; transform: translate(100%, -50%);
left: 50%;
transform: translate(-50%, -100%);
} }
} }
@@ -379,3 +375,4 @@ export default defineComponent({
} }
} }
</style> </style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="progress-bar">
<span class="bar-bg"></span>
<span class="bar-fg" :style="{ width: `${~~progressPercent}%`, backgroundColor: bgColor }"></span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
progressPercent: {
type: Number,
required: true,
},
progressType: {
type: String,
required: false,
},
},
computed: {
bgColor() {
switch (this.progressType) {
case 'abandoned':
return 'salmon';
default:
return 'springgreen';
}
},
},
});
</script>
<style lang="scss" scoped>
.progress-bar {
position: relative;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
}
</style>
+6 -30
View File
@@ -3,6 +3,10 @@
<div class="select-box_content"> <div class="select-box_content">
<button class="selected" @click="toggleBox"> <button class="selected" @click="toggleBox">
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
<div class="arrow">
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
</div>
</button> </button>
<ul class="options" :ref="(el) => (listRef = el as Element)"> <ul class="options" :ref="(el) => (listRef = el as Element)">
@@ -21,10 +25,6 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="arrow">
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
</div>
</div> </div>
</template> </template>
@@ -129,46 +129,22 @@ export default defineComponent({
} }
.select-box { .select-box {
position: relative; display: flex;
width: auto; align-items: center;
} }
.arrow { .arrow {
position: absolute;
top: 50%;
right: 0;
padding: 0;
img { img {
vertical-align: middle; vertical-align: middle;
width: 1.35em; width: 1.35em;
} }
transform: translateY(-50%);
pointer-events: none;
} }
button.selected { button.selected {
background-color: transparent;
color: paleturquoise; color: paleturquoise;
font-size: 1em;
font-weight: bold; font-weight: bold;
padding: 0.1em 0.5em; padding: 0.1em 0.5em;
margin-right: 2em;
display: flex;
width: 100%;
cursor: pointer;
border: none;
outline: none;
text-align: left;
&:focus { &:focus {
background-color: #262626; background-color: #262626;
@@ -0,0 +1,90 @@
<template>
<span class="status-badge" :class="statusID" v-if="isOnline">
{{ $t(`status.${statusID}`) }}
{{ statusID == 'online' ? timestampToString(statusTimestamp!) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
export default defineComponent({
props: {
statusID: {
type: String,
},
statusTimestamp: {
type: Number,
},
isOnline: {
type: Boolean,
},
},
mixins: [dateMixin],
});
</script>
<style lang="scss" scoped>
$free: #8a8a8a;
$ending: #e6c300;
$no-limit: #117fc9;
$unav: #ff3d5d;
$brb: #e6a100;
$no-space: #222;
$online: #09a116;
$unknown: rgb(185, 60, 60);
.status-badge {
border-radius: 1rem;
font-weight: 500;
padding: 0.2em 0.55em;
background-color: $online;
&.free {
background-color: $free;
font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.brb {
background-color: $brb;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
border: 1px solid white;
color: white;
font-size: 0.85em;
}
&.unknown {
background-color: $unknown;
font-size: 0.95em;
}
}
</style>
+119 -119
View File
@@ -1,119 +1,119 @@
<template> <template>
<span class="stop-date"> <span class="stop-date">
<span <span
class="date arrival" class="date arrival"
v-if="!stop.beginsHere" v-if="!stop.beginsHere"
:class="{ :class="{
delayed: stop.arrivalDelay > 0 && stop.confirmed, delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
preponed: stop.arrivalDelay < 0 && stop.confirmed, preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
'on-time': stop.arrivalDelay == 0 && stop.confirmed, 'on-time': stop.arrivalDelay == 0 && stop.confirmed,
}" }"
> >
<span v-if="stop.arrivalDelay != 0 && stop.confirmed"> <span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s> <s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalRealTimestamp) }} {{ timestampToString(stop.arrivalRealTimestamp) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }}) ({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
</span> </span>
<span v-else> <span v-else>
{{ timestampToString(stop.arrivalTimestamp) }} {{ timestampToString(stop.arrivalTimestamp) }}
</span> </span>
</span> </span>
<span class="date stop" v-if="stop.stopTime" :class="stop.stopType.replace(', ', '-')"> <span class="date stop" v-if="stop.stopTime || stop.stopped" :class="stop.stopType.replace(', ', '-')">
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }} {{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
</span> </span>
<span <span
class="date departure" class="date departure"
v-if="!stop.terminatesHere && stop.stopTime != 0" v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
:class="{ :class="{
delayed: stop.departureDelay > 0 && stop.confirmed, delayed: stop.departureDelay > 0 && stop.confirmed,
preponed: stop.departureDelay < 0 && stop.confirmed, preponed: stop.departureDelay < 0 && stop.confirmed,
}" }"
> >
<span v-if="stop.departureDelay != 0 && stop.confirmed"> <span v-if="stop.departureDelay != 0 && stop.confirmed">
<s>{{ timestampToString(stop.departureTimestamp) }}</s> <s>{{ timestampToString(stop.departureTimestamp) }}</s>
{{ timestampToString(stop.departureRealTimestamp) }} {{ timestampToString(stop.departureRealTimestamp) }}
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }}) ({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
</span> </span>
<span v-else> <span v-else>
{{ timestampToString(stop.departureTimestamp) }} {{ timestampToString(stop.departureTimestamp) }}
</span> </span>
</span> </span>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
props: { props: {
stop: { stop: {
type: Object as () => TrainStop, type: Object as () => TrainStop,
required: true, required: true,
}, },
}, },
setup() { setup() {
return {}; return {};
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$preponedClr: lime; $preponedClr: lime;
$delayedClr: salmon; $delayedClr: salmon;
$dateClr: #525151; $dateClr: #525151;
$stopExchangeClr: #db8e29; $stopExchangeClr: #db8e29;
$stopDefaultClr: #252525; $stopDefaultClr: #252525;
.stop-date { .stop-date {
display: flex; display: flex;
align-items: center; align-items: center;
.date { .date {
background: $dateClr; background: $dateClr;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
} }
.stop { .stop {
&.ph, &.ph,
&.ph-pm, &.ph-pm,
&.pm { &.pm {
background: $stopExchangeClr; background: $stopExchangeClr;
} }
background: $stopDefaultClr; background: $stopDefaultClr;
} }
.arrival, .arrival,
.departure { .departure {
&.delayed { &.delayed {
s { s {
color: #999; color: #999;
} }
span { span {
color: $delayedClr; color: $delayedClr;
} }
} }
&.preponed { &.preponed {
s { s {
color: #999; color: #999;
} }
span { span {
color: $preponedClr; color: $preponedClr;
} }
} }
} }
} }
</style> </style>
+148 -80
View File
@@ -1,56 +1,62 @@
<template> <template>
<section class="daily-stats"> <section class="daily-stats">
<span :data-active="data.statsStatus"> <span :data-active="statsStatus">
<b v-if="data.statsStatus == DataStatus.Loading"> <b v-if="statsStatus == DataStatus.Loading">
{{ $t('app.loading') }} {{ $t('app.loading') }}
</b> </b>
<b v-else-if="data.stats.distanceSum == null"> <b v-else-if="stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }} {{ $t('journal.daily-stats-info') }}
</b> </b>
<span> <span class="stats-list" v-else>
<div v-if="data.stats.totalTimetables"> <h3>
{{ $t('journal.daily-stats-title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr style="margin-bottom: 0.5em" />
<div v-if="stats.totalTimetables">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-total"> <i18n-t keypath="journal.timetable-stats-total">
<template #count> <template #count>
<b class="text--primary"> <b class="text--primary">
{{ data.stats.totalTimetables }} {{ stats.totalTimetables }}
{{ $t('journal.timetable-count', data.stats.totalTimetables) }} {{ $t('journal.timetable-count', stats.totalTimetables) }}
</b> </b>
</template> </template>
<template #distance> <template #distance>
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b> <b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="data.stats.timetableId"> <div v-if="stats.timetableId">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-longest"> <i18n-t keypath="journal.timetable-stats-longest">
<template #id> <template #id>
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`"> <router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`">
<b>{{ data.stats.timetableId }}</b> <b>{{ stats.timetableId }}</b>
</router-link> </router-link>
</template> </template>
<template #author> <template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`">
<b>{{ data.stats.timetableAuthor }}</b> <b>{{ stats.timetableAuthor }}</b>
</router-link> </router-link>
</template> </template>
<template #driver> <template #driver>
<b>{{ data.stats.timetableDriver }}</b> <b class="text--primary">{{ stats.timetableDriver }}</b>
</template> </template>
<template #distance> <template #distance>
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b> <b class="text--primary">{{ stats.timetableRouteDistance }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="firstPlaceDispatchers.length == 1"> <div v-if="firstPlaceDispatchers.length == 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active"> <i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher> <template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b> <b>{{ firstPlaceDispatchers[0].name }}</b>
@@ -67,7 +73,7 @@
<div v-if="firstPlaceDispatchers.length > 1"> <div v-if="firstPlaceDispatchers.length > 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active-many"> <i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers> <template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers"> <span v-for="(disp, i) in firstPlaceDispatchers">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span> <span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
@@ -88,95 +94,157 @@
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`">
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<template #station>{{ stats.longestDuties[0].station }}</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</span> </span>
</span> </span>
</section> </section>
</template> </template>
<script setup lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { computed, reactive, ref } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData'; import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
const intervalId = ref(-1); export default defineComponent({
mixins: [dateMixin],
emits: ['toggleStatsOpen'],
const data = reactive({ data() {
statsStatus: DataStatus.Loading, return {
DataStatus,
statsStatus: DataStatus.Loading,
intervalId: -1,
stats: { stats: {
totalTimetables: 0, totalTimetables: 0,
distanceSum: 0, distanceSum: 0,
distanceAvg: 0, distanceAvg: 0,
timetableAuthor: '', timetableAuthor: '',
timetableDriver: '', timetableDriver: '',
timetableId: 0, timetableId: 0,
timetableRouteDistance: 0, timetableRouteDistance: 0,
longestDuties: [],
mostActiveDispatchers: [], mostActiveDrivers: [],
} as ITimetablesDailyStats, mostActiveDispatchers: [],
}); } as ITimetablesDailyStats,
const firstPlaceDispatchers = computed(() => {
if (data.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = data.stats.mostActiveDispatchers[0].count;
return data.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
});
async function fetchDailyTimetableStats() {
try {
const {
distanceAvg,
distanceSum,
maxTimetable,
totalTimetables,
mostActiveDispatchers,
}: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
data.stats = {
totalTimetables,
distanceSum,
distanceAvg,
timetableAuthor: maxTimetable?.authorName || '',
timetableDriver: maxTimetable?.driverName || '',
timetableId: maxTimetable?.timetableId || 0,
timetableRouteDistance: maxTimetable?.routeDistance || 0,
mostActiveDispatchers,
}; };
},
data.statsStatus = DataStatus.Loaded; activated() {
} catch (error) { this.startFetchingDailyStats();
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...'); this.$emit('toggleStatsOpen', true);
data.statsStatus = DataStatus.Error; },
}
}
function startFetchingDailyStats() { deactivated() {
fetchDailyTimetableStats(); this.stopFetchingDailyStats();
intervalId.value = setInterval(fetchDailyTimetableStats, 60000); },
}
function stopFetchingDailyStats() { computed: {
clearInterval(intervalId.value); firstPlaceDispatchers() {
} if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count;
defineExpose({ return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
startFetchingDailyStats, },
stopFetchingDailyStats, },
methods: {
async fetchDailyTimetableStats() {
try {
const res: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
this.stats = {
totalTimetables: res.totalTimetables,
distanceSum: res.distanceSum,
distanceAvg: res.distanceAvg,
timetableAuthor: res.maxTimetable?.authorName || '',
timetableDriver: res.maxTimetable?.driverName || '',
timetableId: res.maxTimetable?.id || 0,
timetableRouteDistance: res.maxTimetable?.routeDistance || 0,
mostActiveDispatchers: res.mostActiveDispatchers,
mostActiveDrivers: res.mostActiveDrivers,
longestDuties: res.longestDuties,
};
this.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = DataStatus.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = setInterval(this.fetchDailyTimetableStats, 60000);
},
stopFetchingDailyStats() {
clearInterval(this.intervalId);
this.intervalId = -1;
},
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss';
.daily-stats { .daily-stats {
text-align: left; text-align: left;
} }
.daily-stats > span[data-active='0'] { .daily-stats > span[data-active='0'] {
opacity: 0.75; opacity: 0.75;
} }
.stats-list a {
text-decoration: underline;
}
@include smallScreen {
.daily-stats {
text-align: justify;
}
h3 {
text-align: center;
}
}
</style> </style>
@@ -1,62 +1,155 @@
<template> <template>
<ul class="journal-list"> <div>
<!-- <transition-group name="journal-list-anim"> --> <transition name="status-anim" mode="out-in">
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }"> <div :key="dataStatus">
<div v-if="typeof item == 'string'" class="journal_day"> <div class="journal_warning" v-if="store.isOffline">
{{ item }} {{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<table class="scenery-history-table">
<thead>
<th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th>
</thead>
<tbody>
<transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td>
<router-link :to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`">
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group>
</tbody>
</table>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && dispatcherHistory.length > 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div> </div>
</transition>
<div <div class="journal_warning" v-if="scrollNoMoreData">
v-else {{ $t('journal.no-further-data') }}
class="journal_item" </div>
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0"
>
<span>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span>
<span class="region-badge" :class="item.region">PL1</span>
</span>
<span> <div class="journal_warning" v-else-if="!scrollDataLoaded">
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span> {{ $t('journal.loading-further-data') }}
<span> </div>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }} </div>
</span>
<span v-if="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span v-if="item.timestampTo">
&gt;
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
</span>
</span>
</div>
</li>
<!-- </transition-group> -->
</ul>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import styleMixin from '../../mixins/styleMixin';
import imageMixin from '../../mixins/imageMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json';
export default defineComponent({ export default defineComponent({
components: { Loading },
mixins: [dateMixin, styleMixin, imageMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>, type: Array as PropType<DispatcherHistory[]>,
required: true, required: true,
}, },
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
}, },
mixins: [dateMixin], data() {
return {
DataStatus,
store: useStore(),
regions,
};
},
computed: { computed: {
computedDispatcherHistory() { computedDispatcherHistory() {
console.log(this.dispatcherHistory.length);
return this.dispatcherHistory.reduce((acc, historyItem, i) => { return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL')); if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem); acc.push(historyItem);
@@ -86,71 +179,61 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/animations.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/variables.scss';
@import '../../styles/JournalSection.scss'; @import '../../styles/JournalSection.scss';
.region-badge { table.scenery-history-table {
padding: 0.1em 0.5em; --_bg-table: #111;
border-radius: 0.5em; --_bg-head: #101010;
font-weight: bold; --_bg-row: #2f2f2f;
&.eu { width: 100%;
background-color: forestgreen; border-collapse: collapse;
position: relative;
text-align: center;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
@media screen and (max-width: 550px) {
font-size: 0.9em;
} }
} }
li.sticky { .text {
position: sticky; &--online {
top: 0;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
padding: 0.75em;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen; color: springgreen;
} }
span[data-status='false'] { &--offline {
color: salmon; color: #ddd;
}
}
.journal_day {
margin-bottom: 1em;
padding: 0.5em;
font-weight: bold;
background-color: #333;
span {
position: relative;
background-color: inherit;
z-index: 10;
padding-right: 1em;
font-weight: bold;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
} }
} }
</style> </style>
+61 -35
View File
@@ -2,11 +2,18 @@
<div class="filters-options" @keydown.esc="showOptions = false"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="bg" v-if="showOptions" @click="showOptions = false"></div> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button"> <div class="actions-bar">
<img :src="getIcon('filter2')" alt="Open filters" /> <button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
{{ $t('options.filters') }} [F] <img :src="getIcon('filter2')" alt="Open filters" />
<span class="active-indicator" v-if="currentOptionsActive"></span> {{ $t('options.filters') }} [F]
</button> <span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
<button class="filter-button btn--filled btn--image" @click="refreshData">
<img :src="getIcon('refresh')" alt="Refresh data" />
{{ $t('general.refresh') }}
</button>
</div>
<datalist id="search-driver"> <datalist id="search-driver">
<option v-for="sugg in driverSuggestions" :value="sugg"></option> <option v-for="sugg in driverSuggestions" :value="sugg"></option>
@@ -22,7 +29,7 @@
<h1 class="option-title">{{ $t('options.search-title') }}</h1> <h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content"> <div class="search_content">
<div class="search" v-for="(_, propName) in searchersValues" :key="propName"> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label> <label v-if="propName == 'search-date'" for="date">{{ $t(`options.search-${optionsType}-date`) }}</label>
<div class="search-box"> <div class="search-box">
<input <input
@@ -42,15 +49,6 @@
</button> </button>
</div> </div>
</div> </div>
<div class="search_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button>
</div>
</div> </div>
<h1 class="option-title">{{ $t('options.sort-title') }}</h1> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
@@ -67,15 +65,31 @@
</div> </div>
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1> <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<button <div class="options_filter-sections" v-if="filters.length != 0 && filterList">
v-for="filter in filters" <section class="filter-section" v-for="section in JournalFilterSection">
class="filter-option btn--option" <p>{{ $t(`options.filter-section-${section}`) }}</p>
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id" <div class="options_filters">
@click="onFilterChange(filter)" <button
> v-for="filter in filterList.filter((f) => f.filterSection == section)"
{{ $t(`options.filter-${filter.id}`) }} class="filter-option btn--option"
:class="{ checked: filter.isActive }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
</section>
</div>
<div class="options_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button> </button>
</div> </div>
</div> </div>
@@ -93,13 +107,14 @@ import { DataStatus } from '../../scripts/enums/DataStatus';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType';
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
emits: ['onSearchConfirm', 'onOptionsReset'], emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
mixins: [imageMixin, keyMixin], mixins: [imageMixin, keyMixin],
props: { props: {
@@ -109,7 +124,7 @@ export default defineComponent({
}, },
filters: { filters: {
type: Array as PropType<JournalTimetableFilter[]>, type: Array as PropType<JournalFilter[]>,
default: [], default: [],
}, },
@@ -122,11 +137,17 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
optionsType: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
showOptions: false, showOptions: false,
JournalFilterSection,
driverSuggestions: [] as string[], driverSuggestions: [] as string[],
dispatcherSuggestions: [] as string[], dispatcherSuggestions: [] as string[],
@@ -142,7 +163,8 @@ export default defineComponent({
return { return {
searchersValues: inject('searchersValues') as { [key: string]: string }, searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, sorterActive: inject('sorterActive') as { id: string | number; dir: number },
journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter, // journalFilterActive: inject('journalFilterActive') as JournalFilter,
filterList: inject('filterList') as JournalFilter[] | undefined,
}; };
}, },
@@ -162,7 +184,8 @@ export default defineComponent({
watch: { watch: {
async driverStatsName(value: string) { async driverStatsName(value: string) {
await this.fetchDriverStats(); await this.fetchDriverStats();
this.store.currentStatsTab = value ? 'driver' : 'daily';
// if (value) this.store.currentStatsTab = 'driver';
}, },
async 'searchersValues.search-driver'(value: string | undefined) { async 'searchersValues.search-driver'(value: string | undefined) {
@@ -206,6 +229,10 @@ export default defineComponent({
} }
}, },
refreshData() {
this.$emit('onRefreshData');
},
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) { startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
if (this[`${type}Suggestions`].includes(value)) return; if (this[`${type}Suggestions`].includes(value)) return;
@@ -233,18 +260,17 @@ export default defineComponent({
}); });
}, },
focusEnd() {
console.log('focus end');
},
onSorterChange(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
}, },
onFilterChange(filter: JournalTimetableFilter) { onFilterChange(filter: JournalFilter) {
this.journalFilterActive = filter; // this.journalFilterActive = filter;
this.filterList?.filter((f) => f.filterSection === filter.filterSection).forEach((f) => (f.isActive = false));
filter.isActive = true;
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
}, },
+29 -21
View File
@@ -1,11 +1,13 @@
<template> <template>
<div class="journal-stats" v-show="!store.isOffline"> <div class="journal-stats" v-if="!store.isOffline">
<div class="tabs"> <div class="tabs">
<button <button
v-for="tab in data.tabs" v-for="tab in data.tabs"
class="btn--filled" class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen" :data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive" :data-inactive="tab.inactive"
:data-disabled="tab.inactive"
:disabled="tab.inactive"
@click="onTabButtonClick(tab.name)" @click="onTabButtonClick(tab.name)"
> >
{{ $t(tab.titlePath) }} {{ $t(tab.titlePath) }}
@@ -14,7 +16,7 @@
<div class="stats-tab" v-show="areStatsOpen"> <div class="stats-tab" v-show="areStatsOpen">
<keep-alive> <keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" /> <JournalDailyStats v-if="store.currentStatsTab == 'daily'" @toggleStatsOpen="toggleStatsOpen" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" /> <JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive> </keep-alive>
</div> </div>
@@ -22,21 +24,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue'; import { computed, KeepAlive, onMounted, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue'; import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue'; import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../scripts/managers/storageManager';
// Types // Types
type TStatTab = 'daily' | 'driver'; type TStatTab = 'daily' | 'driver';
// Variables // Variables
const store = useStore(); const store = useStore();
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
const areStatsOpen = ref(true); const lastDailyStatsOpen = ref(false);
const lastClickedTab = ref('daily'); const areStatsOpen = ref(false);
const lastClickedTab: Ref<'daily' | 'driver' | null> = ref(null);
let data = reactive({ let data = reactive({
tabs: [ tabs: [
@@ -47,38 +49,44 @@ let data = reactive({
{ {
name: 'driver', name: 'driver',
titlePath: 'journal.driver-stats-title', titlePath: 'journal.driver-stats-title',
inactive: true, // inactive: true,
}, },
] as { name: TStatTab; titlePath: string; inactive?: boolean }[], ] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
}); });
// Methods // Methods
function onTabButtonClick(tab: TStatTab) { function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) { if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
areStatsOpen.value = !areStatsOpen.value;
if (tab == 'daily') {
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
lastDailyStatsOpen.value = areStatsOpen.value;
} }
store.currentStatsTab = tab; store.currentStatsTab = tab;
lastClickedTab.value = tab; lastClickedTab.value = tab;
if (areStatsOpen.value == false) store.currentStatsTab = null;
} }
onActivated(() => { function toggleStatsOpen(open: boolean) {
dailyStatsComp.value?.startFetchingDailyStats(); areStatsOpen.value = open;
}); }
onDeactivated(() => {
dailyStatsComp.value?.stopFetchingDailyStats();
});
watch( watch(
computed(() => store.driverStatsData), computed(() => store.driverStatsData),
(statsData) => { (statsData) => {
data.tabs[1].inactive = statsData ? false : true; store.currentStatsTab = statsData ? 'driver' : lastClickedTab.value;
areStatsOpen.value = statsData ? true : lastClickedTab.value !== null;
lastClickedTab.value = statsData ? 'driver' : 'daily';
if (statsData) areStatsOpen.value = true;
} }
); );
onMounted(() => {
if (StorageManager.getBooleanValue('dailyStatsOpen')) {
areStatsOpen.value = true;
store.currentStatsTab = 'daily';
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,138 +1,250 @@
<template> <template>
<ul class="journal-list"> <div class="journal-list">
<li <transition name="status-anim" mode="out-in">
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory" <div :key="dataStatus">
class="journal_item" <div class="journal_warning" v-if="store.isOffline">
:key="timetable.timetableId" {{ $t('app.offline') }}
> </div>
<div class="journal_item-info">
<div class="info-top">
<span
tabindex="0"
@click="showTimetable(timetable)"
@keydown.enter="showTimetable(timetable)"
style="cursor: pointer"
>
<b class="text--primary">{{ timetable.trainCategoryCode }}&nbsp;</b>
<b>{{ timetable.trainNo }}</b>
| <span>{{ timetable.driverName }}</span> |
<span class="text--grayed">#{{ timetable.timetableId }}</span>
</span>
<span> <Loading v-else-if="dataStatus == DataStatus.Loading" />
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
<b <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
class="info-status" {{ $t('app.error') }}
:class="{ </div>
fulfilled: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9,
terminated: timetable.terminated && !timetable.fulfilled, <div v-else-if="timetableHistory.length == 0" class="journal_warning">
active: !timetable.terminated, {{ $t('app.no-result') }}
}" </div>
<div v-else>
<transition-group tag="ul" name="list-anim">
<li
v-for="{ timetable, stockHistoryComp, stops, showExtraInfo, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
> >
{{ <div class="journal_item-info">
!timetable.terminated <div class="info-general">
? $t('journal.timetable-active') <span
: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9 class="general-train"
? $t('journal.timetable-fulfilled') tabindex="0"
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}` @click.stop="showTimetable(timetable, $event.currentTarget)"
}} @keydown.enter="showTimetable(timetable, $event.currentTarget)"
</b> >
</span> <span class="text--grayed">#{{ timetable.id }}</span>
</div>
<div class="info-route"> <span class="badges" v-if="timetable.skr || timetable.twr">
<b>{{ timetable.route.replace('|', ' - ') }}</b> <span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
</div> <span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
</span>
<hr /> <span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<div class="scenery-list"> <strong>{{ timetable.driverName }}</strong>
<span v-for="(scenery, i) in sceneryList" :key="scenery.name" :class="{ confirmed: scenery.confirmed }"> </span>
<span v-if="i > 0"> &gt;</span>
{{ scenery.name }}
<!-- Data odjazdu ze stacji początkowej --> <span class="general-time">
<span v-if="i == 0" v-html="scenery.beginDateHTML"></span> <b class="info-date"
>{{
new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? localeDateTime(timetable.createdAt, $i18n.locale)
: localeDateTime(timetable.beginDate, $i18n.locale)
}}
</b>
<!-- Data przyjazdu do stacji końcowej --> <b
<span v-if="i == sceneryList.length - 1" v-html="scenery.endDateHTML"> </span> class="info-badge"
</span> :class="{
</div> fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<!-- Status RJ --> <div class="info-route">
<div style="margin: 0.5em 0"> <b>{{ timetable.route.replace('|', ' - ') }}</b>
<span> </div>
<b>{{ $t('journal.route-length') }}</b>
{{ !timetable.fulfilled ? timetable.currentDistance + ' /' : '' }}
{{ timetable.routeDistance }} km
</span>
&bull;
<span>
<b>{{ $t('journal.station-count') }}</b>
{{ timetable.confirmedStopsCount }} /
{{ timetable.allStopsCount }}
</span>
<span class="text--grayed" v-if="!timetable.fulfilled && timetable.currentSceneryName">
&bull;
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
</b>
</span>
</div>
<!-- Nick dyżurnego --> <hr />
<div v-if="timetable.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link class="dispatcher-link" :to="`/journal/dispatchers?dispatcherName=${timetable.authorName}`">
<b>{{ timetable.authorName }}</b>
</router-link>
</div>
<button <!-- Spis postojów -->
v-if="timetable.stockString" <div class="stop-list" v-if="showExtraInfo.value == true">
class="btn--option btn--show" <span
@click="item.showStock.value = !item.showStock.value" v-for="(stop, i) in stops.filter((_, i) =>
> !showExtraInfo.value ? i == 0 || i == stops.length - 1 : true
{{ $t('journal.stock-info') }} )"
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" /> class="stop-list-item"
</button> :key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo.value && i == 1 && stops.length > 2">
... (+{{ stops.length - 2 }}) &gt;
</span>
</span>
<div class="info-extended" v-if="timetable.stockString && item.showStock.value"> <span class="stop-name">{{ stop.stopName }}</span>
<hr /> <span v-html="stop.html"></span>
</span>
</div>
<div> <!-- Status RJ -->
<span class="badge info-badge"> <div class="info-status" style="margin: 0.5em 0">
<span>{{ $t('journal.stock-max-speed') }}</span> <ProgressBar
<span>{{ timetable.maxSpeed }}km/h</span> :progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
</span> :progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
/>
<span class="badge info-badge"> <span>
<span>{{ $t('journal.stock-length') }}</span> <span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }">
<span>{{ timetable.stockLength }}m</span> {{ timetable.currentDistance + ' km' }}
</span> </span>
<span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span>
|
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span>
</span>
<span class="badge info-badge"> <span class="text--grayed" v-if="timetable.currentSceneryName">
<span>{{ $t('journal.stock-mass') }}</span> <b>
<span>{{ Math.floor(timetable.stockMass! / 1000) }}t</span> {{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
</span> {{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
</div>
<ul class="stock-list"> <span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&lpar;</span>
<li v-for="(car, i) in timetable.stockString.split(';')" :key="i">
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div> <span v-if="timetable.currentLocation[1]">
{{ $t('journal.timetable-location-route') }} {{ timetable.currentLocation[1] }}
</span>
<span v-else-if="timetable.currentLocation[0]">
{{ $t('journal.timetable-location-signal') }} {{ timetable.currentLocation[0] }}
</span>
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&rpar;</span>
</b>
</span>
</div>
<button class="btn--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Dodatkowe informacje -->
<div class="info-extended" v-if="timetable.stockString && timetable.stockMass && showExtraInfo.value">
<hr />
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
item.currentHistoryIndex.value == 0
? timetable.stockLength
: stockHistoryComp[item.currentHistoryIndex.value].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(item.currentHistoryIndex.value == 0
? timetable.stockMass!
: stockHistoryComp[item.currentHistoryIndex.value].stockMass || timetable.stockMass) /
1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistoryComp.length > 1">
<button
class="btn--action"
v-for="(sh, i) in stockHistoryComp"
:data-checked="i == item.currentHistoryIndex.value"
@click.stop="item.currentHistoryIndex.value = i"
>
{{ sh.updatedAt }}
</button>
</div>
<ul class="stock-list">
<li
v-for="(car, i) in (item.currentHistoryIndex.value == 0
? timetable.stockString
: stockHistoryComp[item.currentHistoryIndex.value].stockString
).split(';')"
:key="i"
>
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
</div>
</li> </li>
</ul> </transition-group>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div> </div>
</div> </div>
</li> </transition>
</ul>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -140,68 +252,118 @@ import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import styleMixin from '../../mixins/styleMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData'; import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import ProgressBar from '../Global/ProgressBar.vue';
export default defineComponent({ export default defineComponent({
components: { ProgressBar, Loading },
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
props: { props: {
timetableHistory: { timetableHistory: {
type: Array as PropType<TimetableHistory[]>, type: Array as PropType<TimetableHistory[]>,
required: true, required: true,
}, },
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
}, },
mixins: [dateMixin, imageMixin, modalTrainMixin], data() {
return {
DataStatus,
store: useStore(),
};
},
computed: { computed: {
computedTimetableHistory() { computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({ return this.timetableHistory.map((timetable) => ({
timetable, timetable,
sceneryList: this.getSceneryList(timetable), stockHistoryComp: timetable.stockHistory
showStock: ref(false), .slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
}),
showExtraInfo: ref(false),
stops: this.getTimetableStops(timetable),
currentHistoryIndex: ref(0),
})); }));
}, },
}, },
methods: { methods: {
getSceneryList(timetable: TimetableHistory) { getTimetableStops(timetable: TimetableHistory) {
return timetable.sceneriesString.split('%').map((name, i) => { const stopNames = timetable.sceneriesString.split('%');
const beginDateHTML =
' (o. ' +
(timetable.beginDate != timetable.scheduledBeginDate
? `<s class='text--grayed'>${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s> `
: '') +
`<span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML = const beginDateHTML = ` (o. ${
' (p. ' + timetable.beginDate != timetable.scheduledBeginDate
(timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled ? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
? `<s class='text--grayed'>${this.localeTime( : ''
timetable.fulfilled ? timetable.endDate : timetable.scheduledEndDate, } <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
this.$i18n.locale
)}</s> `
: '') +
`<span>${this.localeTime(
timetable.fulfilled || (timetable.terminated && !timetable.fulfilled)
? timetable.scheduledEndDate
: timetable.endDate,
this.$i18n.locale
)}</span>)`;
const abandonedDateHTML = ` (porz. ${this.localeTime( const endDateHTML = ` (p. ${
timetable.fulfilled ? timetable.scheduledEndDate : timetable.endDate, timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
this.$i18n.locale ? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
)})`; : ''
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML, abandonedDateHTML }; return stopNames.map((stopName, i) => {
const confirmed = i < timetable.confirmedStopsCount;
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(timetable.checkpointDeparturesScheduled?.at(i));
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(timetable.checkpointArrivalsScheduled?.at(i));
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML =
(arrivalDateReal && arrivalDateScheduled && arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML =
(departureDateReal &&
departureDateScheduled &&
departureDateReal?.getTime() != departureDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
if (html) html = ` (${html})`;
return { stopName, html, confirmed };
}); });
}, },
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
if (timetable?.terminated) return;
showTimetable(timetable: TimetableHistory) { this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
if (timetable.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
}, },
onImageError(e: Event) { onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png'); imageEl.src = this.getImage('unknown.png');
@@ -211,11 +373,16 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/animations.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; @import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss'; @import '../../styles/JournalSection.scss';
.journal_item {
cursor: pointer;
}
hr { hr {
margin: 0.25em 0; margin: 0.25em 0;
} }
@@ -225,7 +392,7 @@ hr {
margin-right: 0.5em; margin-right: 0.5em;
} }
&-status { &-badge {
padding: 0.05em 0.35em; padding: 0.05em 0.35em;
color: black; color: black;
@@ -242,10 +409,14 @@ hr {
} }
} }
&-top { &-general {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
} }
&-route { &-route {
@@ -255,6 +426,21 @@ hr {
&-extended { &-extended {
margin-top: 0.5em; margin-top: 0.5em;
} }
&-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25em;
} }
ul.stock-list { ul.stock-list {
@@ -269,18 +455,64 @@ ul.stock-list {
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.9em;
} }
li > img {
vertical-align: text-bottom;
max-height: 60px;
}
} }
.scenery-list { .stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.specs-badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
// badge.scss
.badges {
display: flex;
gap: 0.25em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad; color: #adadad;
span.confirmed {
color: #a3eba3; &-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
} }
} }
.btn--show { .btn--show {
display: flex; display: flex;
margin-top: 1em;
font-weight: bold; font-weight: bold;
padding: 0.2em 0.45em; padding: 0.2em 0.45em;
@@ -289,23 +521,8 @@ ul.stock-list {
} }
} }
.info-badge {
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen { @include smallScreen {
.info-top { .journal_item-info {
flex-direction: column;
span {
margin: 0.1em auto;
}
}
.info-extended {
text-align: center; text-align: center;
} }
@@ -314,8 +531,19 @@ ul.stock-list {
justify-content: center; justify-content: center;
} }
.info-status {
justify-content: center;
}
.btn--show { .btn--show {
margin: 1em auto 0 auto; margin: 1em auto 0 auto;
} }
.info-general,
.general-train,
.stock-specs,
.stock-history {
justify-content: center;
}
} }
</style> </style>
@@ -1,33 +1,61 @@
<template> <template>
<section class="scenery-dispatchers-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <table class="scenery-history-table" v-else="historyList.length">
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<ul class="history-list" v-else> <tbody>
<li class="list-item" v-for="historyItem in dispatcherHistoryList"> <tr v-for="historyItem in historyList">
<div> <td>#{{ historyItem.stationHash }}</td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <td>
<span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span> <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<b>{{ historyItem.dispatcherName }}</b> <b>{{ historyItem.dispatcherName }}</b>
</router-link> </router-link>
</div> </td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(historyItem.dispatcherLevel, historyItem.dispatcherIsSupporter)"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
<div v-if="historyItem.timestampTo"> {{ timestampToString(historyItem.timestampFrom) }}
<b>{{ $d(historyItem.timestampFrom) }}</b> - {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
</div>
{{ timestampToString(historyItem.timestampFrom) }} <div class="dispatcher-online" v-else>
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }}) {{ $t('journal.online-since') }}
</div> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
<div class="dispatcher-online" v-else> </div>
{{ $t('journal.online-since') }} </td>
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> </tr>
({{ calculateDuration(historyItem.currentDuration) }}) </tbody>
</div> </table>
</li>
</ul>
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -39,37 +67,53 @@ import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIDa
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
mixins: [dateMixin], mixins: [dateMixin, styleMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
dispatcherHistoryList: [] as DispatcherHistory[], historyList: [] as DispatcherHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
mounted() {
this.fetchAPIData(); async activated() {
// if (this.historyList.length == 0) {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory;
// }
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 30) { async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> {
try { try {
this.dataStatus = DataStatus.Loading;
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
} catch (error) { } catch (error) {
this.dataStatus = DataStatus.Error;
console.error(error); console.error(error);
return null;
} }
}, },
navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
},
}, },
components: { Loading }, components: { Loading },
}); });
@@ -77,23 +121,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.history-list { .level-badge {
padding: 0 0.5em; margin: 0 auto;
}
.list-item {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: left;
background-color: #353535;
padding: 0.5em;
margin: 0.5em 0;
line-height: 1.5em;
} }
.dispatcher-online { .dispatcher-online {
+16 -4
View File
@@ -1,9 +1,13 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<a class="scenery-name" :href="station.generalInfo?.url"> <a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
{{ station.name }} {{ station.name }}
</a> </a>
<div class="scenery-abbrev">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
</div>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div>
</section> </section>
</template> </template>
@@ -26,18 +30,26 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.info-header {
margin-top: 1em;
}
.scenery-name { .scenery-name {
font-weight: bold; font-weight: bold;
position: relative;
font-size: 3em; font-size: 3em;
text-transform: uppercase; text-transform: uppercase;
} }
.scenery-abbrev {
font-size: 1.3em;
color: #aaa;
}
.scenery-hash { .scenery-hash {
margin-top: 0.5em;
color: #aaa; color: #aaa;
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
+21 -14
View File
@@ -1,10 +1,10 @@
<template> <template>
<div class="scenery-info"> <div class="scenery-info">
<section v-if="!timetableOnly"> <section v-if="!timetableOnly">
<div class="info-general" v-if="station.generalInfo"> <div class="scenery-info-general" v-if="station.generalInfo">
<scenery-info-icons :station="station" /> <scenery-info-icons :station="station" />
<div class="general-list"> <div class="scenery-general-list">
<span> <span>
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }} <b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
@@ -26,26 +26,33 @@
</span> </span>
<span v-if="station.generalInfo.project"> <span v-if="station.generalInfo.project">
&bull; <b>{{ $t('scenery.project-title') }}: </b> &bull; <b>{{ $t('scenery.project-title') }}: </b>
<b style="color: salmon">{{ station.generalInfo.project }}</b> <a
style="color: salmon; text-decoration: underline; font-weight: bold"
:href="station.generalInfo.projectUrl"
target="_blank"
>
{{ station.generalInfo.project }}
</a>
</span> </span>
</div> </div>
<scenery-info-routes :station="station" /> <scenery-info-routes :station="station" />
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
<b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b> <b>
{{
$t(
'scenery.authors-title',
{ authors: station.generalInfo.authors.length },
station.generalInfo.authors.length
)
}}:
</b>
{{ station.generalInfo.authors.join(', ') }} {{ station.generalInfo.authors.join(', ') }}
</div> </div>
<br />
<div class="scenery-topic" v-if="station.generalInfo.url">
<a :href="station.generalInfo.url" target="_blank">
&gt; {{ $t('scenery.forum-topic', { name: station.name }) }} &lt;
</a>
</div>
</div> </div>
<div style="margin: 2em 0; height: 2px; background-color: white" /> <div style="margin: 2em 0; height: 2px; background-color: white"></div>
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
@@ -124,11 +131,11 @@ h3.section-header {
margin-top: 1em; margin-top: 1em;
} }
.info-general { .scenery-info-general {
margin-top: 1em; margin-top: 1em;
} }
.general-list { .scenery-general-list {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -21,18 +21,11 @@
</span> </span>
</div> </div>
<span class="status-badge" v-if="station.onlineInfo && onlineFrom > 0"> <StationStatusBadge
OD {{ new Date(onlineFrom).toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }} :statusID="station.onlineInfo?.statusID"
</span> :isOnline="station.onlineInfo ? true : false"
:statusTimestamp="station.onlineInfo?.statusTimestamp"
<span class="status-badge" v-if="station.onlineInfo" :class="station.onlineInfo.statusID"> />
{{ $t(`status.${station.onlineInfo.statusID}`) }}
{{ station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</section> </section>
</template> </template>
@@ -43,20 +36,21 @@ import imageMixin from '../../../mixins/imageMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station'; import Station from '../../../scripts/interfaces/Station';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, imageMixin], mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
},
onlineFrom: {
type: Number,
default: -1,
},
}, },
components: { StationStatusBadge }
onlineFrom: {
type: Number,
default: -1,
},
},
}); });
</script> </script>
@@ -104,3 +98,4 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -1,114 +1,129 @@
<template> <template>
<section class="info-routes" v-if="station.generalInfo"> <section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0"> <div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
<b>{{ $t('scenery.one-way-routes') }}</b> <b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li v-for="route in station.generalInfo.routes.oneWay" @click="setActiveShowLength(route.name)">
v-for="route in station.generalInfo.routes.oneWay" <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span>
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }" <span v-if="route.speed" class="speed">
> {{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
{{ route.name }} </span>
<b v-if="route.SBL">SBL</b> <span v-if="route.SBL" class="sbl">SBL</span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0"> <div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
<b>{{ $t('scenery.two-way-routes') }}</b> <b>{{ $t('scenery.two-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li v-for="(route, i) in station.generalInfo.routes.twoWay" @click="setActiveShowLength(route.name)">
v-for="route in station.generalInfo.routes.twoWay" <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }" <span v-if="route.speed" class="speed">
> {{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
{{ route.name }} <b v-if="route.SBL">SBL</b> </span>
</li> <span v-if="route.SBL" class="sbl">SBL</span>
</ul> </li>
</div> </ul>
</div>
<!-- <div </section>
class="route-info" </template>
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
v-for="route in [...station.generalInfo.routes.oneWay, ...station.generalInfo.routes.twoWay].filter( <script lang="ts">
(route) => route.name != '-' import { defineComponent } from 'vue';
)" import Station from '../../../scripts/interfaces/Station';
:key="route.name"
:title="`Szlak ${route.name}: ${route.isInternal ? 'wewnętrzny' : 'zewnętrzny'}, ${ export default defineComponent({
route.tracks == 2 ? 'dwutorowy' : 'jednotorowy' props: {
}, ${route.catenary ? 'zelektryfikowany' : 'niezelektryfikowany'} z ${route.SBL ? 'SBL' : 'PBL'} ${ station: {
route.TWB ? 'i blokadą dwukierunkową' : '' type: Object as () => Station,
}`" default: {},
> --> },
<!-- <span class="track-name"> },
<b>{{ route.name }}</b>
</span> --> methods: {
<!-- setActiveShowLength(name: string) {
<span class="track-specs"> if (this.activeShowLength.includes(name)) this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1);
{{ route.tracks }}tor else this.activeShowLength.push(name);
<img v-if="route.catenary" :src="icons.trackCatenary" alt="icon track catenary" /> },
<img v-else :src="icons.trackNoCatenary" alt="icon track no catenary" /> },
<img v-if="route.TWB" :src="icons.trackTWB" alt="icon track twb" /> data() {
<img v-if="route.SBL" :src="icons.trackSBL" alt="icon track sbl" /> return {
</span> --> activeShowLength: [] as string[],
<!-- </div> --> };
</section> },
</template> });
</script>
<script lang="ts">
import { defineComponent } from 'vue'; <style lang="scss" scoped>
import Station from '../../../scripts/interfaces/Station'; .info-routes {
display: flex;
export default defineComponent({ justify-content: center;
props: { flex-wrap: wrap;
station: {
type: Object as () => Station, margin: 1em 0;
default: {}, }
},
}, .routes {
}); display: flex;
</script> justify-content: center;
align-items: center;
<style lang="scss" scoped> flex-wrap: wrap;
.info-routes {
display: flex; padding: 0.25em;
justify-content: center; }
flex-wrap: wrap;
ul.routes-list {
margin: 1em 0; margin: 0.45em 0.25em;
} display: flex;
justify-content: center;
.routes { flex-wrap: wrap;
display: flex;
justify-content: center; li {
align-items: center; margin: 0.5em 0.25em;
flex-wrap: wrap; cursor: pointer;
padding: 0.25em; user-select: none;
} -moz-user-select: none;
-webkit-user-select: none;
ul.routes-list {
margin: 0.45em 0.25em; span {
display: flex; padding: 0.2em 0.25em;
background-color: #007599;
li { font-weight: bold;
background-color: #007599;
&.no-catenary {
padding: 0.2em 0.25em; background-color: #686868;
margin-left: 0.25em; }
&.no-catenary { &.internal {
background-color: #686868; text-decoration: underline;
} }
&.internal { &.speed {
text-decoration: underline; background-color: #404040;
} color: #cfcfcf;
}
b {
color: var(--clr-primary); &.sbl {
} color: var(--clr-primary);
} background-color: #404040;
} }
</style>
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
&:first-child {
border-radius: 0.5em 0 0 0.5em;
}
&:only-child {
border-radius: 0.5em;
}
}
}
}
</style>
@@ -1,52 +1,65 @@
<template> <template>
<section class="info-spawn-list"> <section class="info-spawn-list">
<h3 class="spawn-header section-header"> <h3 class="spawn-header section-header">
<img :src="getIcon('spawn')" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
&nbsp;{{ $t('scenery.spawns') }} &nbsp; &nbsp;{{ $t('scenery.spawns') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
</h3> </h3>
<span v-if="station.onlineInfo"> <span v-if="station.onlineInfo">
<span <span
class="badge spawn" class="badge spawn"
v-for="(spawn, i) in station.onlineInfo.spawns" v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + station.onlineInfo?.dispatcherName + i" :key="spawn.spawnName + station.onlineInfo?.dispatcherName + i"
> :data-electrified="spawn.isElectrified"
<span class="spawn_name">{{ spawn.spawnName }}</span> >
<span class="spawn_length">{{ spawn.spawnLength }}m</span> <span class="spawn_name">{{ spawn.spawnName }}</span>
</span> <span class="spawn_length">{{ spawn.spawnLength }}m</span>
</span> </span>
</span>
<span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
>{{ $t('scenery.no-spawns') }} <span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
</span> >{{ $t('scenery.no-spawns') }}
</section> </span>
</template> </section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'; <script lang="ts">
import imageMixin from '../../../mixins/imageMixin'; import { defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station'; import imageMixin from '../../../mixins/imageMixin';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({
mixins: [imageMixin], export default defineComponent({
mixins: [imageMixin],
props: {
station: { props: {
type: Object as () => Station, station: {
default: {}, type: Object as () => Station,
}, default: {},
}, },
}); },
</script>
computed: {
<style lang="scss" scoped> sortedSpawns() {
@import '../../../styles/variables.scss'; return this.station.onlineInfo?.spawns.sort((s1, s2) => (s1.spawnLength < s2.spawnLength ? 1 : -1));
},
.spawn { },
&_length { });
background: $accentCol; </script>
color: black;
} <style lang="scss" scoped>
} @import '../../../styles/variables.scss';
</style>
.spawn {
color: white;
&_length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
}
}
</style>
@@ -1,133 +1,131 @@
<template> <template>
<section class="info-user-list"> <section class="info-user-list">
<h3 class="user-header section-header"> <h3 class="user-header section-header">
<img :src="getIcon('user')" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
&nbsp;{{ $t('scenery.users') }} &nbsp; &nbsp;{{ $t('scenery.users') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span <span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
>&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span> >&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
</h3> </h3>
<div <div
v-for="(train, i) in computedStationTrains" v-for="(train, i) in computedStationTrains"
class="badge user" class="badge user"
:class="train.stopStatus" :class="train.stopStatus"
:key="train.trainId" :key="train.trainId"
tabindex="0" tabindex="0"
@click="selectModalTrain(train.trainId)" @click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId)" @keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
</div> </div>
<div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0"> <div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0">
{{ $t('scenery.no-users') }} {{ $t('scenery.no-users') }}
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue';
import { computed, defineComponent } from 'vue'; import imageMixin from '../../../mixins/imageMixin';
import imageMixin from '../../../mixins/imageMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import routerMixin from '../../../mixins/routerMixin';
import routerMixin from '../../../mixins/routerMixin'; import Station from '../../../scripts/interfaces/Station';
import Station from '../../../scripts/interfaces/Station'; import { useStore } from '../../../store/store';
import { useStore } from '../../../store/store';
export default defineComponent({
export default defineComponent({ mixins: [routerMixin, imageMixin, modalTrainMixin],
mixins: [routerMixin, imageMixin, modalTrainMixin],
props: {
props: { station: {
station: { type: Object as () => Station,
type: Object as () => Station, default: {},
default: {}, },
}, },
},
setup(props) {
setup(props) { const store = useStore();
const store = useStore();
const computedStationTrains = computed(() => {
const computedStationTrains = computed(() => { if (!props.station) return [];
if (!props.station) return [];
const station = props.station as Station;
const station = props.station as Station; if (!station.onlineInfo) return [];
if (!station.onlineInfo) return []; if (!station.onlineInfo.stationTrains) return [];
if (!station.onlineInfo.stationTrains) return [];
return station.onlineInfo.stationTrains.map((train) => {
return station.onlineInfo.stationTrains.map((train) => { const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
return {
return { ...train,
...train, stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable',
stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable', };
}; });
}); });
});
return { computedStationTrains, store };
return { computedStationTrains, store }; },
}, });
}); </script>
</script>
<style lang="scss" scoped>
<style lang="scss" scoped> $no-timetable: #aaa;
$no-timetable: #aaa; $departed: springgreen;
$departed: springgreen; $stopped: #ffa600;
$stopped: #ffa600; $online: gold;
$online: gold; $terminated: salmon;
$terminated: salmon; $disconnected: slategray;
$disconnected: slategray;
.info-user-list {
.info-user-list { width: 100%;
width: 100%;
ul {
ul { display: flex;
display: flex; flex-wrap: wrap;
flex-wrap: wrap; justify-content: center;
justify-content: center; }
} }
}
.user {
.user { cursor: pointer;
cursor: pointer;
&_train {
&_train { color: black;
color: black; background-color: $no-timetable;
background-color: $no-timetable;
transition: background-color 200ms;
transition: background-color 200ms; -ms-transition: background-color 200ms;
-ms-transition: background-color 200ms; -webkit-transition: background-color 200ms;
-webkit-transition: background-color 200ms; }
}
&.no-timetable .user_train {
&.no-timetable .user_train { background-color: $no-timetable;
background-color: $no-timetable; }
}
&.departed > &_train {
&.departed > &_train { background-color: $departed;
background-color: $departed; }
}
&.stopped > &_train {
&.stopped > &_train { background-color: $stopped;
background-color: $stopped; }
}
&.online > &_train {
&.online > &_train { background-color: $online;
background-color: $online; }
}
&.terminated > &_train {
&.terminated > &_train { background-color: $terminated;
background-color: $terminated; }
}
&.disconnected > &_train {
&.disconnected > &_train { background-color: $disconnected;
background-color: $disconnected; }
}
&.offline {
&.offline { background: firebrick;
background: firebrick; pointer-events: none;
pointer-events: none; }
} }
} </style>
</style>
+80 -124
View File
@@ -2,17 +2,37 @@
<section class="scenery-timetable"> <section class="scenery-timetable">
<div class="timetable-header"> <div class="timetable-header">
<h3> <h3>
<img :src="getIcon('timetable')" alt="icon-timetable" />&nbsp; <img :src="getIcon('timetable')" alt="icon-timetable" />
<span>{{ $t('scenery.timetables') }}</span> <span>{{ $t('scenery.timetables') }}</span>
&nbsp;
<span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span> <span>
<span>&nbsp;/&nbsp;</span> <span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
<span class="text--grayed"> <span> / </span>
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} <span class="text--grayed">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
</span>
</span>
<span class="header_links">
<a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img :src="getIcon('pragotron')" alt="icon-pragotron" />
</a>
<a
:href="`https://tablice-td2.web.app/?station=${station.name}`"
target="_blank"
:title="$t('scenery.tablice-link')"
>
<img :src="getIcon('tablice', 'ico')" alt="icon-tablice" />
</a>
</span> </span>
</h3> </h3>
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i"> <span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
{{ (i > 0 && '&bull;') || '' }} {{ (i > 0 && '&bull;') || '' }}
@@ -41,14 +61,14 @@
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
</span> </span>
<transition-group name="timetables-anim"> <transition-group name="list-anim">
<div <div
class="timetable-item" class="timetable-item"
v-for="(scheduledTrain, i) in computedScheduledTrains" v-for="(scheduledTrain, i) in computedScheduledTrains"
:key="scheduledTrain.trainId" :key="scheduledTrain.trainId"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)" @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
@@ -101,25 +121,17 @@
</span> </span>
<span class="schedule-stop"> <span class="schedule-stop">
<span class="stop-time"> <span class="stop-connection">
<span v-if="scheduledTrain.stopInfo.stopTime"> {{ scheduledTrain.arrivingLine }}
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span>
<span v-else>&nbsp;</span>
</span> </span>
<span class="arrow"></span> <span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{ scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : '' }}
</span>
<span class="stop-line"> <span class="stop-connection">
<span> {{ scheduledTrain.departureLine }}
{{ scheduledTrain.arrivingLine }}
</span>
<span></span>
<span>
{{ scheduledTrain.departureLine }}
</span>
</span> </span>
</span> </span>
@@ -168,7 +180,6 @@ import { useStore } from '../../store/store';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -272,22 +283,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/animations.scss';
.timetables-anim-move,
.timetables-anim-enter-active,
.timetables-anim-leave-active {
transition: all 250ms ease;
}
.timetables-anim-enter-from,
.timetables-anim-leave-to {
opacity: 0;
transform: translateY(30px);
}
.timetables-anim-leave-active {
position: absolute;
}
.scenery-timetable { .scenery-timetable {
height: 100%; height: 100%;
@@ -296,24 +292,36 @@ export default defineComponent({
} }
.timetable-header { .timetable-header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 99; z-index: 99;
background-color: #181818; background-color: #181818;
padding: 0.5em;
img {
width: 25px;
vertical-align: middle;
}
h3 { h3 {
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 0.5em;
font-size: 1.3em; font-size: 1.3em;
} }
} }
.header_links {
display: flex;
gap: 0.5em;
margin-left: 0.5em;
}
.timetable { .timetable {
&-count { &-count {
margin-left: 0.5em; margin-left: 0.5em;
@@ -325,8 +333,8 @@ export default defineComponent({
max-width: 1100px; max-width: 1100px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2em 0.5em; gap: 1.2em 0.5em;
overflow: hidden; overflow: hidden;
@@ -352,7 +360,9 @@ export default defineComponent({
&-schedule { &-schedule {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 0.2em;
align-items: center;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
@@ -370,7 +380,8 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
padding: 0.75em 0;
margin-top: 0.5em;
button.checkpoint_item { button.checkpoint_item {
color: #aaa; color: #aaa;
@@ -383,33 +394,6 @@ export default defineComponent({
} }
} }
.arrow {
border: solid white;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 2px;
margin-left: 50px;
position: relative;
transform: rotate(-45deg);
&::before {
content: '';
position: absolute;
display: block;
width: 55px;
height: 3px;
top: 4px;
left: 4px;
transform: translate(-100%, -1px) rotate(45deg);
transform-origin: right bottom;
background: white;
}
}
.general-info { .general-info {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -436,47 +420,34 @@ export default defineComponent({
.schedule { .schedule {
&-arrival, &-arrival,
&-stop,
&-departure { &-departure {
display: flex;
justify-content: center;
align-items: center;
margin: 0 0.3rem;
font-size: 1.15em; font-size: 1.15em;
} }
&-stop { &-stop {
position: relative; display: grid;
display: flex; grid-template-columns: repeat(3, 1fr);
flex-direction: column; gap: 0.5em;
font-size: 0.9em; align-items: end;
padding: 0.3em 0; .stop-connection {
font-size: 0.95em;
.stop-line {
display: flex;
position: absolute;
span {
width: 65px;
word-break: break-all;
}
span:first-child {
text-align: right;
}
span:last-child {
text-align: left;
}
} }
.stop-time { .stop-time {
position: absolute; position: relative;
transform: translateY(-15px); inline-size: max-content;
align-self: center;
font-size: 0.9em;
color: $accentCol; color: $accentCol;
&::after {
content: '\027F6';
display: block;
font-size: 2.2em;
line-height: 0.65em;
}
} }
} }
} }
@@ -486,21 +457,6 @@ export default defineComponent({
font-size: 0.85em; font-size: 0.85em;
} }
.scenery-timetable-list-anim {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active {
transition: all 100ms ease-out;
}
&-leave-active {
transition: all 100ms ease-out 100ms;
}
}
@include smallScreen { @include smallScreen {
.timetable-item { .timetable-item {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -1,35 +1,51 @@
<template> <template>
<section class="scenery-timetables-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <table class="scenery-history-table" v-else>
<ul class="history-list" v-else> <thead>
<li class="list-item" v-for="historyItem in sceneryHistoryList"> <th>{{ $t('scenery.timetables-history-id') }}</th>
<div> <th>{{ $t('scenery.timetables-history-number') }}</th>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> <th>{{ $t('scenery.timetables-history-route') }}</th>
{{ localeTime(historyItem.beginDate, $i18n.locale) }} <th>{{ $t('scenery.timetables-history-driver') }}</th>
</div> <th>{{ $t('scenery.timetables-history-author') }}</th>
<th>{{ $t('scenery.timetables-history-date') }}</th>
</thead>
<div> <tbody>
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`"> <tr v-for="historyItem in historyList">
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <td>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b> <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
<div>{{ historyItem.driverName }}</div> </td>
</router-link> <td>
</div> <b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
{{ historyItem.trainNo }}
<div>{{ historyItem.route.replace('|', ' -> ') }}</div> </td>
<!-- <div>{{ historyItem.routeDistance }} km</div> --> <td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<div> <td>{{ historyItem.driverName }}</td>
{{ $t('scenery.timetable-author-title') }}: <td>
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b> <router-link
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> v-if="historyItem.authorName"
</div> :to="`/journal/timetables?authorName=${historyItem.authorName}`"
>{{ historyItem.authorName }}
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> --> </router-link>
</li> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</ul> </td>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</td>
</tr>
</tbody>
</table>
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -41,37 +57,48 @@ import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfa
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
sceneryHistoryList: [] as TimetableHistory[], historyList: [] as TimetableHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
mounted() {
this.fetchAPIData(); async activated() {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory.timetables;
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) { async fetchAPIData(countFrom = 0, countLimit = 15): Promise<SceneryTimetableHistory | null> {
try { try {
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getIssuedTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data; const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
this.sceneryHistoryList = historyAPIData.sceneryTimetables;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null;
} }
}, },
navigateToHistory() {
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
},
}, },
components: { Loading }, components: { Loading },
}); });
@@ -79,34 +106,5 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
.history-list {
padding: 0 0.5em;
}
.list-item {
display: grid;
grid-template-columns: 1fr 2fr 2fr 1fr;
gap: 1em;
align-items: center;
background-color: #353535;
padding: 0.5em;
margin: 0.5em 0;
line-height: 1.5em;
}
@include smallScreen {
.list-item {
grid-template-columns: 1fr 1fr;
}
}
</style> </style>
@@ -1,47 +1,19 @@
<template> <template>
<div class="general-status"> <div class="general-status">
<span :class="scheduledTrain.stopStatus"> <span :class="computedScheduledTrain.stopStatus" :title="computedScheduledTrain.stopStatusDescription">
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ computedScheduledTrain.stopStatusIndicator }}
<span v-if="scheduledTrain.prevDepartureLine">({{ scheduledTrain.prevDepartureLine }})</span>
{{ scheduledTrain.prevStationName }}
&gt;<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName || '---' }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'departed'">
&gt;&gt; <span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'departed-away'">
&gt;&gt;&gt;
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'online'">
&gt;
<span v-if="scheduledTrain.nextArrivalLine">
({{ scheduledTrain.nextArrivalLine }}) {{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="!scheduledTrain.nextStationName">{{ $t('timetables.end') }}</span>
<span v-else>{{ scheduledTrain.nextStationName }}</span>
</span>
<span v-else-if="scheduledTrain.stopStatus == 'stopped'">
&gt;
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
{{ scheduledTrain.nextStationName }}
</span>
<span v-else-if="scheduledTrain.stopStatus == 'terminated'">X {{ $t('timetables.terminated') }}</span>
</span> </span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain'; import { ScheduledTrain, StopStatus } from '../../scripts/interfaces/ScheduledTrain';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -50,6 +22,58 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
computed: {
computedScheduledTrain(): ScheduledTrainComp {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = this.scheduledTrain;
const prevDepartureIndicator = prevDepartureLine ? `(${prevDepartureLine}) ${prevStationName}` : '---';
const nextArrivalIndicator = nextArrivalLine ? `(${nextArrivalLine}) ${nextStationName}` : '---';
let stopStatusDescription = '',
stopStatusIndicator = '';
switch (stopStatus) {
case StopStatus.arriving:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', { prevStationName, prevDepartureLine });
break;
case StopStatus.online:
case StopStatus.stopped:
stopStatusIndicator = nextArrivalLine
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
: '';
break;
case StopStatus.departed:
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
stopStatusDescription = this.$t('timetables.desc-departed', { nextStationName, nextArrivalLine });
break;
case StopStatus['departed-away']:
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
stopStatusDescription = this.$t('timetables.desc-departed-away', { nextStationName, nextArrivalLine });
break;
case StopStatus.terminated:
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
stopStatusDescription = this.$t('timetables.desc-terminated');
break;
default:
break;
}
return {
...this.scheduledTrain,
stopStatusDescription,
stopStatusIndicator,
};
},
},
}); });
</script> </script>
@@ -86,3 +110,4 @@ export default defineComponent({
} }
} }
</style> </style>
+54 -61
View File
@@ -1,7 +1,10 @@
<template> <template>
<button class="btn--action" :class="option.section" :data-selected="option.value" @click="handleChange"> <label @dblclick="handleDbClick">
{{ $t(`filters.${option.id}`) }} <input v-model="option.value" type="checkbox" :class="option.section" :name="option.id" />
</button> <span>
{{ $t(`filters.${option.id}`) }}
</span>
</label>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -29,78 +32,68 @@ export default defineComponent({
filterStore: useStationFiltersStore(), filterStore: useStationFiltersStore(),
}; };
}, },
methods: {
handleChange() {
this.option.value = !this.option.value;
this.filterStore.changeFilterValue({ watch: {
name: this.option.name, 'option.value'() {
value: !this.option.value, this.filterStore.changeFilterValue(this.option.name, !this.option.value);
}); },
},
methods: {
handleDbClick(e: Event) {
e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id;
this.option.value = true;
this.filterStore.inputs.options
.filter((option) => {
return option.section == this.option.section && option.id != this.option.id;
})
.forEach((option) => {
option.value = !this.option.value;
});
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$accessCol: #e03b07; @import '../../styles/variables.scss';
$controlCol: #0085ff;
$signalCol: #bf7c00;
$statusCol: #349b32;
$saveCol: #28a826;
$routesCol: #9049c0;
button { label {
width: 100%; position: relative;
padding: 0.4em; user-select: none;
border-radius: 0.4em; -webkit-user-select: none;
-moz-user-select: none;
&:focus-visible { span {
outline: 1px solid white; cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
background-color: #444;
} }
&[data-selected='true'] { span:hover {
&.access { background-color: #555;
background-color: $accessCol; }
box-shadow: 0 0 6px 1px $accessCol;
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: forestgreen;
font-weight: bold;
} }
&.control { &:focus-visible + span {
background-color: $controlCol; outline: 1px solid $accentCol;
box-shadow: 0 0 6px 1px $controlCol;
}
&.signals {
background-color: $signalCol;
box-shadow: 0 0 6px 1px $signalCol;
}
&.routes {
background-color: $routesCol;
box-shadow: 0 0 6px 1px $routesCol;
}
&.status {
background-color: $statusCol;
box-shadow: 0 0 6px 1px $statusCol;
}
&.save {
background-color: $saveCol;
box-shadow: 0 0 6px 1px $saveCol;
}
&.troll {
background-color: firebrick;
box-shadow: 0 0 6px 1px firebrick;
}
&.mode {
background-color: lightgreen;
color: black;
font-weight: 500;
} }
} }
} }
</style> </style>
+102 -53
View File
@@ -4,6 +4,7 @@
<button class="btn--filled btn--image" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="filter icon" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} [F] {{ $t('options.filters') }} [F]
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
</button> </button>
<label for="scenery-search"> <label for="scenery-search">
@@ -26,15 +27,44 @@
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl"> <div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
<div class="card_content"> <div class="card_content">
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p>
<section class="card_options"> <section class="card_options">
<filter-option <!-- QUICK ACTIONS (TODO) -->
v-for="(option, i) in filterStore.inputs.options" <!-- <div class="quick-actions">
:option="option" <h3 class="text--primary">{{ $t('filters.sections.quick') }}</h3>
:key="i" <hr />
@optionChange="handleChange"
/> <div>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-available')">
{{ $t('filters.all-available') }}
</button>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-free')">
{{ $t('filters.all-free') }}
</button>
</div>
</div> -->
<div class="option-section" v-for="section in filterStore.inputs.optionSections">
<h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
</h3>
<hr />
<div class="section-inputs">
<FilterOption
v-for="(option, i) in filterStore.inputs.options.filter((o) => o.section == section)"
:option="option"
:key="i"
/>
</div>
</div>
</section> </section>
<section class="card_timestamp" style="text-align: center"> <section class="card_timestamp" style="text-align: center">
<div>{{ $t('filters.minimum-hours-title') }}</div> <div>{{ $t('filters.minimum-hours-title') }}</div>
<span class="clock"> <span class="clock">
@@ -80,18 +110,25 @@
</div> </div>
</div> </div>
</section> </section>
<section class="card_actions">
<div class="action-buttons">
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
{{ $t('filters.save') }}
</button>
<button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div>
</section>
</div> </div>
<section class="card_actions">
<div class="action-buttons">
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
{{ $t('filters.save') }}
</button>
<button
class="btn--action"
@click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
>
{{ $t('filters.reset') }}
</button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div>
</section>
</div> </div>
</transition> </transition>
</section> </section>
@@ -156,6 +193,10 @@ export default defineComponent({
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase())) .filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1)); .sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
}, },
currentOptionsActive() {
return true;
},
}, },
watch: { watch: {
@@ -181,22 +222,10 @@ export default defineComponent({
this.isVisible = !this.isVisible; this.isVisible = !this.isVisible;
}, },
handleChange(change: { name: string; value: boolean }) {
this.filterStore.changeFilterValue({
name: change.name,
value: !change.value,
});
if (this.saveOptions) StorageManager.setBooleanValue(change.name, change.value);
},
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue({ this.filterStore.changeFilterValue(target.name, target.value);
name: target.name,
value: target.value,
});
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value); if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
}, },
@@ -210,11 +239,7 @@ export default defineComponent({
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue({ this.filterStore.changeFilterValue(name, value);
name,
value,
});
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value); if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
}, },
@@ -281,6 +306,14 @@ export default defineComponent({
} }
.card { .card {
display: grid;
grid-template-rows: 1fr auto;
&_info {
background-color: #111;
padding: 0.5em;
}
&_controls { &_controls {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
@@ -292,13 +325,13 @@ export default defineComponent({
} }
&_content { &_content {
padding: 1em 0.5em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1em; gap: 1em;
overflow: auto;
max-height: 90vh;
padding: 1em;
} }
&_title { &_title {
@@ -309,18 +342,6 @@ export default defineComponent({
text-align: center; text-align: center;
} }
&_options {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(4, 1fr);
gap: 0.5em;
@include smallScreen() {
grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));
grid-template-rows: auto;
}
}
&_regions { &_regions {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -391,6 +412,9 @@ export default defineComponent({
} }
&_actions { &_actions {
width: 100%;
padding: 0.5em;
.filter-option { .filter-option {
max-width: 50%; max-width: 50%;
margin: 0 auto; margin: 0 auto;
@@ -409,14 +433,39 @@ export default defineComponent({
padding: 0.5em; padding: 0.5em;
&[data-selected='true'] { &[data-selected='true'] {
background-color: lightgreen; background-color: forestgreen;
color: black;
} }
} }
} }
} }
} }
.option-section h3 {
display: flex;
align-items: center;
margin-bottom: 0.25em;
gap: 0.5em;
button {
padding: 0.15em;
color: coral;
}
}
.section-inputs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider { .slider {
display: flex; display: flex;
align-items: center; align-items: center;
+56 -39
View File
@@ -1,33 +1,39 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<button class="return-btn" @click="scrollToTop" v-if="showReturnButton">
<img :src="icons.arrow" alt="return arrow" />
</button>
<div class="table_wrapper"> <div class="table_wrapper">
<table> <table>
<thead> <thead>
<tr> <tr>
<th v-for="(id, i) in headIds" :key="id" @click="() => changeSorter(i)"> <th
v-for="(headerName, i) in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
>
<span class="header_wrapper"> <span class="header_wrapper">
<div v-html="$t(`sceneries.${id}`)"></div> <div v-html="$t(`sceneries.${headerName}`)"></div>
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i" v-if="sorterActive.headerName == headerName"
:src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
</th> </th>
<th v-for="(id, i) in headIconsIds" :key="id" @click="() => changeSorter(i + 7)"> <th
v-for="(headerName, i) in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
>
<span class="header_wrapper"> <span class="header_wrapper">
<img :src="getIcon(id)" :alt="id" :title="$t(`sceneries.${id}s`)" /> <img :src="getIcon(headerName)" :alt="headerName" :title="$t(`sceneries.${headerName}`)" />
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.index == i + 7" v-if="sorterActive.headerName == headerName"
:src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')" :src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
alt="sort icon" alt="sort icon"
/> />
@@ -83,16 +89,11 @@
</td> </td>
<td class="station_status"> <td class="station_status">
<span class="status-badge" :class="station.onlineInfo.statusID" v-if="station.onlineInfo"> <StationStatusBadge
{{ $t(`status.${station.onlineInfo.statusID}`) }} :statusID="station.onlineInfo?.statusID"
{{ :isOnline="station.onlineInfo ? true : false"
station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' :statusTimestamp="station.onlineInfo?.statusTimestamp"
}} />
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</td> </td>
<td class="station_dispatcher-name"> <td class="station_dispatcher-name">
@@ -190,25 +191,31 @@
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td class="station_users" :class="{ inactive: !station.onlineInfo }">
<span> <span>
<span class="highlight">{{ station.onlineInfo?.currentUsers || '0' }}</span> <span class="highlight">{{ station.onlineInfo?.currentUsers || 0 }}</span>
/ /
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span> <span class="highlight">{{ station.onlineInfo?.maxUsers || 0 }}</span>
</span> </span>
</td> </td>
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }"> <td class="station_spawns" :class="{ inactive: !station.onlineInfo }">
<span class="highlight">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span>{{ station.onlineInfo?.spawns.length || 0 }}</span>
</td> </td>
<td class="station_schedules" :class="{ inactive: !station.onlineInfo }"> <td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
<span> <span class="highlight">
<span class="highlight"> {{ station.onlineInfo?.scheduledTrains?.length || 0 }}
{{ station.onlineInfo?.scheduledTrains?.length || '0' }} </span>
</span> </td>
/
<span style="color: #bbb"> <td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} <span style="color: #ccc">
</span> {{ station.onlineInfo?.scheduledTrains?.filter((train) => !train.stopInfo.confirmed).length || 0 }}
</span>
</td>
<td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
<span style="color: #66ff6c">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || 0 }}
</span> </span>
</td> </td>
</tr> </tr>
@@ -236,6 +243,8 @@ import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -245,12 +254,12 @@ export default defineComponent({
}, },
}, },
components: { Loading }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin], mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'], headIconsIds,
headIconsIds: ['user', 'spawn', 'timetable'], headIds,
lastSelectedStationName: '', lastSelectedStationName: '',
}), }),
@@ -291,8 +300,10 @@ export default defineComponent({
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(i: number) { changeSorter(headerName: HeadIdsTypes) {
this.stationFiltersStore.changeSorter(i); if (headerName == 'general' || headerName == 'routes') return;
this.stationFiltersStore.changeSorter(headerName);
}, },
}, },
}); });
@@ -349,9 +360,15 @@ table {
position: sticky; position: sticky;
top: 0; top: 0;
min-width: 75px; &.header-text {
min-width: 140px;
}
padding: 0.5em; &.header-image {
min-width: 60px;
}
padding: 0.5em 0.25em;
background-color: $bgCol; background-color: $bgCol;
white-space: pre-wrap; white-space: pre-wrap;
+29 -79
View File
@@ -1,23 +1,24 @@
<template> <template>
<div class="train-info" tabindex="0"> <div class="train-info">
<section class="train-route"> <section class="train-route">
<div class="train_general"> <div class="train_general">
<span> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span> <span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
<span class="timetable_warnings"> <span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span> <span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span> <span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">SKR</span>
</span>
<strong class="timetable-category" v-if="train.timetableData">
{{ train.timetableData.category }}
</strong>
<strong class="train-number">&nbsp;{{ train.trainNo }}</strong>
|
<span class="train-driver" :class="{ supporter: train.isSupporter }">{{ train.driverName }}</span>
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
</span> </span>
<strong>
<span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }}&nbsp;</span>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b class="level-badge driver" :style="calculateExpStyle(train.driverLevel, train.isSupporter)">
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<span>{{ train.driverName }}</span>
</div> </div>
<div class="timetable_route" v-if="train.timetableData"> <div class="timetable_route" v-if="train.timetableData">
@@ -40,15 +41,7 @@
</div> </div>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData"> <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<!-- <span> </span> --> <ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="timetable_progress-bar">
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; -->
<span class="bar-bg"></span>
<span
class="bar-fg"
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
></span>
</span>
<span class="timetable_progress-distance"> <span class="timetable_progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / &nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
@@ -94,8 +87,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -103,19 +98,19 @@ export default defineComponent({
type: Object as () => Train, type: Object as () => Train,
required: true, required: true,
}, },
extended: { extended: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
mixins: [trainInfoMixin, imageMixin, styleMixin],
mixins: [trainInfoMixin, imageMixin], components: { ProgressBar },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
.image-warning { .image-warning {
height: 1em; height: 1em;
@@ -149,7 +144,6 @@ export default defineComponent({
} }
.timetable-id { .timetable-id {
margin-right: 0.3em;
color: #d2d2d2; color: #d2d2d2;
} }
@@ -159,11 +153,7 @@ export default defineComponent({
display: inline-block; display: inline-block;
text-align: center; text-align: center;
width: 1.25em; padding: 0 0.25em;
height: 1.25em;
border-radius: 50%;
margin-left: 0.25em;
} }
.timetable_stops { .timetable_stops {
@@ -174,31 +164,15 @@ export default defineComponent({
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25em;
margin-right: 1.5em;
} }
.train-status-badges { .train-status-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
}
.train-badge { gap: 0.25em;
padding: 0.15em 0.35em;
margin-right: 0.3em;
font-weight: bold;
font-size: 0.9em;
&.twr {
background-color: var(--clr-twr);
}
&.skr {
background-color: var(--clr-skr);
}
&.offline {
background-color: #b83b2d;
}
} }
.train-driver { .train-driver {
@@ -216,7 +190,8 @@ export default defineComponent({
} }
.timetable_warnings { .timetable_warnings {
color: black; display: flex;
gap: 0.25em;
} }
.timetable_progress { .timetable_progress {
@@ -225,31 +200,6 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
} }
.timetable_progress-bar {
position: relative;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
}
.timetable_progress-distance { .timetable_progress-distance {
margin-right: 0.25em; margin-right: 0.25em;
} }
+41 -23
View File
@@ -43,29 +43,34 @@
<h1 class="option-title">{{ $t('options.sort-title') }}</h1> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<div class="options_sorters"> <div class="options_sorters">
<div v-for="opt in translatedSorterOptions"> <button
v-for="opt in translatedSorterOptions"
class="sort-option btn--option"
:data-selected="opt.id == sorterActive.id"
@click="onSorterChange(opt)"
>
{{ opt.value.toUpperCase() }}
</button>
</div>
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div v-for="section in Object.keys(TrainFilterSection)">
<button <button
class="sort-option btn--option" class="btn--option"
:data-selected="opt.id == sorterActive.id" v-for="filter in trainFilterList.filter((f) => f.section == section)"
@click="onSorterChange(opt)" :data-inactive="!filter.isActive"
@click="onFilterChange(filter)"
> >
{{ opt.value.toUpperCase() }} {{ $t(`options.filter-${filter.id}`) }}
</button> </button>
</div> </div>
</div> </div>
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1> <div class="filter-actions">
<div class="options_filters"> <div></div>
<div class="filter-option" v-for="filter in trainFilterList"> <button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
<button class="btn--option" :data-inactive="!filter.isActive" @click="onFilterChange(filter)">
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
<div class="filter-actions">
<button class="btn--action" @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -77,9 +82,10 @@
import { defineComponent, inject, PropType } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { TrainFilterSection } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
@@ -101,6 +107,7 @@ export default defineComponent({
return { return {
showOptions: false, showOptions: false,
lastSelectedFilter: null as TrainFilter | null, lastSelectedFilter: null as TrainFilter | null,
TrainFilterSection,
}; };
}, },
@@ -183,13 +190,24 @@ export default defineComponent({
margin: 0 auto; margin: 0 auto;
} }
.filter-option { .options_sorters {
display: flex;
grid-template-columns: repeat(3, 1fr);
}
.options_filters > div {
display: flex;
width: 100%;
gap: 0.5em;
button { button {
color: white; width: 100%;
color: springgreen;
font-weight: bold; font-weight: bold;
&[data-disabled='true'] { &[data-inactive='true'] {
color: #888; color: #aaa;
} }
} }
} }
@@ -201,7 +219,7 @@ export default defineComponent({
margin-top: 1em; margin-top: 1em;
button { > * {
width: 100%; width: 100%;
} }
} }
+4 -1
View File
@@ -89,6 +89,7 @@ import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
import { useStore } from '../../store/store';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
export default defineComponent({ export default defineComponent({
@@ -106,6 +107,8 @@ export default defineComponent({
setup(props) { setup(props) {
return { return {
store: useStore(),
lastConfirmed: computed(() => { lastConfirmed: computed(() => {
return props.train.timetableData!.followingStops.findIndex( return props.train.timetableData!.followingStops.findIndex(
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped (stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
@@ -199,7 +202,6 @@ ul.stock-list {
img { img {
max-height: 60px; max-height: 60px;
max-width: 320px;
} }
} }
@@ -424,3 +426,4 @@ ul.stop_list > li.stop {
} }
} }
</style> </style>
+7 -19
View File
@@ -12,22 +12,18 @@
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0"> <transition-group name="list-anim" tag="ul" class="train-list" v-else>
<b class="warning-timeout">?</b>
{{ $t('trains.timeout') }}
</div> -->
<ul class="train-list" v-else>
<li <li
class="train-row" class="train-row"
v-for="train in currentTrains" v-for="train in currentTrains"
:key="train.trainId" :key="train.trainId"
@click.stop="selectModalTrain(train.trainId)" tabindex="0"
@keydown.enter="selectModalTrain(train.trainId)" @click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" />
</li> </li>
</ul> </transition-group>
</div> </div>
</transition> </transition>
</div> </div>
@@ -71,18 +67,9 @@ export default defineComponent({
id: string | number; id: string | number;
dir: number; dir: number;
}, },
distanceLimitExceeded: computed(
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1
),
}; };
}, },
computed: {
trainNumbersWithTimeouts() {
return this.store.trainList.filter((train) => train.isTimeout).map((train) => train.trainNo);
},
},
activated() { activated() {
const query = this.$route.query; const query = this.$route.query;
if (query.trainNo && query.driverName) { if (query.trainNo && query.driverName) {
@@ -98,6 +85,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/animations.scss';
.anim { .anim {
&-enter-from, &-enter-from,
@@ -158,7 +146,7 @@ img.train-image {
.train { .train {
&-list { &-list {
overflow: auto; position: relative;
@include smallScreen() { @include smallScreen() {
width: 100%; width: 100%;
@@ -1,28 +1,46 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType"; import { JournalFilterSection, JournalFilterType } from '../../scripts/enums/JournalFilterType';
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes"; import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export const journalTimetableFilters: JournalTimetableFilter[] = [ export const journalTimetableFilters: JournalFilter[] = [
{ {
id: JournalFilterType.all, id: JournalFilterType.ALL,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: true, isActive: true,
}, },
{ {
id: JournalFilterType.active, id: JournalFilterType.ACTIVE,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false, isActive: false,
}, },
{ {
id: JournalFilterType.fulfilled, id: JournalFilterType.FULFILLED,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false, isActive: false,
}, },
{ {
id: JournalFilterType.abandoned, id: JournalFilterType.ABANDONED,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false,
},
{
id: JournalFilterType.TWR_SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: true,
},
{
id: JournalFilterType.TWR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false,
},
{
id: JournalFilterType.SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false, isActive: false,
}, },
]; ];
+32 -3
View File
@@ -1,33 +1,58 @@
import { TrainFilterType } from '../../scripts/enums/TrainFilterType'; import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes'; import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
id: TrainFilterType.twr, id: TrainFilterType.twr,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.skr, id: TrainFilterType.skr,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true, isActive: true,
}, },
{
id: TrainFilterType.common,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true,
},
{ {
id: TrainFilterType.passenger, id: TrainFilterType.passenger,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.freight, id: TrainFilterType.freight,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.other, id: TrainFilterType.other,
section: TrainFilterSection.TIMETABLE_TYPE,
isActive: true,
},
{
id: TrainFilterType.withComments,
section: TrainFilterSection.COMMENTS,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.comments, id: TrainFilterType.noComments,
section: TrainFilterSection.COMMENTS,
isActive: true,
},
{
id: TrainFilterType.withTimetable,
section: TrainFilterSection.TIMETABLE,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.noTimetable, id: TrainFilterType.noTimetable,
section: TrainFilterSection.TIMETABLE,
isActive: true, isActive: true,
}, },
]; ];
@@ -37,6 +62,10 @@ export const sorterOptions = [
id: 'distance', id: 'distance',
value: 'kilometraż', value: 'kilometraż',
}, },
{
id: 'id',
value: 'id rozkładu',
},
{ {
id: 'progress', id: 'progress',
value: 'przebyta trasa', value: 'przebyta trasa',
+65 -46
View File
@@ -1,41 +1,38 @@
{ {
"optionSections": ["reality", "package-access", "access", "control", "addons", "blockades", "signals", "status"],
"options": [ "options": [
{
"id": "default",
"name": "default",
"iconName": "td2",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"iconName": "",
"section": "access",
"value": true,
"defaultValue": true
},
{ {
"id": "real", "id": "real",
"name": "real", "name": "real",
"iconName": "lock", "section": "reality",
"section": "access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "fictional", "id": "fictional",
"name": "fictional", "name": "fictional",
"iconName": "user", "section": "reality",
"section": "access", "value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "non-public", "id": "non-public",
"name": "nonPublic", "name": "nonPublic",
"iconName": "user",
"section": "access", "section": "access",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -43,7 +40,6 @@
{ {
"id": "unavailable", "id": "unavailable",
"name": "unavailable", "name": "unavailable",
"iconName": "user",
"section": "access", "section": "access",
"value": false, "value": false,
"defaultValue": false "defaultValue": false
@@ -51,7 +47,6 @@
{ {
"id": "abandoned", "id": "abandoned",
"name": "abandoned", "name": "abandoned",
"iconName": "user",
"section": "access", "section": "access",
"value": false, "value": false,
"defaultValue": false "defaultValue": false
@@ -59,7 +54,6 @@
{ {
"id": "SPK", "id": "SPK",
"name": "SPK", "name": "SPK",
"iconName": "SPK",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -67,7 +61,6 @@
{ {
"id": "SCS", "id": "SCS",
"name": "SCS", "name": "SCS",
"iconName": "SCS",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -75,15 +68,21 @@
{ {
"id": "SPE", "id": "SPE",
"name": "SPE", "name": "SPE",
"iconName": "SPE", "section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "manual", "id": "SCS-M",
"name": "ręczne", "name": "mechaniczne+SCS",
"iconName": "ręczne",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -91,7 +90,27 @@
{ {
"id": "mechanical", "id": "mechanical",
"name": "mechaniczne", "name": "mechaniczne",
"iconName": "mechaniczne", "section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control", "section": "control",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -99,23 +118,34 @@
{ {
"id": "SUP", "id": "SUP",
"name": "SUP", "name": "SUP",
"iconName": "SUP", "section": "addons",
"section": "control", "value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "SBL", "id": "SBL",
"name": "SBL", "name": "SBL",
"iconName": "SBL", "section": "blockades",
"section": "routes", "value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
}, },
{ {
"id": "modern", "id": "modern",
"name": "współczesna", "name": "współczesna",
"iconName": "współczesna",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -123,7 +153,6 @@
{ {
"id": "semaphores", "id": "semaphores",
"name": "kształtowa", "name": "kształtowa",
"iconName": "kształtowa",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -131,7 +160,6 @@
{ {
"id": "mixed", "id": "mixed",
"name": "mieszana", "name": "mieszana",
"iconName": "mieszana",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -139,7 +167,6 @@
{ {
"id": "historical", "id": "historical",
"name": "historyczna", "name": "historyczna",
"iconName": "historyczna",
"section": "signals", "section": "signals",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -148,7 +175,6 @@
{ {
"id": "free", "id": "free",
"name": "free", "name": "free",
"iconName": "",
"section": "status", "section": "status",
"value": false, "value": false,
@@ -157,7 +183,6 @@
{ {
"id": "occupied", "id": "occupied",
"name": "occupied", "name": "occupied",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -166,7 +191,6 @@
{ {
"id": "endingStatus", "id": "endingStatus",
"name": "endingStatus", "name": "endingStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -175,7 +199,6 @@
{ {
"id": "afkStatus", "id": "afkStatus",
"name": "afkStatus", "name": "afkStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -184,7 +207,6 @@
{ {
"id": "noSpaceStatus", "id": "noSpaceStatus",
"name": "noSpaceStatus", "name": "noSpaceStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -193,7 +215,6 @@
{ {
"id": "unavailableStatus", "id": "unavailableStatus",
"name": "unavailableStatus", "name": "unavailableStatus",
"iconName": "",
"section": "status", "section": "status",
"value": true, "value": true,
@@ -254,7 +275,6 @@
{ {
"id": "include-selected", "id": "include-selected",
"name": "include-selected", "name": "include-selected",
"iconName": "",
"section": "mode", "section": "mode",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
@@ -262,7 +282,6 @@
{ {
"id": "save", "id": "save",
"name": "save", "name": "save",
"iconName": "",
"section": "mode", "section": "mode",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
+125 -31
View File
@@ -1,6 +1,9 @@
{ {
"general": { "general": {
"and": " and " "and": " and ",
"refresh": "REFRESH",
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
}, },
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
@@ -14,6 +17,9 @@
"migration-confirm": "Roger that!", "migration-confirm": "Roger that!",
"offline": "App is in the offline mode!" "offline": "App is in the offline mode!"
}, },
"footer": {
"discord": "Stacjownik Discord server"
},
"update": { "update": {
"title": "New version of the app is available!", "title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!", "paragraph1": "Enjoy the application and may the green signal be with you!",
@@ -34,7 +40,7 @@
"desc": { "desc": {
"control-type": "Control type: ", "control-type": "Control type: ",
"signals-type": "Signals type: ", "signals-type": "Signals type: ",
"SBL": "This scenery has automatic line blockade system on following routes: ", "SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP application (level crossing remote control simulator)", "SUP": "Requires the SUP application (level crossing remote control simulator)",
"TWB-all": "This scenery has two-way route blockade on all routes", "TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ", "TWB-routes": "This scenery has two-way route blockade on following routes: ",
@@ -93,47 +99,75 @@
"search-dispatcher": "Dispatcher name", "search-dispatcher": "Dispatcher name",
"search-station": "Scenery name", "search-station": "Scenery name",
"search-author": "Timetable author name", "search-author": "Timetable author name",
"search-date": "Timetable date (CEST / GMT+2)", "search-issuedFrom": "Origin scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
"sort-mass": "mass", "sort-mass": "mass",
"sort-speed": "speed", "sort-speed": "speed",
"sort-length": "length", "sort-length": "length",
"sort-distance": "distance", "sort-routeDistance": "route distance",
"sort-timetable": "train no.", "sort-timetable": "train no.",
"sort-progress": "route progress", "sort-progress": "route progress",
"sort-delay": "current delay", "sort-delay": "current delay",
"sort-id": "timetable id",
"sort-total-stops": "total stops", "sort-allStopsCount": "total stops",
"sort-beginDate": "date", "sort-beginDate": "date",
"sort-timetableId": "timetable ID", "sort-timetableId": "timetable ID",
"sort-timestampFrom": "date", "sort-timestampFrom": "date",
"sort-duration": "duration", "sort-duration": "duration",
"filter-comments": "COMMENTS", "filter-noComments": "NO COMMENTS",
"filter-twr": "TWR", "filter-withComments": "COMMENTS",
"filter-skr": "SKR", "filter-twr": "HIGH RISK CARGO",
"filter-skr": "EXCEEDED GAUGE",
"filter-twr-skr": "ALL TYPES",
"filter-common": "NO WARNINGS",
"filter-passenger": "PASSENGER", "filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT", "filter-freight": "FREIGHT",
"filter-other": "OTHER", "filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE", "filter-noTimetable": "NO TIMETABLE",
"filter-withTimetable": "TIMETABLE",
"filter-reset": "RESET FILTERS", "filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS", "filter-clear": "CLEAR FILTERS",
"filter-section-timetable-status": "TIMETABLE STATUS",
"filter-section-twrskr": "WARNINGS",
"filter-all": "ALL ENTRIES", "filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED", "filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED", "filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE" "filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"desc": " &bull; Left mouse click: select / unselect chosen filter <br /> &bull; Double left click: unselect all filters but chosen from a <b class='text--primary'>group</b> <br /> &bull; <span style='color: coral'>RESET</span>: reset all filters from a <b class='text--primary'>group</b>",
"sections": {
"quick": "QUICK FILTERS",
"reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY",
"control": "CONTROLS",
"signals": "SIGNALLING",
"addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS"
},
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
"afkStatus": "AFK", "afkStatus": "AFK",
"noSpaceStatus": "NO SPACE", "noSpaceStatus": "NO SPACE",
"unavailableStatus": "UNAVAILABLE", "unavailableStatus": "UNAVAILABLE",
"title": "STATION FILTER", "title": "STATION FILTERS",
"default": "DEFAULT", "default": "IN-GAME",
"not-default": "OTHER", "not-default": "ADDITIONAL",
"real": "REAL", "real": "REAL",
"fictional": "FICTIONAL", "fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED", "unavailable": "UNSUPPORTED",
@@ -141,12 +175,22 @@
"abandoned": "ABANDONED", "abandoned": "ABANDONED",
"SPK": "SPK", "SPK": "SPK",
"SPK-R": "SPK + MANUAL",
"SPK-M": "SPK + MECH.",
"SCS": "SCS", "SCS": "SCS",
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "MANUAL", "manual": "MANUAL",
"mechanical": "MECHANICAL", "mechanical": "MECHANICAL",
"SUP": "SUP",
"SBL": "SBL", "SUP": "SUP (RASP-UZK)",
"noSUP": "WITHOUT SUP",
"SBL": "AUTOMATIC (SBL)",
"PBL": "SEMIAUTOMATIC (PBL)",
"modern": "MODERN", "modern": "MODERN",
"semaphores": "SEMAPHORES", "semaphores": "SEMAPHORES",
"mixed": "MIXED", "mixed": "MIXED",
@@ -167,7 +211,7 @@
"hour": "h", "hour": "h",
"no-limit": "NO LIMIT", "no-limit": "NO LIMIT",
"include-selected": "INCLUDE SELECTED", "include-selected": "INCLUDE SELECTED",
"save": "SAVE FILTERS", "save": "REMEMBER FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -179,9 +223,11 @@
"dispatcher-lvl": "Dispatcher\nlevel", "dispatcher-lvl": "Dispatcher\nlevel",
"routes": "Routes\ndouble / single", "routes": "Routes\ndouble / single",
"general": "General info", "general": "General info",
"users": "Drivers online", "user": "Drivers online",
"spawns": "Spawns online", "spawn": "Spawns online",
"timetables": "Active timetables", "timetableAll": "Active timetables",
"timetableConfirmed": "Confirmed timetables",
"timetableUnconfirmed": "Unconfirmed timetables",
"no-stations": "No stations to show here!", "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..." "scenery-search": "Search for scenery..."
}, },
@@ -232,6 +278,7 @@
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
"loading": "Loading dispatcher history data...", "loading": "Loading dispatcher history data...",
"no-history": "No dispatcher history found!", "no-history": "No dispatcher history found!",
"data-refreshed-at": "Data refreshed at",
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
@@ -241,7 +288,7 @@
"route-length": "Route length:", "route-length": "Route length:",
"station-count": "Stations:", "station-count": "Stations:",
"dispatcher-name": "Created by", "dispatcher-name": "Author",
"timetable-day": "Timetable created at", "timetable-day": "Timetable created at",
"timetable-active": "ACTIVE", "timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED", "timetable-fulfilled": "FULFILLED",
@@ -249,13 +296,15 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins",
"stock-info": "STOCK INFO", "hours": "{value} hour | {value} hours",
"minutes": "{value} min | {value} mins",
"seconds": "{value} s",
"stock-info": "EXTRA INFO",
"stock-length": "Length", "stock-length": "Length",
"stock-mass": "Mass", "stock-mass": "Mass",
"stock-max-speed": "Maximum registered speed", "stock-max-speed": "Max. speed",
"load-data": "Load further data...", "load-data": "Load further data...",
@@ -270,10 +319,13 @@
"stats-distance": "DISTANCE", "stats-distance": "DISTANCE",
"stats-stations": "STATIONS", "stats-stations": "STATIONS",
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}", "timetable-stats-title": "Daily stats on {date}",
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}", "timetable-stats-total": "Issued timetables: {count} (total distance: {distance})",
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}", "timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each", "timetable-stats-most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
"timetable-stats-most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
"timetable-stats-most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
"timetable-stats-longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
"timetable-count": "timetable | timetables", "timetable-count": "timetable | timetables",
@@ -284,7 +336,18 @@
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!", "driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"stats-loading": "Fetching statistics...", "stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/" "stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
"timetable-location-signal": "signal:",
"timetable-location-route": "route:",
"history-name": "Scenery name",
"history-hash": "Hash",
"history-dispatcher": "Dispatcher",
"history-level": "Level",
"history-rate": "Rate",
"history-region": "Region",
"history-date": "Service date"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
@@ -299,22 +362,41 @@
"history-btn": "View the dispatcher history", "history-btn": "View the dispatcher history",
"info-btn": "Return to the scenery view", "info-btn": "Return to the scenery view",
"authors-title": "Scenery author | Scenery authors", "authors-title": "Scenery author | Scenery authors",
"abbrev": "Station symbol:",
"lines-title": "Real lines", "lines-title": "Real lines",
"project-title": "Project name", "project-title": "Project name",
"one-way-routes": "One way routes", "one-way-routes": "One way routes",
"two-way-routes": "Two way routes", "two-way-routes": "Two way routes",
"option-active-timetables": "Active timetables", "option-active-timetables": "Active timetables",
"option-timetables-history": "Scenery timetables history", "option-timetables-history": "Timetables history",
"option-dispatchers-history": "Scenery dispatchers history", "option-dispatchers-history": "Dispatchers history",
"timetable-author-title": "Issued by", "timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown", "timetable-author-unknown": "Author unknown",
"timetables-history-id": "ID",
"timetables-history-number": "Number",
"timetables-history-route": "Route",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No recorded scenery history!",
"forum-topic": "Official {name} forum topic" "forum-topic": "Official {name} forum topic",
"pragotron-link": "Timetable pallet board (beta)",
"tablice-link": "Timetable summary board (by Thundo)",
"bottom-info": "Show full history in the Journal tab"
}, },
"availability": { "availability": {
"title": "Availability", "title": "Availability",
@@ -329,7 +411,19 @@
"end": "Timetable terminates here", "end": "Timetable terminates here",
"terminated": "Timetable terminated", "terminated": "Timetable terminated",
"begins": "BEGINS HERE", "begins": "BEGINS HERE",
"terminates": "TERMINATES\nHERE" "terminates": "TERMINATES\nHERE",
"from": "FROM",
"to": "TO",
"desc-arriving": "The train is not here yet. It's going to come from: {prevStationName} (szlak {prevDepartureLine})",
"desc-online": "The train is at the station. It's going to leave to: {nextStationName} (szlak {nextArrivalLine})",
"desc-stopped": "The train is at the station and is stopped. It's going to leave towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-next-arrival": "Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed": "The train is at the station and it's been departed. Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed-away": "The train has been departed to: {nextStationName} (szlak {nextArrivalLine})",
"desc-end": "The train terminates here",
"desc-terminated": "The train has been terminated"
}, },
"history": { "history": {
"title": "TIMETABLE JOURNAL", "title": "TIMETABLE JOURNAL",
+123 -30
View File
@@ -1,6 +1,9 @@
{ {
"general": { "general": {
"and": " oraz " "and": " oraz ",
"refresh": "ODŚWIEŻ",
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
}, },
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
@@ -14,7 +17,9 @@
"migration-confirm": "Przyjąłem!", "migration-confirm": "Przyjąłem!",
"offline": "Aplikacja w trybie offline!" "offline": "Aplikacja w trybie offline!"
}, },
"footer": {
"discord": "Serwer Discord Stacjownika"
},
"update": { "update": {
"title": "Nowa wersja Stacjownika jest dostępna!", "title": "Nowa wersja Stacjownika jest dostępna!",
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!", "paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
@@ -22,7 +27,6 @@
"confirm-button": "ZAKTUALIZUJ", "confirm-button": "ZAKTUALIZUJ",
"later-button": "PÓŹNIEJ" "later-button": "PÓŹNIEJ"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!", "S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!", "S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
@@ -95,14 +99,18 @@
"search-dispatcher": "Nick dyżurnego", "search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii", "search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy", "search-author": "Nick autora rozkładu jazdy",
"search-date": "Data rozkładu jazdy (czas polski)", "search-issuedFrom": "Sceneria początkowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
"sort-distance": "kilometraż", "sort-routeDistance": "kilometraż",
"sort-total-stops": "stacje", "sort-allStopsCount": "stacje",
"sort-beginDate": "data", "sort-beginDate": "data",
"sort-timetableId": "ID rozkładu", "sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data", "sort-timestampFrom": "data",
"sort-duration": "czas dyżuru", "sort-duration": "czas dyżuru",
"sort-id": "id rozkładu",
"sort-mass": "masa", "sort-mass": "masa",
"sort-speed": "prędkość", "sort-speed": "prędkość",
@@ -112,23 +120,47 @@
"sort-delay": "opóźnienie", "sort-delay": "opóźnienie",
"sort-comments": "uwagi ekspl.", "sort-comments": "uwagi ekspl.",
"filter-comments": "UWAGI EKSPLOATACYJNE", "filter-withComments": "UWAGI EKSPLOATACYJNE",
"filter-twr": "TWR", "filter-noComments": "BEZ UWAG",
"filter-skr": "PRZEKR. SKRAJNIA", "filter-twr": "WYS. RYZYKA",
"filter-skr": "SKRAJNIA",
"filter-twr-skr": "WSZYSTKIE",
"filter-common": "ZWYKŁE",
"filter-passenger": "PASAŻERSKIE", "filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE", "filter-freight": "TOWAROWE",
"filter-other": "INNE", "filter-other": "INNE",
"filter-noTimetable": "BEZ RJ", "filter-noTimetable": "BEZ RJ",
"filter-withTimetable": "ROZKŁAD JAZDY",
"filter-reset": "ZRESETUJ FILTRY", "filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY", "filter-clear": "WYŁĄCZ FILTRY",
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
"filter-section-twrskr": "UWAGI",
"filter-all": "WSZYSTKIE", "filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE", "filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE", "filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE" "filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"desc": " &bull; Kliknięcie: zaznaczenie / odznaczenie filtru <br /> &bull; Podwójne kliknięcie: odznaczenie reszty filtrów z <b class='text--primary'>grupy</b> <br /> &bull; <span style='color: coral'>RESET</span>: zresetowanie filtrów z <b class='text--primary'>grupy</b>",
"sections": {
"quick": "SZYBKIE FILTRY",
"reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY",
"blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE"
},
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
"afkStatus": "Z/W", "afkStatus": "Z/W",
"noSpaceStatus": "BRAK MIEJSCA", "noSpaceStatus": "BRAK MIEJSCA",
@@ -144,18 +176,29 @@
"abandoned": "WYCOFANA", "abandoned": "WYCOFANA",
"SPK": "SPK", "SPK": "SPK",
"SPK-R": "SPK + RĘCZNE",
"SPK-M": "SPK + MECH.",
"SCS": "SCS", "SCS": "SCS",
"SCS-R": "SCS + RĘCZNE",
"SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "RĘCZNE", "manual": "RĘCZNE",
"SUP": "SUP",
"SBL": "SBL", "SUP": "SUP (RASP-UZK)",
"noSUP": "BEZ SUP",
"SBL": "SAMOCZYNNA",
"PBL": "PÓŁSAMOCZYNNA",
"mechanical": "MECHANICZNE", "mechanical": "MECHANICZNE",
"modern": "WSPÓŁCZESNA", "modern": "WSPÓŁCZESNA",
"semaphores": "KSZTAŁTOWA", "semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA", "mixed": "MIESZANA",
"historical": "HISTORYCZNA", "historical": "HISTORYCZNA",
"free": "WOLNA", "free": "WOLNA",
"occupied": "ZAJĘTA", "occupied": "ZAJĘTA",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
@@ -164,27 +207,31 @@
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)" "routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
}, },
"authors-search": "Szukaj autora (uwzględnia inne filtry)", "authors-search": "Szukaj autora (uwzględnia inne filtry)",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:", "minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ", "now": "TERAZ",
"hour": " godz.", "hour": " godz.",
"no-limit": "BEZ LIMITU", "no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE", "include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPISZ FILTRY", "save": "ZAPAMIĘTAJ FILTRY",
"reset": "RESETUJ FILTRY", "reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
"sceneries": { "sceneries": {
"station": "Stacja", "station": "Stacja",
"abbr": "Skrót\nposterunku",
"min-lvl": "Min. poziom\ndyżurnego", "min-lvl": "Min. poziom\ndyżurnego",
"status": "Status", "status": "Status",
"dispatcher": "Dyżurny", "dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego", "dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor", "routes": "Szlaki\n2tor / 1tor",
"general": "Informacje\nogólne", "general": "Informacje\nogólne",
"users": "Maszyniści online", "user": "Maszyniści online",
"spawns": "Otwarte spawny", "spawn": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy", "timetableAll": "Aktywne rozkłady jazdy",
"timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!", "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..." "scenery-search": "Wyszukaj scenerię..."
}, },
@@ -236,6 +283,7 @@
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
"loading": "Ładowanie historii dyżurów...", "loading": "Ładowanie historii dyżurów...",
"no-history": "Brak historii dyżurów dla tej scenerii!", "no-history": "Brak historii dyżurów dla tej scenerii!",
"data-refreshed-at": "Dane odświeżone o",
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
@@ -245,21 +293,22 @@
"online-since": "ONLINE OD", "online-since": "ONLINE OD",
"duty-lasted": "Dyżur trwał", "duty-lasted": "Dyżur trwał",
"minutes": "{minutes} min.", "hours": "{value} godz.",
"hours": "{hours} godz. {minutes} min.", "minutes": "{value} min.",
"seconds": "{value} sek.",
"route-length": "Kilometraż:", "route-length": "Kilometraż:",
"station-count": "Stacje:", "station-count": "Stacje:",
"dispatcher-name": "Wystawiony przez dyżurnego", "dispatcher-name": "Autor",
"timetable-day": "Rozkład z dnia", "timetable-day": "Rozkład z dnia",
"timetable-active": "AKTYWNY", "timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY", "timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY", "timetable-abandoned": "PORZUCONY",
"stock-info": "INFORMACJE O SKŁADZIE", "stock-info": "DODATKOWE INFORMACJE",
"stock-length": "Długość", "stock-length": "Długość",
"stock-mass": "Masa", "stock-mass": "Masa",
"stock-max-speed": "Maks. zarejestrowana prędkość", "stock-max-speed": "Prędkość maks.",
"load-data": "Pobierz dalszą historię...", "load-data": "Pobierz dalszą historię...",
@@ -274,10 +323,12 @@
"stats-distance": "DYSTANS", "stats-distance": "DYSTANS",
"stats-stations": "STACJE", "stats-stations": "STACJE",
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}", "timetable-stats-total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}", "timetable-stats-longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}", "timetable-stats-most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}", "timetable-stats-most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
"timetable-stats-most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
"timetable-stats-longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
"timetable-count": "rozkład jazdy | rozkładów jazdy", "timetable-count": "rozkład jazdy | rozkładów jazdy",
@@ -288,7 +339,18 @@
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!", "driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"stats-loading": "Pobieranie statystyk...", "stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/" "stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/",
"timetable-location-signal": "semafor:",
"timetable-location-route": "szlak:",
"history-name": "Sceneria",
"history-hash": "Hash",
"history-dispatcher": "Dyżurny",
"history-level": "Poziom",
"history-rate": "Ocena",
"history-region": "Region",
"history-date": "Data służby"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
@@ -301,24 +363,43 @@
"no-scenery": "Ups! Ta sceneria nie istnieje!", "no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "Wróć na stronę główną", "return-btn": "Wróć na stronę główną",
"history-btn": "Przejdź do widoku historii dyżurnych ruchu", "history-btn": "Przejdź do widoku historii dyżurnych ruchu",
"info-btn": "Wróc do widoku scenerii", "info-btn": "Wróć do widoku scenerii",
"authors-title": "Autor scenerii | Autorzy scenerii", "authors-title": "Autor scenerii | Autorzy scenerii",
"abbrev": "Skrót posterunku:",
"lines-title": "Rzeczywiste linie", "lines-title": "Rzeczywiste linie",
"project-title": "Projekt", "project-title": "Projekt",
"one-way-routes": "Szlaki jednotorowe", "one-way-routes": "Szlaki jednotorowe",
"two-way-routes": "Szlaki dwutorowe", "two-way-routes": "Szlaki dwutorowe",
"option-active-timetables": "Aktywne rozkłady jazdy", "option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów scenerii", "option-timetables-history": "Historia rozkładów",
"option-dispatchers-history": "Historia dyżurów scenerii", "option-dispatchers-history": "Historia dyżurów",
"timetable-author-title": "Wydany przez", "timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany", "timetable-author-unknown": "Autor nieznany",
"timetables-history-id": "ID",
"timetables-history-number": "Numer",
"timetables-history-route": "Trasa",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!", "history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}" "forum-topic": "Oficjalny wątek scenerii {name}",
"pragotron-link": "Paletowa tablica informacyjna (beta)",
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
}, },
"availability": { "availability": {
"title": "Dostępność", "title": "Dostępność",
@@ -333,7 +414,19 @@
"end": "Koniec rozkładu jazdy", "end": "Koniec rozkładu jazdy",
"terminated": "Rozkład jazdy zakończony", "terminated": "Rozkład jazdy zakończony",
"begins": "ROZPOCZYNA\nBIEG", "begins": "ROZPOCZYNA\nBIEG",
"terminates": "KOŃCZY BIEG" "terminates": "KOŃCZY BIEG",
"from": "Z",
"to": "DO",
"desc-arriving": "Pociągu nie ma jeszcze na tej scenerii. Przyjedzie z: {prevStationName} (szlak {prevDepartureLine})",
"desc-online": "Pociąg jest na tej scenerii. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
"desc-stopped": "Pociąg jest na tej scenerii i odbywa postój. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
"desc-next-arrival": "Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed": "Pociąg jest na tej scenerii i został odprawiony. Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
"desc-departed-away": "Pociąg został odprawiony i odjechał do: {nextStationName} (szlak {nextArrivalLine})",
"desc-end": "Pociąg kończy bieg",
"desc-terminated": "Pociąg skończył bieg"
}, },
"history": { "history": {
"title": "DZIENNIK ROZKŁADÓW JAZDY" "title": "DZIENNIK ROZKŁADÓW JAZDY"
+1
View File
@@ -11,6 +11,7 @@ import { createPinia } from 'pinia';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false, legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
messages: { messages: {
en: enLang, en: enLang,
+77 -50
View File
@@ -1,50 +1,77 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
methods: { methods: {
localeDate(dateString: string, locale: string) { localeDate(dateString: string, locale: string) {
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}); });
}, },
localeDay(dateString: string, locale: string) { localeDay(dateString: string, locale: string) {
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
day: 'numeric', day: 'numeric',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
}); });
}, },
localeTime(dateString: string, locale: string) { localeDateTime(dateString: string, locale: string) {
return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
hour: '2-digit', timeStyle: 'short',
minute: '2-digit', dateStyle: 'medium'
}); });
}, },
timestampToString(timestamp: number | null) { localeTime(dateString: string, locale: string) {
return timestamp return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
? new Date(timestamp).toLocaleTimeString('pl-PL', { hour: '2-digit',
hour: '2-digit', minute: '2-digit',
minute: '2-digit', });
}) },
: '';
}, stringToDate(dateString?: string) {
return dateString ? new Date(dateString) : null;
calculateDuration(timestampMs: number) { },
const minsTotal = Math.round(timestampMs / 60000);
const hoursTotal = Math.floor(minsTotal / 60); parseDateToTimeString(date: Date | null) {
const minsInHour = minsTotal % 60; return (
date?.toLocaleTimeString('pl-PL', {
return minsTotal > 60 hour: '2-digit',
? this.$t('journal.hours', { hours: hoursTotal, minutes: minsInHour }) minute: '2-digit',
: this.$t('journal.minutes', { minutes: minsTotal }); }) || ''
}, );
}, },
});
timestampToString(timestamp: number | null) {
return timestamp
? new Date(timestamp).toLocaleTimeString('pl-PL', {
hour: '2-digit',
minute: '2-digit',
})
: '';
},
calculateDuration(timestampMs: number, showSeconds = false) {
const secondsTotal = Math.floor(timestampMs / 1000);
const minsTotal = Math.round(timestampMs / 60000);
const hoursTotal = Math.floor(minsTotal / 60);
const minsInHour = minsTotal % 60;
return minsTotal >= 60
? `${this.$t('journal.hours', { value: hoursTotal }, hoursTotal)} ${this.$t(
'journal.minutes',
{ value: minsInHour },
minsInHour
)}`
: showSeconds && secondsTotal <= 60
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
: this.$t('journal.minutes', { value: minsTotal }, minsTotal);
},
},
});
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null,
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver((entries) => {
console.log(entries);
if (entries[0].intersectionRatio > 0.5) actionFunction();
}, { threshold: 0.2 });
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
},
},
});
+4 -2
View File
@@ -1,4 +1,4 @@
import { defineComponent } from 'vue'; import { Ref, defineComponent } from 'vue';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
@@ -15,15 +15,17 @@ export default defineComponent({
}, },
methods: { methods: {
selectModalTrain(trainId: string) { selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
if (target) this.store.modalLastClickedTarget = target;
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
setTimeout(() => { setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
}, 150); }, 150);
}, },
+13 -7
View File
@@ -4,11 +4,17 @@ export default defineComponent({
methods: { methods: {
calculateExpStyle(exp: number, isSupporter = false): string { calculateExpStyle(exp: number, isSupporter = false): string {
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666'; const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black'; const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : ''; const boxShadow = isSupporter ? `box-shadow: 0 0 6px 2px ${bgColor};` : '';
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`; return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow};`;
},
calculateTextExpStyle(exp: number, isSupporter = false): string {
const textColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 75%, 50%)`) : '#666';
return `color: ${textColor}; ${isSupporter ? 'text-shadow: 0 0 6px ' + textColor : ''};`;
}, },
statusClasses(occupiedTo: string) { statusClasses(occupiedTo: string) {
@@ -41,6 +47,6 @@ export default defineComponent({
} }
return className; return className;
} },
} },
}) });
@@ -0,0 +1,49 @@
import Filter from "../../interfaces/Filter";
export const filterInitStates: Filter = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ręczne: false,
'ręczne+SPK': false,
'ręczne+SCS': false,
mechaniczne: false,
'mechaniczne+SPK': false,
'mechaniczne+SCS': false,
współczesna: false,
kształtowa: false,
historyczna: false,
mieszana: false,
SBL: false,
PBL: false,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
ending: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
authors: '',
onlineFromHours: 0,
};
+5
View File
@@ -0,0 +1,5 @@
export const headIds = ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'] as const;
export const headIconsIds = ['user', 'spawn', 'timetableAll', 'timetableUnconfirmed', 'timetableConfirmed'] as const;
export type HeadIdsTypes = typeof headIds[number] | typeof headIconsIds[number];
+14 -6
View File
@@ -1,6 +1,14 @@
export const enum JournalFilterType { export const enum JournalFilterType {
active = "active", ACTIVE = 'active',
fulfilled = "fulfilled", FULFILLED = 'fulfilled',
abandoned = "abandoned", ABANDONED = 'abandoned',
all = "all" ALL = 'all',
} TWR = 'twr',
SKR = 'skr',
TWR_SKR = 'twr-skr',
}
export enum JournalFilterSection {
TIMETABLE_STATUS = 'timetable-status',
TWRSKR = 'twrskr',
}
+20 -8
View File
@@ -1,9 +1,21 @@
export const enum TrainFilterType { export enum TrainFilterSection {
comments = "comments", TRAIN_TYPE = 'TRAIN_TYPE',
twr = "twr", TIMETABLE_TYPE = 'TIMETABLE_TYPE',
skr = "skr", COMMENTS = 'COMMENTS',
passenger = "passenger", TIMETABLE = 'TIMETABLE',
freight = "freight", }
other = "other",
noTimetable = "noTimetable" export const enum TrainFilterType {
noComments = 'noComments',
withComments = 'withComments',
twr = 'twr',
skr = 'skr',
common = 'common',
passenger = 'passenger',
freight = 'freight',
other = 'other',
noTimetable = 'noTimetable',
withTimetable = 'withTimetable',
} }
+12 -6
View File
@@ -1,16 +1,22 @@
export default interface Filter { export default interface Filter {
[key: string]: (boolean | number | string), [key: string]: boolean | number | string;
default: boolean; default: boolean;
notDefault: boolean; notDefault: boolean;
real: boolean; real: boolean;
fictional: boolean; fictional: boolean;
"SPK": boolean; SPK: boolean;
"SCS": boolean; SCS: boolean;
"SPE": boolean; SPE: boolean;
"SUP": boolean; SUP: boolean;
noSUP: boolean;
ręczne: boolean; ręczne: boolean;
'ręczne+SPK': boolean;
'ręczne+SCS': boolean;
mechaniczne: boolean; mechaniczne: boolean;
"SBL": boolean; 'mechaniczne+SPK': boolean;
'mechaniczne+SCS': boolean;
SBL: boolean;
PBL: boolean;
współczesna: boolean; współczesna: boolean;
kształtowa: boolean; kształtowa: boolean;
historyczna: boolean; historyczna: boolean;
+34 -25
View File
@@ -1,32 +1,41 @@
import TrainStop from "./TrainStop"; import TrainStop from './TrainStop';
export default interface ScheduledTrain { export enum StopStatus {
trainId: string; 'arriving' = 'arriving',
trainNo: number; 'departed' = 'departed',
'departed-away' = 'departed-away',
driverName: string; 'online' = 'online',
driverId: number; 'stopped' = 'stopped',
currentStationName: string; 'terminated' = 'terminated',
currentStationHash: string; }
category: string;
stopInfo: TrainStop;
terminatesAt: string; export interface ScheduledTrain {
beginsAt: string; trainId: string;
trainNo: number;
prevStationName: string; driverName: string;
nextStationName: string; driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
arrivingLine: string | null; terminatesAt: string;
departureLine: string | null; beginsAt: string;
prevDepartureLine: string | null; prevStationName: string;
nextArrivalLine: string | null; nextStationName: string;
signal: string; arrivingLine: string | null;
connectedTrack: string; departureLine: string | null;
stopLabel: string; prevDepartureLine: string | null;
stopStatus: string; nextArrivalLine: string | null;
stopStatusID: number;
} signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
}
+6 -3
View File
@@ -1,5 +1,5 @@
import { Availability } from '../../store/storeTypes'; import { Availability } from './store/storeTypes';
import ScheduledTrain from './ScheduledTrain'; import {ScheduledTrain} from './ScheduledTrain';
import StationRoutes from './StationRoutes'; import StationRoutes from './StationRoutes';
export default interface Station { export default interface Station {
@@ -8,12 +8,15 @@ export default interface Station {
generalInfo?: { generalInfo?: {
name: string; name: string;
url: string; url: string;
abbr: string;
reqLevel: number; reqLevel: number;
// supportersOnly: boolean; // supportersOnly: boolean;
lines: string; lines: string;
project: string; project: string;
projectUrl?: string;
signalType: string; signalType: string;
controlType: string; controlType: string;
@@ -38,7 +41,7 @@ export default interface Station {
maxUsers: number; maxUsers: number;
currentUsers: number; currentUsers: number;
spawns: { spawnName: string; spawnLength: number }[]; spawns: { spawnName: string; spawnLength: number; isElectrified: boolean }[];
dispatcherRate: number; dispatcherRate: number;
dispatcherName: string; dispatcherName: string;
dispatcherExp: number; dispatcherExp: number;
+30 -27
View File
@@ -1,27 +1,30 @@
export default interface StationRoutes { export default interface StationRoutes {
oneWay: oneWay: {
{ name: string;
name: string; catenary: boolean;
catenary: boolean; SBL: boolean;
SBL: boolean; TWB: boolean;
TWB: boolean; isInternal: boolean;
isInternal: boolean; tracks: number;
tracks: number; speed: number;
}[]; length: number;
}[];
twoWay: {
name: string; twoWay: {
catenary: boolean; name: string;
SBL: boolean; catenary: boolean;
TWB: boolean; SBL: boolean;
isInternal: boolean; TWB: boolean;
tracks: number; isInternal: boolean;
}[]; tracks: number;
speed: number;
/* [catenary, noCatenary] */ length: number;
oneWayCatenaryRouteNames: string[]; }[];
oneWayNoCatenaryRouteNames: string[];
twoWayCatenaryRouteNames: string[]; /* [catenary, noCatenary] */
twoWayNoCatenaryRouteNames: string[]; oneWayCatenaryRouteNames: string[];
sblRouteNames: string[]; oneWayNoCatenaryRouteNames: string[];
} twoWayCatenaryRouteNames: string[];
twoWayNoCatenaryRouteNames: string[];
sblRouteNames: string[];
}
+1
View File
@@ -12,6 +12,7 @@ export default interface Train {
driverId: number; driverId: number;
trainNo: number; trainNo: number;
driverName: string; driverName: string;
driverLevel: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
locoURL: string; locoURL: string;
+1 -1
View File
@@ -1,4 +1,4 @@
export default interface TrainStop { export default interface TrainStop {
stopName: string; stopName: string;
stopNameRAW: string; stopNameRAW: string;
stopType: string; stopType: string;
@@ -0,0 +1,7 @@
import { TrainFilterSection, TrainFilterType } from '../../enums/TrainFilterType'
export interface TrainFilter {
id: TrainFilterType;
section: TrainFilterSection;
isActive: boolean;
}
@@ -1,9 +1,13 @@
export interface DispatcherHistory { export interface DispatcherHistory {
id: string; id: string;
currentDuration: number; currentDuration: number;
dispatcherId: number; dispatcherId: number;
dispatcherName: string; dispatcherName: string;
dispatcherLevel: number | null;
dispatcherRate: number;
dispatcherIsSupporter: boolean;
dispatcherStatus?: number;
isOnline: boolean; isOnline: boolean;
lastOnlineTimestamp: number; lastOnlineTimestamp: number;
region: string; region: string;
@@ -11,4 +15,4 @@ export interface DispatcherHistory {
stationName: string; stationName: string;
timestampFrom: number; timestampFrom: number;
timestampTo?: number; timestampTo?: number;
} }
@@ -14,6 +14,17 @@ export interface ITimetablesDailyStats {
name: string; name: string;
count: number; count: number;
}[]; }[];
mostActiveDrivers: {
name: string;
distance: number;
}[];
longestDuties: {
name: string;
duration: number;
station: string;
}[];
} }
export interface ITimetablesDailyStatsResponse { export interface ITimetablesDailyStatsResponse {
@@ -26,5 +37,16 @@ export interface ITimetablesDailyStatsResponse {
name: string; name: string;
count: number; count: number;
}[]; }[];
mostActiveDrivers: {
name: string;
distance: number;
}[];
longestDuties: {
name: string;
duration: number;
station: string;
}[];
} }
@@ -1,13 +1,22 @@
export interface TimetableHistory { export interface TimetableHistory {
id: number;
createdAt: string;
updatedAt: string;
timetableId: number; timetableId: number;
trainNo: number; trainNo: number;
trainCategoryCode: string; trainCategoryCode: string;
driverId: number; driverId: number;
driverName: string; driverName: string;
driverLevel: number | null;
driverIsSupporter: boolean;
route: string; route: string;
twr: number; twr: number;
skr: number; skr: number;
sceneriesString: string; sceneriesString: string;
currentLocation: string[];
routeDistance: number; routeDistance: number;
currentDistance: number; currentDistance: number;
@@ -27,7 +36,11 @@ export interface TimetableHistory {
authorName?: string; authorName?: string;
authorId?: number; authorId?: number;
stopsString?: string;
stockString?: string; stockString?: string;
stockHistory: string[];
stockMass?: number; stockMass?: number;
stockLength?: number; stockLength?: number;
maxSpeed?: number; maxSpeed?: number;
@@ -35,10 +48,20 @@ export interface TimetableHistory {
hashesString?: string; hashesString?: string;
currentSceneryName?: string; currentSceneryName?: string;
currentSceneryHash?: string; currentSceneryHash?: string;
routeSceneries?: string;
checkpointArrivals?: string[];
checkpointDepartures?: string[];
checkpointArrivalsScheduled?: string[];
checkpointDeparturesScheduled?: string[];
checkpointStopTypes?: string[];
} }
export interface SceneryTimetableHistory { export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[]; timetables: TimetableHistory[];
totalCount: number; // totalCount: number;
sceneryName: string; // sceneryName: string;
} }
@@ -0,0 +1,23 @@
import { JournalTimetableSorter } from '../../types/JournalTimetablesTypes';
export interface TimetablesQueryParams {
driverName?: string;
trainNo?: string;
timetableId?: string;
authorName?: string;
timestampFrom?: number;
timestampTo?: number;
issuedFrom?: string;
countFrom?: number;
countLimit?: number;
fulfilled?: number;
terminated?: number;
twr?: number;
skr?: number;
sortBy?: JournalTimetableSorter['id'];
}
+43 -38
View File
@@ -1,4 +1,44 @@
export default interface TrainAPIData { export interface TimetableStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: number;
mainStop: boolean;
arrivalLine: string;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
comments?: any;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number;
}
export interface TrainTimetable {
timetableId: number;
category: string;
route: string;
stopList: TimetableStop[];
TWR: boolean;
SKR: boolean;
sceneries: string[];
}
export interface TrainAPIData {
trainNo: number; trainNo: number;
mass: number; mass: number;
@@ -13,6 +53,7 @@ export default interface TrainAPIData {
driverName: string; driverName: string;
driverId: number; driverId: number;
driverIsSupporter: boolean; driverIsSupporter: boolean;
driverLevel?: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
@@ -23,41 +64,5 @@ export default interface TrainAPIData {
region: string; region: string;
isTimeout: boolean; isTimeout: boolean;
timetable?: { timetable?: TrainTimetable;
timetableId: number;
category: string;
route: string;
stopList: {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: number;
mainStop: boolean;
arrivalLine: string;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
comments?: any;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number;
}[];
TWR: boolean;
SKR: boolean;
sceneries: string[];
};
} }
@@ -1,11 +1,12 @@
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../../enums/DataStatus';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import StationAPIData from '../api/StationAPIData';
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData'; import { TrainAPIData } from '../api/TrainAPIData';
import Station from '../scripts/interfaces/Station'; import Station from '../Station';
import Train from '../scripts/interfaces/Train'; import Train from '../Train';
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData'; import { DispatcherStatsAPIData } from '../api/DispatcherStatsAPIData';
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../api/DriverStatsAPIData';
import { Ref } from 'vue';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
@@ -34,7 +35,7 @@ export interface StoreState {
chosenModalTrainId?: string; chosenModalTrainId?: string;
currentStatsTab: 'daily' | 'driver'; currentStatsTab: 'daily' | 'driver' | null;
dataStatuses: { dataStatuses: {
connection: DataStatus; connection: DataStatus;
@@ -46,6 +47,7 @@ export interface StoreState {
listenerLaunched: boolean; listenerLaunched: boolean;
blockScroll: boolean; blockScroll: boolean;
modalLastClickedTarget: EventTarget | null;
} }
export interface APIData { export interface APIData {
@@ -55,11 +57,23 @@ export interface APIData {
connectedSocketCount: number; connectedSocketCount: number;
} }
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
}
export interface StationJSONData { export interface StationJSONData {
name: string; name: string;
abbr: string;
url: string; url: string;
lines: string; lines: string;
project: string; project: string;
projectUrl: string;
reqLevel: number; reqLevel: number;
@@ -68,7 +82,9 @@ export interface StationJSONData {
SUP: boolean; SUP: boolean;
routes: string; // routes: string;
routesInfo: StationRoutesInfo[];
checkpoints: string | null; checkpoints: string | null;
authors?: string; authors?: string;
+99 -84
View File
@@ -1,115 +1,130 @@
import { TrainFilter } from "../../types/Trains/TrainOptionsTypes"; import { TrainFilter } from '../interfaces/Trains/TrainFilter';
import { TrainFilterType } from "../enums/TrainFilterType"; import { TrainFilterType } from '../enums/TrainFilterType';
import Train from "../interfaces/Train"; import Train from '../interfaces/Train';
import TrainStop from "../interfaces/TrainStop"; import TrainStop from '../interfaces/TrainStop';
function confirmedPercentage(stops: TrainStop[] | undefined) { function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1; if (!stops) return -1;
return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0)); return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0));
}; }
function currentDelay(stops: TrainStop[] | undefined) { function currentDelay(stops: TrainStop[] | undefined) {
if (!stops) return -Infinity; if (!stops) return -Infinity;
const delay = const delay =
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)) stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
?.departureDelay || 0; ?.departureDelay || 0;
return delay; return delay;
}; }
function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) { function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) {
return trainList.filter( return trainList.filter((train) => {
(train) => { const isFiltered = filters.every((f) => {
const isFiltered = filters.every(f => { if (f.isActive) return true;
if (f.isActive) return true;
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) { switch (f.id) {
case TrainFilterType.comments: case TrainFilterType.noTimetable:
return !train.timetableData.followingStops.some(stop => stop.comments); return train.timetableData;
case TrainFilterType.twr:
return !train.timetableData.TWR;
case TrainFilterType.skr:
return !train.timetableData.SKR;
case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
case TrainFilterType.freight:
return !train.timetableData.category.startsWith('T');
case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
default:
return true;
}
})
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) && case TrainFilterType.withTimetable:
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered return !train.timetableData;
}
case TrainFilterType.withComments:
return !train.timetableData?.followingStops.some((stop) => stop.comments);
case TrainFilterType.noComments:
return train.timetableData?.followingStops.some((stop) => stop.comments);
case TrainFilterType.twr:
return !train.timetableData?.TWR;
case TrainFilterType.skr:
return !train.timetableData?.SKR;
case TrainFilterType.common:
return train.timetableData?.SKR || train.timetableData?.TWR;
case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData?.category || '');
case TrainFilterType.freight:
return !train.timetableData?.category.startsWith('T');
case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData?.category || '');
default:
return true;
}
});
return (
(searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) &&
(!train.timetableData ? train.online : train.timetableData) &&
isFiltered
); );
});
} }
function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) { function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
return trainList.sort((a: Train, b: Train) => { return trainList.sort((a: Train, b: Train) => {
switch (sorterActive.id) { switch (sorterActive.id) {
case 'mass': case 'id':
if (a.mass > b.mass) return sorterActive.dir; if ((a.timetableData?.timetableId || -1) > (b.timetableData?.timetableId || -1)) return sorterActive.dir;
return -sorterActive.dir;
case 'distance': return -sorterActive.dir;
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir; case 'mass':
if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir;
case 'progress': case 'routeDistance':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops)) if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'delay': case 'progress':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops)) if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
return sorterActive.dir; return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'speed': case 'delay':
if (a.speed > b.speed) return sorterActive.dir; if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return -sorterActive.dir; return sorterActive.dir;
case 'timetable': return -sorterActive.dir;
if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir;
case 'length': case 'speed':
if (a.length > b.length) return sorterActive.dir; if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
default: case 'timetable':
break; if (a.trainNo > b.trainNo) return sorterActive.dir;
} return -sorterActive.dir;
return 0; case 'length':
}); if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir;
default:
break;
}
return 0;
});
} }
export function filteredTrainList( export function filteredTrainList(
trainList: Train[], trainList: Train[],
searchedTrain: string, searchedTrain: string,
searchedDriver: string, searchedDriver: string,
sorterActive: { id: string; dir: number }, sorterActive: { id: string; dir: number },
filters: TrainFilter[] filters: TrainFilter[]
) { ) {
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters); return [...sortTrainList(filtered, sorterActive)];
return [...sortTrainList(filtered, sorterActive)]; }
};
@@ -0,0 +1,25 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JournalTimetableSearchKey =
| 'search-driver'
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
export type JournalTimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export type JournalTimetableSearchType = {
[key in JournalTimetableSearchKey]: string;
};
export interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: JournalTimetableSorterKey;
dir: 'asc' | 'desc';
}
+3 -1
View File
@@ -1,5 +1,7 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl', import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD
? 'http://localhost:3001'
: 'https://stacjownik.spythere.pl',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
}; };
+156
View File
@@ -0,0 +1,156 @@
import { HeadIdsTypes } from '../data/stationHeaderNames';
import Filter from '../interfaces/Filter';
import Station from '../interfaces/Station';
export const sortStations = (a: Station, b: Station, sorter: { headerName: HeadIdsTypes; dir: number }) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff = (a.onlineInfo?.statusTimestamp || 0) - (b.onlineInfo?.statusTimestamp || 0);
break;
case 'dispatcher':
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
return sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'user':
diff = (b.onlineInfo ? b.onlineInfo.currentUsers : -1) - (a.onlineInfo ? a.onlineInfo.currentUsers : -1);
break;
case 'spawn':
diff = (a.onlineInfo ? a.onlineInfo.spawns.length : -1) - (b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrains
? a.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length
: -1) -
(b.onlineInfo?.scheduledTrains
? b.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length
: -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrains
? a.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length
: -1) -
(b.onlineInfo?.scheduledTrains
? b.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length
: -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrains ? a.onlineInfo.scheduledTrains.length : -1) -
(b.onlineInfo?.scheduledTrains ? b.onlineInfo.scheduledTrains.length : -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Filter) => {
if (!station.onlineInfo && filters['free']) return false;
if (station.onlineInfo) {
const { statusID, statusTimestamp } = station.onlineInfo;
const isEnding = statusID == 'ending' && filters['endingStatus'];
const isNotSigned = (statusID == 'not-signed' || statusID == 'unavailable') && filters['unavailableStatus'];
const isAFK = statusID == 'brb' && filters['afkStatus'];
const isNoSpace = statusID == 'no-space' && filters['noSpaceStatus'];
const isOccupied = station.onlineInfo && filters['occupied'];
const isOnlineInBounds =
(filters['onlineFromHours'] < 8 &&
statusTimestamp > 0 &&
statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000) ||
(filters['onlineFromHours'] > 0 && statusTimestamp <= 0) ||
(filters['onlineFromHours'] == 8 && statusID != 'no-limit');
if (isEnding || isOnlineInBounds || isNotSigned || isAFK || isNoSpace || isOccupied) return false;
}
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) return false;
if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, authors } = station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) return false;
if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false;
if (availability == 'default' && filters['default']) return false;
if (
availability != 'default' &&
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable')
)
return false;
if (filters['real'] && lines) return false;
if (filters['fictional'] && !lines) return false;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false;
if (
filters['no-1track'] &&
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
)
return false;
if (
filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
)
return false;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return false;
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return false;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return false;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false;
if (filters[signalType]) return false;
if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return false;
if (filters['PBL'] && routes.sblRouteNames.length == 0) return false;
if (
filters['authors'].length > 3 &&
!authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
return false;
}
return true;
};
+12 -12
View File
@@ -1,4 +1,4 @@
import ScheduledTrain from '../interfaces/ScheduledTrain'; import { ScheduledTrain, StopStatus } from '../interfaces/ScheduledTrain';
import Train from '../interfaces/Train'; import Train from '../interfaces/Train';
import TrainStop from '../interfaces/TrainStop'; import TrainStop from '../interfaces/TrainStop';
@@ -66,40 +66,41 @@ export const parseSpawns = (spawnString: string) => {
const spawnArray = spawn.split(','); const spawnArray = spawn.split(',');
const spawnName = spawnArray[6] ? spawnArray[6] : spawnArray[0]; const spawnName = spawnArray[6] ? spawnArray[6] : spawnArray[0];
const spawnLength = parseInt(spawnArray[2]); const spawnLength = parseInt(spawnArray[2]);
const isElectrified = spawnArray[3] == 'True';
return { spawnName, spawnLength }; return { spawnName, spawnLength, isElectrified };
}); });
}; };
export const getTimestamp = (date: string | null): number => (date ? new Date(date).getTime() : 0); export const getTimestamp = (date: string | null): number => (date ? new Date(date).getTime() : 0);
export const getTrainStopStatus = (stopInfo: TrainStop, currentStationName: string, stationName: string) => { export const getTrainStopStatus = (stopInfo: TrainStop, currentStationName: string, stationName: string) => {
let stopStatus = '', let stopStatus = StopStatus['arriving'],
stopLabel = '', stopLabel = '',
stopStatusID = -1; stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) { if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = 'terminated'; stopStatus = StopStatus['terminated'];
stopLabel = 'Skończył bieg'; stopLabel = 'Skończył bieg';
stopStatusID = 5; stopStatusID = 5;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == stationName) { } else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == stationName) {
stopStatus = 'departed'; stopStatus = StopStatus['departed'];
stopLabel = 'Odprawiony'; stopLabel = 'Odprawiony';
stopStatusID = 2; stopStatusID = 2;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != stationName) { } else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != stationName) {
stopStatus = 'departed-away'; stopStatus = StopStatus['departed-away'];
stopLabel = 'Odjechał'; stopLabel = 'Odjechał';
stopStatusID = 4; stopStatusID = 4;
} else if (currentStationName == stationName && !stopInfo.stopped) { } else if (currentStationName == stationName && !stopInfo.stopped) {
stopStatus = 'online'; stopStatus = StopStatus['online'];
stopLabel = 'Na stacji'; stopLabel = 'Na stacji';
stopStatusID = 0; stopStatusID = 0;
} else if (currentStationName == stationName && stopInfo.stopped) { } else if (currentStationName == stationName && stopInfo.stopped) {
stopStatus = 'stopped'; stopStatus = StopStatus['stopped'];
stopLabel = 'Postój'; stopLabel = 'Postój';
stopStatusID = 1; stopStatusID = 1;
} else if (currentStationName != stationName) { } else if (currentStationName != stationName) {
stopStatus = 'arriving'; stopStatus = StopStatus['arriving'];
stopLabel = 'W drodze'; stopLabel = 'W drodze';
stopStatusID = 3; stopStatusID = 3;
} }
@@ -122,7 +123,7 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
for (let i = trainStopIndex - 1; i >= 0; i--) { for (let i = trainStopIndex - 1; i >= 0; i--) {
if (/strong|podg/g.test(followingStops[i].stopName)) { if (/strong|podg/g.test(followingStops[i].stopName)) {
prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g,""); prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break; break;
} }
@@ -130,7 +131,7 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
for (let i = trainStopIndex + 1; i < followingStops.length; i++) { for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
if (/strong|podg/g.test(followingStops[i].stopName)) { if (/strong|podg/g.test(followingStops[i].stopName)) {
nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g,""); nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break; break;
} }
@@ -172,7 +173,6 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
signal: train.signal, signal: train.signal,
connectedTrack: train.connectedTrack, connectedTrack: train.connectedTrack,
driverName: train.driverName, driverName: train.driverName,
driverId: train.driverId, driverId: train.driverId,
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
+9
View File
@@ -0,0 +1,9 @@
import { defineStore } from 'pinia';
export const useJournalFiltersStore = defineStore('journalFiltersStore', {
state: () => ({
timetableFilters: {
},
}),
});
+116 -308
View File
@@ -1,308 +1,116 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import inputData from '../data/options.json'; import inputData from '../data/options.json';
import Filter from '../scripts/interfaces/Filter'; import Station from '../scripts/interfaces/Station';
import Station from '../scripts/interfaces/Station'; import StorageManager from '../scripts/managers/storageManager';
import StorageManager from '../scripts/managers/storageManager'; import { useStore } from './store';
import { useStore } from './store'; import { filterInitStates } from '../scripts/constants/stores/initFilterStates';
import { filterStations, sortStations } from '../scripts/utils/filterUtils';
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => { import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
switch (sorter.index) {
case 0: export const useStationFiltersStore = defineStore('stationFiltersStore', {
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); state() {
return {
case 1: inputs: inputData,
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir; filters: { ...filterInitStates },
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir; sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
break; store: useStore(),
lastClickedFilterId: '',
case 2: };
if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir; },
if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir;
break; getters: {
areFiltersAtDefault(state) {
case 3: return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) },
return sorter.dir; },
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
return -sorter.dir; actions: {
break; getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList
case 4: .map((station) => {
if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir; if (station.onlineInfo && station.onlineInfo.region != region) {
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir; delete station.onlineInfo;
break; }
case 7: return station;
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir; })
if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir; .filter((station) => filterStations(station, this.filters))
.sort((a, b) => sortStations(a, b, this.sorterActive));
if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir; },
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break; setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
case 8:
if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 0)) return sorter.dir; this.inputs.options.forEach((option) => {
if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir; if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name);
break;
this.filters[option.name] = savedValue;
case 9: option.value = !savedValue;
if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0)) });
return sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0)) this.inputs.sliders.forEach((slider) => {
return -sorter.dir; if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
default:
break; this.filters[slider.name] = savedValue;
} slider.value = savedValue;
});
return a.name.localeCompare(b.name); },
};
// Quick actions (TODO)
const filterStations = (station: Station, filters: Filter, isOffline = false) => { handleQuickAction(actionName: string) {
const returnMode = false; // switch (actionName) {
// case 'all-available':
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) // this.resetFilters();
return returnMode; // this.inputs.options
// .filter((option) => /^(free|non-public)/.test(option.id))
if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode; // .forEach((option) => (option.value = !option.defaultValue));
// break;
if ( // case 'all-free':
station.onlineInfo && // this.resetFilters();
station.onlineInfo.statusTimestamp > 0 && // this.inputs.options
filters['onlineFromHours'] < 8 && // .filter((option) => /^(free|occupied)/.test(option.id))
station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000 // .forEach((option) => (option.value = !option.defaultValue));
) // break;
return returnMode; // default:
// break;
if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0) // }
return returnMode; },
if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode;
changeFilterValue(name: string, value: any) {
if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode; this.filters[name] = value;
if ( if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') && },
filters['unavailableStatus']
) resetFilters() {
return returnMode; this.filters = { ...filterInitStates };
if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode; this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
if (station.onlineInfo && filters['occupied']) return returnMode; StorageManager.setBooleanValue(option.name, !option.defaultValue);
if (!station.onlineInfo && filters['free']) return returnMode; });
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return returnMode; this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
if (station.generalInfo) { StorageManager.setNumericValue(slider.name, slider.defaultValue);
const routes = station.generalInfo.routes; });
const availability = station.generalInfo.availability; },
if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode; resetSectionOptions(section: string) {
this.inputs.options
if (availability == 'default' && filters['default']) return returnMode; .filter((option) => option.section == section)
if ( .forEach((option) => {
availability != 'default' && option.value = option.defaultValue;
filters['notDefault'] && StorageManager.setBooleanValue(option.name, !option.defaultValue);
!(availability == 'abandoned' || availability == 'unavailable') });
) },
return returnMode;
changeSorter(headerName: HeadIdsTypes) {
if (filters['real'] && station.generalInfo.lines != '') return returnMode; if (headerName == this.sorterActive.headerName) this.sorterActive.dir = -1 * this.sorterActive.dir;
if ( else this.sorterActive.dir = 1;
filters['fictional'] &&
station.generalInfo.lines == '' && this.sorterActive.headerName = headerName;
availability != 'abandoned' && },
availability != 'unavailable' },
) });
return returnMode;
if (
station.generalInfo.reqLevel +
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) <
filters['minLevel']
)
return returnMode;
if (
station.generalInfo.reqLevel +
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) >
filters['maxLevel']
)
return returnMode;
if (
filters['no-1track'] &&
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
)
return returnMode;
if (
filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
)
return returnMode;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode;
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
if (filters[station.generalInfo.controlType]) return returnMode;
if (filters[station.generalInfo.signalType]) return returnMode;
if (
filters['SPK'] &&
(station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
)
return returnMode;
if (
filters['SCS'] &&
(station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS'))
)
return returnMode;
if (
filters['SPE'] &&
(station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE'))
)
return returnMode;
if (filters['SUP'] && station.generalInfo.SUP) return returnMode;
if (
filters['SCS'] &&
filters['SPK'] &&
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS'))
)
return returnMode;
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode;
if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
if (
filters['authors'].length > 3 &&
!station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
return returnMode;
}
return true;
};
const filterInitStates: Filter = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
ręczne: false,
mechaniczne: false,
współczesna: false,
kształtowa: false,
historyczna: false,
mieszana: false,
SBL: false,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
ending: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
authors: '',
onlineFromHours: 0,
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { index: 0, dir: 1 },
store: useStore(),
};
},
actions: {
getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList
.map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) {
delete station.onlineInfo;
}
return station;
})
.filter((station) => filterStations(station, this.filters, this.store.isOffline))
.sort((a, b) => sortStations(a, b, this.sorterActive));
},
setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.id)) return;
const savedValue = StorageManager.getBooleanValue(option.id);
this.filters[option.id] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(filter: { name: string; value: any }) {
this.filters[filter.name] = filter.value;
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
},
resetFilters() {
this.filters = { ...filterInitStates };
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
changeSorter(index: number) {
if (index > 4 && index < 7) return;
if (index == this.sorterActive.index) this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.index = index;
},
},
});
+42 -53
View File
@@ -3,7 +3,7 @@ import { defineStore } from 'pinia';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../scripts/enums/DataStatus';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import ScheduledTrain from '../scripts/interfaces/ScheduledTrain'; import { ScheduledTrain } from '../scripts/interfaces/ScheduledTrain';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import StationRoutes from '../scripts/interfaces/StationRoutes'; import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
@@ -15,7 +15,7 @@ import {
getScheduledTrain, getScheduledTrain,
parseSpawns, parseSpawns,
} from '../scripts/utils/storeUtils'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from '../scripts/interfaces/store/storeTypes';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
@@ -24,6 +24,7 @@ export const useStore = defineStore('store', {
stationList: [], stationList: [],
trainList: [], trainList: [],
routesList: [],
sceneryData: [], sceneryData: [],
lastDispatcherStatuses: [], lastDispatcherStatuses: [],
@@ -53,10 +54,11 @@ export const useStore = defineStore('store', {
trains: DataStatus.Loading, trains: DataStatus.Loading,
}, },
currentStatsTab: 'daily', currentStatsTab: null,
blockScroll: false, blockScroll: false,
listenerLaunched: false, listenerLaunched: false,
modalLastClickedTarget: null
} as StoreState), } as StoreState),
actions: { actions: {
@@ -101,6 +103,7 @@ export const useStore = defineStore('store', {
isTimeout: train.isTimeout, isTimeout: train.isTimeout,
isSupporter: train.driverIsSupporter, isSupporter: train.driverIsSupporter,
driverLevel: train.driverLevel,
timetableData: timetable timetableData: timetable
? { ? {
@@ -114,8 +117,8 @@ export const useStore = defineStore('store', {
sceneries: timetable.sceneries, sceneries: timetable.sceneries,
} }
: undefined, : undefined,
}; } as Train;
}) as Train[]; });
}, },
getDispatcherStatus(onlineStationData: StationAPIData) { getDispatcherStatus(onlineStationData: StationAPIData) {
@@ -284,56 +287,41 @@ export const useStore = defineStore('store', {
}, },
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await ( const sceneryData: StationJSONData[] = await (await axios.get(`${URLs.stacjownikAPI}/api/getSceneries`)).data;
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
).data;
if (!sceneryData) { if (!sceneryData) {
this.dataStatuses.sceneries = DataStatus.Error; this.dataStatuses.sceneries = DataStatus.Error;
return; return;
} }
this.stationList = sceneryData.map((scenery) => ({ this.stationList = sceneryData.map((scenery) => {
name: scenery.name, return {
name: scenery.name,
generalInfo: { generalInfo: {
...scenery, ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes:
scenery.routes scenery.routesInfo.reduce(
?.split(';') (acc, route) => {
.filter((routeString) => routeString) const propName: keyof StationRoutes = `${route.routeTracks == 2 ? 'twoWay' : 'oneWay'}${
.reduce( route.isElectric ? '' : 'No'
(acc, routeString) => { }CatenaryRouteNames`;
const specs1 = routeString.split('_')[0];
const isInternal = specs1.startsWith('!');
const name = isInternal ? specs1.replace('!', '') : specs1;
const specs2 = routeString.split('_')[1].split(''); acc[route.routeTracks == 2 ? 'twoWay' : 'oneWay'].push({
const twoWay = specs2[0] == '2'; name: route.routeName,
const catenary = specs2[1] == 'E'; SBL: route.isRouteSBL,
const SBL = specs2[2] == 'S'; TWB: false,
const TWB = specs2[3] ? true : false; catenary: route.isElectric,
isInternal: route.isInternal,
const propName = twoWay tracks: route.routeTracks,
? catenary length: route.routeLength,
? 'twoWayCatenaryRouteNames' speed: route.routeSpeed,
: 'twoWayNoCatenaryRouteNames'
: catenary
? 'oneWayCatenaryRouteNames'
: 'oneWayNoCatenaryRouteNames';
acc[twoWay ? 'twoWay' : 'oneWay'].push({
name,
SBL,
TWB,
catenary,
isInternal,
tracks: twoWay ? 2 : 1,
}); });
if (!isInternal) acc[propName].push(name);
if (SBL) acc['sblRouteNames'].push(name); if (!route.isInternal) acc[propName].push(route.routeName);
if (route.isRouteSBL) acc['sblRouteNames'].push(route.routeName);
return acc; return acc;
}, },
@@ -347,19 +335,19 @@ export const useStore = defineStore('store', {
twoWayNoCatenaryRouteNames: [], twoWayNoCatenaryRouteNames: [],
} as StationRoutes } as StationRoutes
) || {}, ) || {},
checkpoints: scenery.checkpoints checkpoints: scenery.checkpoints
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [], : [],
}, },
})); };
});
}, },
connectToWebsocket() { connectToWebsocket() {
const socket = io(URLs.stacjownikAPI, { const socket = io(URLs.stacjownikAPI, {
transports: ['websocket', 'polling'], // transports: ['websocket', 'polling'],
rememberUpgrade: true, rememberUpgrade: true,
reconnection: true, reconnection: true,
timeout: 2000,
}); });
socket.on('connect_error', (err) => { socket.on('connect_error', (err) => {
@@ -373,8 +361,9 @@ export const useStore = defineStore('store', {
}); });
socket.emit('FETCH_DATA', {}, (data: APIData) => { socket.emit('FETCH_DATA', {}, (data: APIData) => {
this.apiData = data;
this.dataStatuses.connection = DataStatus.Loaded; this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = data;
this.setOnlineData(); this.setOnlineData();
}); });
+18 -17
View File
@@ -1,21 +1,5 @@
@import 'responsive.scss'; @import 'responsive.scss';
@import 'animations.scss';
// Animations
.warning {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active {
transition: all 150ms 100ms ease-out;
}
&-leave-active {
transition: all 150ms 100ms ease-out;
}
}
//Styles //Styles
.list_wrapper { .list_wrapper {
@@ -26,6 +10,10 @@
padding-right: 0.2em; padding-right: 0.2em;
} }
.journal-list {
position: relative;
}
.journal_wrapper { .journal_wrapper {
max-width: 1350px; max-width: 1350px;
width: 100%; width: 100%;
@@ -35,6 +23,15 @@
padding: 1em 0; padding: 1em 0;
} }
.journal_refreshed-date {
background-color: #333;
color: #ddd;
text-align: end;
padding: 0.25em;
margin: 0.5em 0;
}
.journal_warning { .journal_warning {
text-align: center; text-align: center;
font-size: 1.3em; font-size: 1.3em;
@@ -83,6 +80,10 @@
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
.journal_refreshed-date {
text-align: center;
}
} }
@media (orientation: landscape) { @media (orientation: landscape) {
-11
View File
@@ -1,11 +0,0 @@
.scenery-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
+31
View File
@@ -0,0 +1,31 @@
.list-anim-move,
.list-anim-enter-active,
.list-anim-leave-active {
transition: all 250ms ease;
}
.list-anim-enter-from,
.list-anim-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-anim-leave-active {
position: absolute;
width: 100%;
}
.status-anim {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active {
transition: all 100ms ease-out;
}
&-leave-active {
transition: all 100ms ease-out;
}
}
+99 -28
View File
@@ -1,28 +1,99 @@
.badge { .badge {
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
padding: 0; padding: 0;
background: #585858; margin: 0.25em;
margin: 0.25em; span {
display: inline-block;
span { background: #585858;
display: inline-block; padding: 0.2em 0.4em;
padding: 0.2em 0.4em; }
}
&-none {
&-none { font-weight: 600;
font-weight: 600;
padding: 0.2em 0.4em;
padding: 0.2em 0.4em; background: firebrick;
background: firebrick;
text-align: center;
text-align: center;
@include smallScreen() {
@include smallScreen() { font-size: 1em;
font-size: 1em; }
} }
} }
}
.level-badge {
display: flex;
justify-content: center;
align-items: center;
&.driver {
border-radius: 50%;
width: 1.7em;
height: 1.7em;
}
&.dispatcher {
border-radius: 0.25em;
width: 1.6em;
height: 1.6em;
}
}
.region-badge {
padding: 0.25em 0.5em;
border-radius: 0.5em;
font-weight: bold;
color: white;
&[aria-describedby='eu'] {
background-color: forestgreen;
}
&[aria-describedby='cae'] {
background-color: lightcoral;
color: black;
}
&[aria-describedby='usw'] {
background-color: lightblue;
color: black;
}
&[aria-describedby='us'] {
background-color: lightblue;
color: black;
}
&[aria-describedby='ru'] {
background-color: lightslategray;
}
}
.train-badge {
padding: 0.1em 0.2em;
border-radius: 0.2em;
font-weight: bold;
font-size: 0.9em;
&.twr {
background-color: var(--clr-twr);
box-shadow: 0 0 5px 1px var(--clr-twr);
color: black;
}
&.skr {
background-color: var(--clr-skr);
box-shadow: 0 0 5px 1px var(--clr-skr);
}
&.offline {
background-color: #be3728;
}
}
+13 -8
View File
@@ -21,17 +21,16 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
overflow-x: hidden; overflow: hidden;
background: #202020da; background: #202020e8;
box-shadow: 0 0 15px 5px #303030; box-shadow: 0 0 15px 0 black;
border: 1px solid #202020e8;
width: 600px; width: 95%;
max-width: 700px;
@include smallScreen { max-height: 95vh;
width: 100%;
height: 80vh;
}
&-exit { &-exit {
position: absolute; position: absolute;
@@ -46,3 +45,9 @@
cursor: pointer; cursor: pointer;
} }
} }
@include smallScreen {
.card {
max-height: 85vh;
}
}
+28 -27
View File
@@ -6,13 +6,9 @@
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.filter-button .active-indicator { .actions-bar {
width: 7px; display: flex;
height: 7px; gap: 0.5em;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
} }
h1.option-title { h1.option-title {
@@ -62,7 +58,7 @@ h1.option-title {
box-shadow: 0 5px 10px 2px #0f0f0f; box-shadow: 0 5px 10px 2px #0f0f0f;
width: 97%; width: 97%;
max-width: 500px; max-width: 550px;
padding: 1em; padding: 1em;
z-index: 100; z-index: 100;
@@ -72,24 +68,30 @@ h1.option-title {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
padding: 0.25em 0.25em 0 0; padding: 0.25em 0.25em 0 0;
} }
.options_filter-sections section {
margin: 0.5em 0;
}
.options_filters { .options_filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0.5em 0 0 0;
gap: 0.5em;
margin: 0.25em 0;
} }
.sort-option, .sort-option,
.filter-option { .filter-option {
margin: 0.25em 0.25em 0.25em 0; padding: 0.25em 0.5em;
} }
.sort-option[data-selected='true'] { .sort-option[data-selected='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold;
} }
.filter-option { .filter-option {
@@ -111,17 +113,6 @@ h1.option-title {
margin: 0.5em auto; margin: 0.5em auto;
} }
.search_actions {
display: flex;
gap: 0.5em;
margin: 1em 0;
width: 100%;
button {
width: 100%;
}
}
.search-box { .search-box {
.search-exit { .search-exit {
position: absolute; position: absolute;
@@ -132,6 +123,17 @@ h1.option-title {
} }
} }
.options_actions {
display: flex;
gap: 0.5em;
width: 100%;
margin-top: 1em;
button {
width: 100%;
}
}
@include smallScreen() { @include smallScreen() {
h1 { h1 {
text-align: center; text-align: center;
@@ -148,13 +150,12 @@ h1.option-title {
max-width: 100%; max-width: 100%;
} }
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.options_filters, .options_filters,
.options_sorters { .options_sorters {
justify-content: center; justify-content: center;
} }
.filter-section {
text-align: center;
}
} }
+17 -23
View File
@@ -18,19 +18,21 @@
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 1rem; width: 15px;
height: 1rem; height: 15px;
background-color: transparent; background-color: transparent;
&-track { &-track {
border-radius: 0.5em;
background-color: #333; background-color: #333;
} }
&-thumb { &-thumb {
border-radius: 0.5em;
background-color: #666; background-color: #666;
} }
&-corner {
background-color: #333;
}
} }
html { html {
@@ -43,11 +45,12 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-weight: 500;
overflow-y: scroll; overflow-y: scroll;
&.no-scroll { &.no-scroll {
overflow-y: hidden; overflow-y: hidden;
padding-right: 1rem; padding-right: 15px;
@include smallScreen() { @include smallScreen() {
padding: 0; padding: 0;
@@ -79,22 +82,9 @@ body {
transition: opacity 0.3s; transition: opacity 0.3s;
padding: 0.25em; padding: 0.25em;
// @include smallScreen() {
// right: 0;
// left: 0;
// &::after {
// left: 75%;
// }
// }
} }
&:hover > .content { &:hover > .content {
// @include smallScreen() {
// display: none;
// }
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
@@ -103,7 +93,6 @@ body {
button, button,
input, input,
select { select {
// font-family: "Open Sans", sans-serif;
border: none; border: none;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-size: 1em; font-size: 1em;
@@ -149,6 +138,15 @@ input {
padding: 0.35em 0; padding: 0.35em 0;
} }
.active-indicator {
width: 7px;
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
a { a {
display: inline-block; display: inline-block;
@@ -213,10 +211,6 @@ button {
pointer-events: none; pointer-events: none;
opacity: 0.85; opacity: 0.85;
} }
&[data-inactive='true'] {
opacity: 0.55;
}
} }
button.btn--filled { button.btn--filled {
+16 -11
View File
@@ -1,18 +1,23 @@
@mixin smallScreen() { @mixin smallScreen() {
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
@content; @content;
} }
} }
@mixin midScreen() { @mixin midScreen() {
@media only screen and (max-width: 1150px) { @media only screen and (max-width: 1150px) {
@content; @content;
} }
}
@mixin screenLandscape() {
@media only screen and (orientation: landscape) and (max-device-height: 450px) {
@content;
}
} }
@mixin bigScreen() { @mixin bigScreen() {
@media only screen and (min-width: 2000px) { @media only screen and (min-width: 2000px) {
@content; @content;
} }
} }
+46
View File
@@ -0,0 +1,46 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table {
width: 100%;
border-collapse: collapse;
thead {
position: sticky;
top: 0;
background-color: #222222;
}
th {
padding: 0.5em;
}
tr {
background-color: #353535;
border: none;
}
td {
padding: 0.75em;
border-bottom: solid 5px #111;
}
}
.no-history {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
color: #ccc;
}
.bottom-info {
display: flex;
justify-content: center;
button {
padding: 0.5em;
}
}
-56
View File
@@ -1,56 +0,0 @@
$free: #8a8a8a;
$ending: #e6c300;
$no-limit: #117fc9;
$unav: #ff3d5d;
$brb: #e6a100;
$no-space: #222;
$taken: #09a116;
$unknown: rgb(185, 60, 60);
.status-badge {
border-radius: 1rem;
font-weight: 500;
padding: 0.2em .55em;
background-color: $taken;
&.free {
background-color: $free;
font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.brb {
background-color: $brb;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
color: white;
font-size: 0.85em;
}
&.unknown {
background-color: $unknown;
font-size: 0.95em;
}
}
@@ -1,18 +0,0 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
export type JournalTimetableSearchType = {
[key in JournalTimetableSearchKey]: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
-6
View File
@@ -1,6 +0,0 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+270 -288
View File
@@ -1,288 +1,270 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <JournalOptions
@on-search-confirm="fetchHistoryData" @on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions" @on-options-reset="resetOptions"
:sorter-option-ids="['timestampFrom', 'duration']" @on-refresh-data="fetchHistoryData(true)"
:data-status="dataStatus" :sorter-option-ids="['timestampFrom', 'duration']"
:current-options-active="currentOptionsActive" :data-status="dataStatus"
/> :current-options-active="currentOptionsActive"
optionsType="dispatchers"
<div class="list_wrapper" @scroll="handleScroll"> />
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> --> <div class="journal_refreshed-date" v-if="dataRefreshedAt">
<div class="journal_warning" v-if="store.isOffline"> {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
{{ $t('app.offline') }} </div>
</div>
<div class="list_wrapper" @scroll="handleScroll">
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" /> <JournalDispatchersList
:dispatcherHistory="computedHistoryList"
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> :addHistoryData="addHistoryData"
{{ $t('app.error') }} :dataStatus="dataStatus"
</div> :scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
<div class="journal_warning" v-else-if="historyList.length == 0"> />
{{ $t('app.no-result') }} </div>
</div> </div>
</section>
<div v-else> </template>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<script lang="ts">
<button import { defineComponent, provide, reactive, Ref, ref } from 'vue';
class="btn btn--option btn--load-data" import axios from 'axios';
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="addHistoryData" import ActionButton from '../components/Global/ActionButton.vue';
> import JournalOptions from '../components/JournalView/JournalOptions.vue';
{{ $t('journal.load-data') }} import DispatcherStats from '../components/JournalView/DispatcherStats.vue';
</button> import SearchBox from '../components/Global/SearchBox.vue';
</div>
<!-- </div> import Loading from '../components/Global/Loading.vue';
</transition> --> import { URLs } from '../scripts/utils/apiURLs';
import { DataStatus } from '../scripts/enums/DataStatus';
<div class="journal_warning" v-if="scrollNoMoreData"> import { useStore } from '../store/store';
{{ $t('journal.no-further-data') }} import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
</div> import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../scripts/types/JournalDispatcherTypes';
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData';
<div class="journal_warning" v-else-if="!scrollDataLoaded"> import JournalHeader from '../components/JournalView/JournalHeader.vue';
{{ $t('journal.loading-further-data') }} import { LocationQuery } from 'vue-router';
</div>
</div> const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
</div>
</section> export default defineComponent({
</template> components: {
SearchBox,
<script lang="ts"> ActionButton,
import { defineComponent, provide, reactive, Ref, ref } from 'vue'; JournalOptions,
import axios from 'axios'; DispatcherStats,
Loading,
import ActionButton from '../components/Global/ActionButton.vue'; JournalDispatchersList,
import JournalOptions from '../components/JournalView/JournalOptions.vue'; JournalHeader,
import DispatcherStats from '../components/JournalView/DispatcherStats.vue'; },
import SearchBox from '../components/Global/SearchBox.vue'; name: 'JournalDispatchers',
import Loading from '../components/Global/Loading.vue'; props: {
import { URLs } from '../scripts/utils/apiURLs'; sceneryName: {
import { DataStatus } from '../scripts/enums/DataStatus'; type: String,
import { useStore } from '../store/store'; required: false,
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue'; },
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../types/Journal/JournalDispatcherTypes';
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData'; dispatcherName: {
import JournalHeader from '../components/JournalView/JournalHeader.vue'; type: String,
import { LocationQuery } from 'vue-router'; required: false,
},
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`; },
export default defineComponent({ data: () => ({
components: { currentQuery: '',
SearchBox, currentQueryArray: [] as string[],
ActionButton, dataRefreshedAt: null as Date | null,
JournalOptions,
DispatcherStats, scrollDataLoaded: true,
Loading, scrollNoMoreData: false,
JournalDispatchersList,
JournalHeader, showReturnButton: false,
}, statsCardOpen: false,
name: 'JournalDispatchers', currentOptionsActive: false,
props: { dataStatus: DataStatus.Loading,
sceneryName: { DataStatus,
type: String,
required: false, historyList: [] as DispatcherHistory[],
}, }),
dispatcherName: { setup() {
type: String, const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
required: false, const journalFilterActive = ref({});
},
}, const searchersValues = reactive({
'search-dispatcher': '',
data: () => ({ 'search-station': '',
currentQuery: '', 'search-date': '',
currentQueryArray: [] as string[], } as JournalDispatcherSearcher);
scrollDataLoaded: true, const countFromIndex = ref(0);
scrollNoMoreData: false, const countLimit = 15;
showReturnButton: false, provide('sorterActive', sorterActive);
statsCardOpen: false, provide('journalFilterActive', journalFilterActive);
currentOptionsActive: false, provide('searchersValues', searchersValues);
provide('filterList', reactive([]));
dataStatus: DataStatus.Initialized,
DataStatus, const scrollElement: Ref<HTMLElement | null> = ref(null);
historyList: [] as DispatcherHistory[], return {
}), store: useStore(),
setup() { sorterActive,
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 }); searchersValues,
const journalFilterActive = ref({});
countFromIndex,
const searchersValues = reactive({ countLimit,
'search-dispatcher': '',
'search-station': '', scrollElement,
'search-date': '', maxCount: ref(15),
} as JournalDispatcherSearcher); };
},
const countFromIndex = ref(0);
const countLimit = 15; watch: {
currentQueryArray(q: string[]) {
provide('sorterActive', sorterActive); this.currentOptionsActive =
provide('journalFilterActive', journalFilterActive); q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
provide('searchersValues', searchersValues); },
},
const scrollElement: Ref<HTMLElement | null> = ref(null);
computed: {
return { computedHistoryList() {
store: useStore(), return this.historyList.filter(
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
sorterActive, );
searchersValues, },
},
countFromIndex,
countLimit, beforeRouteUpdate(to, _) {
this.handleQueries(to.query);
scrollElement, this.fetchHistoryData();
maxCount: ref(15), },
};
}, activated() {
this.handleQueries(this.$route.query);
watch: { this.fetchHistoryData();
currentQueryArray(q: string[]) { },
this.currentOptionsActive =
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom'); methods: {
}, handleScroll(e: Event) {
}, const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
computed: { const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
computedHistoryList() {
return this.historyList.filter( if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
); if (scrollTop > elementHeight * 0.85) this.addHistoryData();
}, },
},
handleQueries(query: LocationQuery) {
beforeRouteUpdate(to, _) { const queryKeys = Object.keys(query);
this.handleQueries(to.query);
this.fetchHistoryData(); if (queryKeys.includes('sceneryName')) this.setSearchers('', `${query.sceneryName}`, '');
}, if (queryKeys.includes('dispatcherName')) this.setSearchers('', '', `${query.dispatcherName}`);
},
activated() {
this.handleQueries(this.$route.query); setSearchers(date: string, station: string, dispatcher: string) {
this.fetchHistoryData(); this.searchersValues['search-date'] = date;
}, this.searchersValues['search-station'] = station;
this.searchersValues['search-dispatcher'] = dispatcher;
methods: { },
handleScroll(e: Event) {
const listElement = e.target as HTMLElement; resetOptions() {
const scrollTop = listElement.scrollTop; this.setSearchers('', '', '');
const elementHeight = listElement.scrollHeight - listElement.offsetHeight; this.sorterActive.id = 'timestampFrom';
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return; this.fetchHistoryData();
},
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
}, async addHistoryData() {
this.scrollDataLoaded = false;
handleQueries(query: LocationQuery) {
if ('sceneryName' in query) this.searchersValues['search-station'] = `${query.sceneryName}`; this.countFromIndex = this.historyList.length;
if ('dispatcherName' in query) this.searchersValues['search-dispatcher'] = `${query.dispatcherName}`;
}, const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${this.countFromIndex}`)
setSearchers(date: string, station: string, dispatcher: string) { ).data;
this.searchersValues['search-date'] = date;
this.searchersValues['search-station'] = station; if (!responseData) return;
this.searchersValues['search-dispatcher'] = dispatcher;
}, if (responseData.length == 0) {
this.scrollNoMoreData = true;
resetOptions() { return;
this.setSearchers('', '', ''); }
this.sorterActive.id = 'timestampFrom';
this.historyList.push(...responseData);
this.fetchHistoryData(); this.scrollDataLoaded = true;
}, },
async addHistoryData() { async fetchHistoryData(reset = false) {
this.scrollDataLoaded = false; const queries: string[] = [];
const countFrom = this.historyList.length; const dispatcher = this.searchersValues['search-dispatcher'].trim();
const station = this.searchersValues['search-station'].trim();
const responseData: DispatcherHistory[] = await ( const dateString = this.searchersValues['search-date'].trim();
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data; const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (!responseData) return;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (responseData.length == 0) { if (station) queries.push(`stationName=${station}`);
this.scrollNoMoreData = true; if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
return;
} // API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
this.historyList.push(...responseData); else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
this.scrollDataLoaded = true; else queries.push('sortBy=timestampFrom');
},
queries.push('countLimit=30');
async fetchHistoryData() {
const queries: string[] = []; if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
const dispatcher = this.searchersValues['search-dispatcher'].trim(); this.currentQuery = queries.join('&');
const station = this.searchersValues['search-station'].trim(); this.currentQueryArray = queries;
const dateString = this.searchersValues['search-date'].trim();
try {
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined; if (reset) this.dataStatus = DataStatus.Loading;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
const responseData: DispatcherHistory[] = await (
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`); await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
if (station) queries.push(`stationName=${station}`); ).data;
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
if (!responseData) {
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; this.dataStatus = DataStatus.Error;
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom'); return;
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration'); }
else queries.push('sortBy=timestampFrom');
if (!responseData) return;
queries.push('countLimit=30');
// Response data exists
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading; this.historyList = responseData;
this.currentQuery = queries.join('&'); // Stats display
this.currentQueryArray = queries; this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
try { ? this.historyList[0].dispatcherName
const responseData: DispatcherHistory[] = await ( : '';
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data; this.dataRefreshedAt = new Date();
this.dataStatus = DataStatus.Loaded;
if (!responseData) { } catch (error) {
this.dataStatus = DataStatus.Error; this.dataStatus = DataStatus.Error;
return; }
}
this.scrollNoMoreData = false;
if (!responseData) return; this.scrollDataLoaded = true;
},
// Response data exists },
this.historyList = responseData; });
</script>
// Stats display
this.store.dispatcherStatsName = <style lang="scss" scoped>
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() @import '../styles/JournalSection.scss';
? this.historyList[0].dispatcherName </style>
: '';
this.dataStatus = DataStatus.Loaded;
} catch (error) {
this.dataStatus = DataStatus.Error;
}
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
@import '../styles/JournalSection.scss';
</style>
+328 -301
View File
@@ -1,301 +1,328 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <JournalOptions
@on-search-confirm="fetchHistoryData" @on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions" @on-options-reset="resetOptions"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']" @on-refresh-data="fetchHistoryData"
:filters="journalTimetableFilters" :sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
:currentOptionsActive="currentOptionsActive" :filters="journalTimetableFilters"
:data-status="dataStatus" :currentOptionsActive="currentOptionsActive"
/> :data-status="dataStatus"
optionsType="timetables"
<JournalStats /> />
<div class="list_wrapper" @scroll="handleScroll"> <JournalStats />
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> --> <div class="journal_refreshed-date" v-if="dataRefreshedAt">
<div class="journal_warning" v-if="store.isOffline"> {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
{{ $t('app.offline') }} </div>
</div>
<div class="list_wrapper" @scroll="handleScroll">
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" /> <JournalTimetablesList
:timetableHistory="timetableHistory"
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> :addHistoryData="addHistoryData"
{{ $t('app.error') }} :dataStatus="dataStatus"
</div> :scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
<div v-else-if="timetableHistory.length == 0" class="journal_warning"> />
{{ $t('app.no-result') }} </div>
</div> </div>
</section>
<div v-else> </template>
<JournalTimetablesList :timetableHistory="timetableHistory" />
<script lang="ts">
<button import { defineComponent, provide, reactive, Ref, ref } from 'vue';
class="btn btn--option btn--load-data" import axios from 'axios';
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData" import imageMixin from '../mixins/imageMixin';
> import dateMixin from '../mixins/dateMixin';
{{ $t('journal.load-data') }} import routerMixin from '../mixins/routerMixin';
</button> import modalTrainMixin from '../mixins/modalTrainMixin';
</div>
<!-- </div> --> import DriverStats from '../components/JournalView/JournalDriverStats.vue';
<!-- </transition> --> import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalStats from '../components/JournalView/JournalStats.vue';
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> import JournalHeader from '../components/JournalView/JournalHeader.vue';
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
</div> import Loading from '../components/Global/Loading.vue';
</div>
</section> import { DataStatus } from '../scripts/enums/DataStatus';
</template> import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../scripts/utils/apiURLs';
<script lang="ts"> import { useStore } from '../store/store';
import { defineComponent, provide, reactive, Ref, ref, watch } from 'vue';
import axios from 'axios'; import { LocationQuery } from 'vue-router';
import { TimetablesQueryParams } from '../scripts/interfaces/api/TimetablesQueryParams';
import DriverStats from '../components/JournalView/JournalDriverStats.vue'; import { JournalFilterType } from '../scripts/enums/JournalFilterType';
import Loading from '../components/Global/Loading.vue'; import {
import { JournalTimetableFilter, JournalTimetableSorter } from '../types/Journal/JournalTimetablesTypes'; JournalFilter,
import dateMixin from '../mixins/dateMixin'; JournalTimetableSearchType,
import routerMixin from '../mixins/routerMixin'; JournalTimetableSorter,
import { DataStatus } from '../scripts/enums/DataStatus'; } from '../scripts/types/JournalTimetablesTypes';
import { JournalFilterType } from '../scripts/enums/JournalFilterType'; import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../scripts/utils/apiURLs'; const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
import { useStore } from '../store/store';
import JournalOptions from '../components/JournalView/JournalOptions.vue'; export default defineComponent({
import { JournalTimetableSearchType } from '../types/Journal/JournalTimetablesTypes'; components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader },
import modalTrainMixin from '../mixins/modalTrainMixin'; mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
import imageMixin from '../mixins/imageMixin';
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue'; name: 'JournalTimetables',
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
import JournalStats from '../components/JournalView/JournalStats.vue'; props: {
import JournalHeader from '../components/JournalView/JournalHeader.vue'; timetableId: {
import { LocationQuery } from 'vue-router'; type: String,
},
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`; },
export default defineComponent({ data: () => ({
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader }, currentQueryParams: {} as TimetablesQueryParams,
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin], dataRefreshedAt: null as Date | null,
name: 'JournalTimetables', scrollDataLoaded: true,
scrollNoMoreData: false,
props: {
timetableId: { showReturnButton: false,
type: String, statsCardOpen: false,
}, currentOptionsActive: false,
},
timetableHistory: [] as TimetableHistory[],
data: () => ({ journalTimetableFilters,
currentQuery: '',
currentQueryArray: [] as string[], dataStatus: DataStatus.Loading,
dataErrorMessage: '',
scrollDataLoaded: true,
scrollNoMoreData: false, DataStatus,
}),
showReturnButton: false,
statsCardOpen: false, setup() {
currentOptionsActive: false, const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 'desc' });
// const journalFilterActive = ref(journalTimetableFilters[0]);
timetableHistory: [] as TimetableHistory[], const initFilters: readonly JournalFilter[] = JSON.parse(JSON.stringify(journalTimetableFilters));
journalTimetableFilters, const filterList: JournalFilter[] = reactive(JSON.parse(JSON.stringify(initFilters)));
dataStatus: DataStatus.Initialized, const searchersValues = reactive({
dataErrorMessage: '', 'search-train': '',
'search-driver': '',
DataStatus, 'search-dispatcher': '',
}), 'search-issuedFrom': '',
'search-date': '',
setup() { } as JournalTimetableSearchType);
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
const journalFilterActive = ref(journalTimetableFilters[0]); const countFromIndex = ref(0);
const countLimit = 15;
const searchersValues = reactive({
'search-train': '', provide('searchersValues', searchersValues);
'search-driver': '', provide('sorterActive', sorterActive);
'search-dispatcher': '', provide('filterList', filterList);
'search-date': '',
} as JournalTimetableSearchType); const scrollElement: Ref<HTMLElement | null> = ref(null);
const countFromIndex = ref(0); return {
const countLimit = 15; sorterActive,
searchersValues,
provide('searchersValues', searchersValues); filterList,
provide('sorterActive', sorterActive); initFilters,
provide('journalFilterActive', journalFilterActive);
countFromIndex,
const scrollElement: Ref<HTMLElement | null> = ref(null); countLimit,
return { scrollElement,
sorterActive,
journalFilterActive, store: useStore(),
searchersValues, };
},
countFromIndex,
countLimit, watch: {
currentQueryParams(q: TimetablesQueryParams) {
scrollElement, this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
},
store: useStore(), },
};
}, // Handle route updates for route-links
beforeRouteUpdate(to, _) {
watch: { this.handleQueries(to.query);
currentQueryArray(q: string[]) { this.fetchHistoryData();
this.currentOptionsActive = },
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timetableId');
}, activated() {
}, this.handleQueries(this.$route.query);
this.fetchHistoryData();
// Handle route updates for route-links },
beforeRouteUpdate(to, _) {
this.handleQueries(to.query); methods: {
this.fetchHistoryData(); handleScroll(e: Event) {
}, const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
activated() { const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
this.handleQueries(this.$route.query);
this.fetchHistoryData(); if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
},
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
methods: { },
handleScroll(e: Event) {
const listElement = e.target as HTMLElement; handleQueries(query: LocationQuery) {
const scrollTop = listElement.scrollTop; const queryKeys = Object.keys(query);
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (queryKeys.includes('timetableId')) this.setSearchers('', '', `#${query.timetableId}`, '', '');
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return; if (queryKeys.includes('issuedFrom')) this.setSearchers('', '', '', '', `${query.issuedFrom}`);
if (queryKeys.includes('authorName')) this.setSearchers('', '', '', `${query.authorName}`, '');
if (scrollTop > elementHeight * 0.85) this.addHistoryData(); },
},
setSearchers(date: string, driver: string, train: string, dispatcher: string, issuedFrom: string) {
handleQueries(query: LocationQuery) { this.searchersValues['search-date'] = date;
if ('timetableId' in query) this.searchersValues['search-train'] = `#${query.timetableId}`; this.searchersValues['search-driver'] = driver;
}, this.searchersValues['search-train'] = train;
this.searchersValues['search-dispatcher'] = dispatcher;
setSearchers(date: string, driver: string, train: string, dispatcher: string) { this.searchersValues['search-issuedFrom'] = issuedFrom;
this.searchersValues['search-date'] = date; },
this.searchersValues['search-driver'] = driver;
this.searchersValues['search-train'] = train; resetOptions() {
this.searchersValues['search-dispatcher'] = dispatcher; this.setSearchers('', '', '', '', '');
},
this.sorterActive.id = 'timetableId';
resetOptions() {
this.setSearchers('', '', '', ''); this.filterList.forEach(
(f) => (f.isActive = this.initFilters.find((initFilter) => initFilter.id == f.id)?.isActive || false)
this.journalFilterActive = this.journalTimetableFilters[0]; );
this.sorterActive.id = 'timetableId';
this.fetchHistoryData();
this.fetchHistoryData(); },
},
async addHistoryData() {
async addHistoryData() { this.scrollDataLoaded = false;
this.scrollDataLoaded = false;
this.currentQueryParams['countFrom'] = this.timetableHistory.length;
const countFrom = this.timetableHistory.length;
const responseData: TimetableHistory[] = await (
const responseData: TimetableHistory[] = await ( await axios.get(`${TIMETABLES_API_URL}`, {
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`) params: { ...this.currentQueryParams },
).data; })
).data;
if (!responseData) return;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true; if (responseData.length == 0) {
return; this.scrollNoMoreData = true;
} return;
}
this.timetableHistory.push(...responseData);
this.scrollDataLoaded = true; this.timetableHistory.push(...responseData);
}, this.scrollDataLoaded = true;
},
async fetchHistoryData() {
const queries: string[] = []; async fetchHistoryData() {
const driverName = this.searchersValues['search-driver'].trim() || undefined;
const driverName = this.searchersValues['search-driver'].trim(); const trainNo = this.searchersValues['search-train'].trim() || undefined;
const trainNo = this.searchersValues['search-train'].trim(); const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const authorName = this.searchersValues['search-dispatcher'].trim(); const dateString = this.searchersValues['search-date'].trim() || undefined;
const dateString = this.searchersValues['search-date'].trim(); const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined; const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined; const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (driverName) queries.push(`driverName=${driverName}`); const queryParams: TimetablesQueryParams = {};
if (trainNo)
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`); this.filterList
if (authorName) queries.push(`authorName=${authorName}`); .filter((f) => f.isActive)
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`); .forEach((f) => {
switch (f.id) {
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; case JournalFilterType.ABANDONED:
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance'); queryParams['fulfilled'] = 0;
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount'); queryParams['terminated'] = 1;
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate'); break;
else queries.push('sortBy=timetableId');
case JournalFilterType.ACTIVE:
queries.push('countLimit=15'); queryParams['fulfilled'] = undefined;
queryParams['terminated'] = 0;
switch (this.journalFilterActive.id) { break;
case JournalFilterType.abandoned:
queries.push('fulfilled=0', 'terminated=1'); case JournalFilterType.FULFILLED:
break; queryParams['terminated'] = undefined;
queryParams['fulfilled'] = 1;
case JournalFilterType.active: break;
queries.push('terminated=0');
break; case JournalFilterType.ALL:
queryParams['terminated'] = undefined;
case JournalFilterType.fulfilled: queryParams['fulfilled'] = undefined;
queries.push('fulfilled=1'); break;
break;
case JournalFilterType.TWR_SKR:
default: queryParams['twr'] = undefined;
break; queryParams['skr'] = undefined;
} break;
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading; case JournalFilterType.TWR:
queryParams['twr'] = 1;
this.currentQuery = queries.join('&'); queryParams['skr'] = undefined;
this.currentQueryArray = queries; break;
try { case JournalFilterType.SKR:
const responseData: TimetableHistory[] = await ( queryParams['twr'] = undefined;
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`) queryParams['skr'] = 1;
).data; break;
if (!responseData) { default:
this.dataStatus = DataStatus.Error; break;
this.dataErrorMessage = 'Brak danych!'; }
return; });
}
queryParams['driverName'] = driverName;
if (!responseData) return; queryParams[trainNo?.startsWith('#') ? 'timetableId' : 'trainNo'] = trainNo?.replace('#', '');
queryParams['countFrom'] = undefined;
// Response data exists queryParams['countLimit'] = undefined;
this.timetableHistory = responseData;
queryParams['authorName'] = authorName;
// Stats display queryParams['timestampFrom'] = timestampFrom;
this.store.driverStatsName = queryParams['timestampTo'] = timestampTo;
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim() queryParams['issuedFrom'] = issuedFrom;
? this.timetableHistory[0].driverName queryParams['sortBy'] = this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
: '';
if (JSON.stringify(this.currentQueryParams) != JSON.stringify(queryParams)) this.dataStatus = DataStatus.Loading;
this.dataStatus = DataStatus.Loaded;
} catch (error) { this.currentQueryParams = queryParams;
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!'; try {
} const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}`, {
this.scrollNoMoreData = false; params: this.currentQueryParams,
this.scrollDataLoaded = true; })
}, ).data;
},
}); if (!responseData) {
</script> this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Brak danych!';
<style lang="scss" scoped> return;
@import '../styles/JournalSection.scss'; }
</style>
if (!responseData) return;
// Response data exists
this.timetableHistory = responseData;
// Stats display
this.store.driverStatsName =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
? this.timetableHistory[0].driverName
: '';
this.dataStatus = DataStatus.Loaded;
this.dataRefreshedAt = new Date();
} catch (error) {
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
}
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
@import '../styles/JournalSection.scss';
</style>
+3 -8
View File
@@ -33,12 +33,7 @@
</div> </div>
<keep-alive> <keep-alive>
<component <component :is="currentViewCompontent" :station="stationInfo" :key="currentViewCompontent"></component>
:is="currentViewCompontent"
:station="stationInfo"
:timetableOnly="timetableOnly"
:key="currentViewCompontent"
></component>
</keep-alive> </keep-alive>
</div> </div>
</div> </div>
@@ -208,14 +203,14 @@ button.back-btn {
.scenery-right { .scenery-right {
background: #181818; background: #181818;
padding: 2em 0.5em; padding: 1em 0.5em;
height: 95vh; height: 95vh;
min-height: 550px; min-height: 550px;
max-height: 1000px; max-height: 1000px;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr auto;
gap: 1em; gap: 1em;
} }
+1 -15
View File
@@ -14,7 +14,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import StorageManager from '../scripts/managers/storageManager';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import SelectBox from '../components/Global/SelectBox.vue'; import SelectBox from '../components/Global/SelectBox.vue';
@@ -44,25 +43,12 @@ export default defineComponent({
computed: { computed: {
computedStationList() { computedStationList() {
const list = this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id); return this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id);
return list;
}, },
}, },
mounted() { mounted() {
this.filterStore.setupFilters(); this.filterStore.setupFilters();
// this.filterStore.inputs.options.forEach((option) => {
// const value = StorageManager.getBooleanValue(option.name);
// option.value = value;
// this.filterStore.changeFilterValue({ name: option.name, value: value });
// });
// this.filterStore.inputs.sliders.forEach((slider) => {
// const value = StorageManager.getNumericValue(slider.name);
// slider.value = value;
// this.filterStore.changeFilterValue({ name: slider.name, value: value });
// });
}, },
}); });
</script> </script>
+6 -4
View File
@@ -2,7 +2,7 @@
<section class="trains-view"> <section class="trains-view">
<div class="trains_wrapper"> <div class="trains_wrapper">
<TrainOptions <TrainOptions
:sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" :sorter-option-ids="['routeDistance', 'id', 'progress', 'delay', 'mass', 'speed', 'length']"
:current-options-active="currentOptionsActive" :current-options-active="currentOptionsActive"
/> />
@@ -21,7 +21,7 @@ import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { filteredTrainList } from '../scripts/managers/trainFilterManager'; import { filteredTrainList } from '../scripts/managers/trainFilterManager';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import { TrainFilter } from '../types/Trains/TrainOptionsTypes'; import { TrainFilter } from '../scripts/interfaces/Trains/TrainFilter';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -57,7 +57,7 @@ export default defineComponent({
const store = useStore(); const store = useStore();
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))]; const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
const sorterActive = reactive({ id: 'distance', dir: -1 }); const sorterActive = reactive({ id: 'routeDistance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const currentOptionsActive = ref(false); const currentOptionsActive = ref(false);
@@ -77,7 +77,9 @@ export default defineComponent({
watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => { watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive); const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'routeDistance' || areFiltersActive;
console.log(sA.id);
}); });
return { return {
+55 -53
View File
@@ -1,53 +1,55 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
server: { server: {
port: 5001, port: 5001,
}, },
plugins: [ plugins: [
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'prompt', registerType: 'autoUpdate',
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,img}'], globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'), urlPattern: new RegExp('^https://stacjownik.spythere.pl/api/getSceneries', 'i'),
handler: 'NetworkFirst', handler: 'NetworkFirst',
options: { options: {
cacheName: 'sceneries-cache', cacheName: 'sceneries-cache',
expiration: { expiration: {
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days maxEntries: 1,
}, maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
cacheableResponse: { },
statuses: [0, 200], cacheableResponse: {
}, statuses: [0, 200],
}, },
}, },
{ },
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i, {
handler: 'CacheFirst', urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
options: { handler: 'CacheFirst',
cacheName: 'images-cache', options: {
expiration: { cacheName: 'images-cache',
maxEntries: 100, expiration: {
maxAgeSeconds: 60 * 60 * 24 * 60, maxEntries: 100,
}, maxAgeSeconds: 60 * 60 * 24 * 60,
cacheableResponse: { },
statuses: [0, 200, 404], cacheableResponse: {
}, statuses: [0, 200, 404],
}, },
}, },
], },
}, ],
devOptions: { },
enabled: true, devOptions: {
}, enabled: true,
}), },
], }),
}); ],
});
+2257 -2257
View File
File diff suppressed because it is too large Load Diff